Decouple title from title block (#696)

* Decouple title from title block

* fix lint

* remove dry run for FormTitleMigration

* Skip form title migration for forms with hidden titles

* Refactor AI Form Generation with Dedicated Prompt Services

- Extract AI form generation logic from GenerateTemplate command into dedicated prompt service classes
- Update GenerateAiForm job to use new prompt generation services
- Improve GptCompleter with more robust error handling and token tracking
- Add error field to AiFormCompletion model for better error logging
- Simplify command signature from 'ai:make-form-template' to 'form:generate'

* Consolidate Template Metadata Generation with Unified Prompt Service

- Create GenerateTemplateMetadataPrompt to centralize template metadata generation
- Update GenerateTemplate command to use new consolidated metadata generation approach
- Enhance GptCompleter to support strict JSON schema validation
- Increase form prompt max length to support more complex form descriptions
- Refactor form generation to simplify metadata extraction and processing

* Implement Form Mode Strategy for Flexible Form Rendering

- Introduce FormModeStrategy to centralize form rendering logic
- Add support for different form modes: LIVE, PREVIEW, TEST, EDIT, PREFILL
- Refactor components to use mode-based rendering strategy
- Remove legacy boolean props like adminPreview and creating
- Enhance form component flexibility and reusability

* Refine Form Mode Strategy Display Behavior

- Update FormModeStrategy to hide hidden fields in PREVIEW mode
- Add FormMode getter in UrlFormPrefill component for mode-specific rendering
- Clarify mode-specific validation and display logic in form strategies

* Enhance Form Template Generation with Advanced Field Options

- Update GenerateTemplate command to use front_url for template URL output
- Expand GenerateFormPrompt with comprehensive field configuration options
- Add support for advanced field types: date with time, toggle switches, radio/checkbox selections
- Introduce field width configuration and HTML formatting for text elements
- Re-enable select, multi-select, and matrix field type definitions with enhanced configurations

* Remove Deprecated Template Metadata Generation Services

- Delete multiple AI prompt services related to template metadata generation
- Simplify GenerateTemplate command to use default values instead of complex metadata generation
- Remove GenerateTemplateMetadataPrompt and related classes like GenerateTemplateDescriptionPrompt, GenerateTemplateImageKeywordsPrompt, etc.
- Update form template generation to use basic fallback metadata generation approach

* Restore GenerateTemplateMetadataPrompt for Comprehensive Template Generation

- Reintroduce GenerateTemplateMetadataPrompt to replace default metadata generation
- Update GenerateTemplate command to use consolidated metadata generation approach
- Extract detailed metadata components including title, description, industries, and image search query
- Improve template generation with more dynamic and AI-generated metadata

* Refactor Template Preview Section Layout

- Remove unnecessary nested div in template preview section
- Simplify HTML structure for the template preview component
- Maintain existing styling and functionality while improving code readability

* Refactor Constructor and Code Formatting in AI Form Generation and Prompt Classes

- Updated the constructor in GenerateAiForm.php to use a block structure for improved readability and consistency.
- Added a blank line in the Prompt.php file to enhance code formatting and maintain consistency with PHP coding standards.
- Modified the migration file to use a more concise class declaration syntax, improving clarity.

These changes aim to enhance code readability and maintainability across the affected files.

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala
2025-03-21 21:29:18 +05:30
committed by GitHub
parent d2b8572d75
commit aa5c1acf3a
28 changed files with 1345 additions and 457 deletions

View File

@@ -7,10 +7,10 @@
<open-form
:theme="theme"
:loading="false"
:show-hidden="true"
:form="form"
:fields="form.properties"
:default-data-form="submission"
:mode="FormMode.EDIT"
@submit="updateForm"
>
<template #submit-btn="{ submitForm }">
@@ -29,6 +29,7 @@
import { ref, defineProps, defineEmits } from "vue"
import OpenForm from "../forms/OpenForm.vue"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
const props = defineProps({
show: { type: Boolean, required: true },

View File

@@ -6,24 +6,10 @@
:style="{ '--font-family': form.font_family, 'direction': form?.layout_rtl ? 'rtl' : 'ltr' }"
>
<link
v-if="adminPreview && form.font_family"
v-if="formModeStrategy.display.showFontLink && form.font_family"
rel="stylesheet"
:href="getFontUrl"
>
<template v-if="!isHideTitle">
<EditableTag
v-if="adminPreview"
v-model="form.title"
element="h1"
class="mb-2"
/>
<h1
v-else
class="mb-2 px-2"
v-text="form.title"
/>
</template>
<div v-if="isPublicFormPage && form.is_password_protected">
<p class="form-description mb-4 text-gray-700 dark:text-gray-300 px-2">
@@ -95,7 +81,7 @@
</div>
<form-cleanings
v-if="!adminPreview"
v-if="formModeStrategy.display.showFormCleanings"
:hideable="true"
class="mb-4 mx-2"
:form="form"
@@ -114,7 +100,7 @@
:fields="form.properties"
:theme="theme"
:dark-mode="darkMode"
:admin-preview="adminPreview"
:mode="mode"
@submit="submitForm"
>
<template #submit-btn="{submitForm: handleSubmit}">
@@ -208,14 +194,18 @@ import {pendingSubmission} from "~/composables/forms/pendingSubmission.js"
import clonedeep from "clone-deep"
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder.js"
import FirstSubmissionModal from '~/components/open/forms/components/FirstSubmissionModal.vue'
import { FormMode, createFormModeStrategy } from "~/lib/forms/FormModeStrategy.js"
export default {
components: { VTransition, OpenFormButton, OpenForm, FormCleanings, FirstSubmissionModal },
props: {
form: { type: Object, required: true },
creating: { type: Boolean, default: false }, // If true, fake form submit
adminPreview: { type: Boolean, default: false }, // If used in FormEditorPreview
mode: {
type: String,
default: FormMode.LIVE,
validator: (value) => Object.values(FormMode).includes(value)
},
submitButtonClass: { type: String, default: '' },
darkMode: {
type: Boolean,
@@ -254,6 +244,12 @@ export default {
},
computed: {
/**
* Gets the comprehensive strategy based on the form mode
*/
formModeStrategy() {
return createFormModeStrategy(this.mode)
},
isEmbedPopup () {
return import.meta.client && window.location.href.includes('popup=true')
},
@@ -266,9 +262,6 @@ export default {
isPublicFormPage () {
return this.$route.name === 'forms-slug'
},
isHideTitle () {
return this.form.hide_title || (import.meta.client && window.location.href.includes('hide_title=true'))
},
getFontUrl() {
if(!this.form || !this.form.font_family) return null
const family = this.form?.font_family.replace(/ /g, '+')
@@ -292,7 +285,8 @@ export default {
methods: {
submitForm (form, onFailure) {
if (this.creating) {
// Check if we should perform actual submission based on the mode
if (!this.formModeStrategy.validation.performActualSubmission) {
this.submitted = true
this.$emit('submitted', true)
return

View File

@@ -56,7 +56,7 @@
ghost-class="ghost-item"
filter=".not-draggable"
:animation="200"
:disabled="!adminPreview"
:disabled="!formModeStrategy.admin.allowDragging"
@change="handleDragDropped"
>
<template #item="{element}">
@@ -68,7 +68,7 @@
:data-form-value="dataFormValue"
:theme="theme"
:dark-mode="darkMode"
:admin-preview="adminPreview"
:mode="mode"
/>
</template>
</draggable>
@@ -133,6 +133,7 @@ import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import FormTimer from './FormTimer.vue'
import { storeToRefs } from 'pinia'
import { FormMode, createFormModeStrategy } from "~/lib/forms/FormModeStrategy.js"
export default {
name: 'OpenForm',
@@ -155,17 +156,16 @@ export default {
type: Boolean,
required: true
},
showHidden: {
type: Boolean,
default: false
},
fields: {
type: Array,
required: true
},
defaultDataForm: { type: [Object, null] },
adminPreview: {type: Boolean, default: false}, // If used in FormEditorPreview
urlPrefillPreview: {type: Boolean, default: false}, // If used in UrlFormPrefill
mode: {
type: String,
default: FormMode.LIVE,
validator: (value) => Object.values(FormMode).includes(value)
},
darkMode: {
type: Boolean,
default: false
@@ -227,6 +227,24 @@ export default {
groups.push(currentGroup)
return groups
},
/**
* Gets the comprehensive strategy based on the form mode
*/
formModeStrategy() {
return createFormModeStrategy(this.mode)
},
/**
* Determines if hidden fields should be shown based on the mode
*/
showHidden() {
return this.formModeStrategy.display.showHiddenFields
},
/**
* Determines if the form is in admin preview mode
*/
isAdminPreview() {
return this.formModeStrategy.admin.showAdminControls
},
formProgress() {
const requiredFields = this.fields.filter(field => field.required)
if (requiredFields.length === 0) {
@@ -343,14 +361,14 @@ export default {
// These watchers ensure the form shows the correct page for the field being edited in admin preview
selectedFieldIndex: {
handler(newIndex) {
if (this.adminPreview && this.showEditFieldSidebar) {
if (this.isAdminPreview && this.showEditFieldSidebar) {
this.setPageForField(newIndex)
}
}
},
showEditFieldSidebar: {
handler(newValue) {
if (this.adminPreview && newValue) {
if (this.isAdminPreview && newValue) {
this.setPageForField(this.selectedFieldIndex)
}
}
@@ -386,6 +404,12 @@ export default {
this.$refs['form-timer'].stopTimer()
this.dataForm.completion_time = this.$refs['form-timer'].completionTime
// Add validation strategy check
if (!this.formModeStrategy.validation.validateOnSubmit) {
this.$emit('submit', this.dataForm, this.onSubmissionFailure)
return
}
this.$emit('submit', this.dataForm, this.onSubmissionFailure)
},
/**
@@ -538,11 +562,12 @@ export default {
this.scrollToTop()
},
nextPage() {
if (this.adminPreview || this.urlPrefillPreview) {
if (!this.formModeStrategy.validation.validateOnNextPage) {
this.formPageIndex++
this.scrollToTop()
return false
}
const fieldsToValidate = this.currentFields.map(f => f.id)
this.dataForm.busy = true
this.dataForm.validate('POST', '/forms/' + this.form.slug + '/answer', {}, fieldsToValidate)

View File

@@ -7,19 +7,19 @@
:class="[
getFieldWidthClasses(field),
{
'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md': adminPreview,
'cursor-pointer':workingFormStore.showEditFieldSidebar && adminPreview,
'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md': isAdminPreview,
'cursor-pointer':workingFormStore.showEditFieldSidebar && isAdminPreview,
'bg-blue-50 hover:!bg-blue-50 dark:bg-gray-800 rounded-md': beingEdited,
}]"
@click="setFieldAsSelected"
>
<div
class="-m-[1px] w-full max-w-full mx-auto"
:class="{'relative transition-colors':adminPreview}"
:class="{'relative transition-colors':isAdminPreview}"
>
<div
v-if="adminPreview"
class="absolute translate-y-full lg:translate-y-0 -bottom-1 left-1/2 -translate-x-1/2 lg:-translate-x-full lg:-left-1 lg:top-1 lg:bottom-0 hidden group-hover/nffield:block z-50"
v-if="isAdminPreview"
class="absolute translate-y-full lg:translate-y-0 -bottom-1 left-1/2 -translate-x-1/2 lg:-translate-x-full lg:-left-1 lg:top-1 lg:bottom-0 hidden group-hover/nffield:block"
>
<div
class="flex lg:flex-col bg-white !bg-white dark:!bg-white border rounded-md shadow-sm z-50 p-[1px] relative"
@@ -151,6 +151,7 @@ import {computed} from 'vue'
import FormLogicPropertyResolver from "~/lib/forms/FormLogicPropertyResolver.js"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import {default as _has} from 'lodash/has'
import { FormMode, createFormModeStrategy } from "~/lib/forms/FormModeStrategy.js"
export default {
name: 'OpenFormField',
@@ -189,16 +190,21 @@ export default {
type: Object,
required: true
},
adminPreview: {type: Boolean, default: false} // If used in FormEditorPreview
mode: {
type: String,
default: FormMode.LIVE
}
},
setup() {
setup(props) {
const workingFormStore = useWorkingFormStore()
return {
workingFormStore,
currentWorkspace: computed(() => useWorkspacesStore().getCurrent),
selectedFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
showEditFieldSidebar: computed(() => workingFormStore.showEditFieldSidebar)
showEditFieldSidebar: computed(() => workingFormStore.showEditFieldSidebar),
formModeStrategy: computed(() => createFormModeStrategy(props.mode)),
isAdminPreview: computed(() => createFormModeStrategy(props.mode).admin.showAdminControls)
}
},
@@ -261,7 +267,7 @@ export default {
return (new FormLogicPropertyResolver(this.field, this.dataFormValue)).isDisabled()
},
beingEdited() {
return this.adminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => {
return this.isAdminPreview && this.showEditFieldSidebar && this.form.properties.findIndex((item) => {
return item.id === this.field.id
}) === this.selectedFieldIndex
},
@@ -281,7 +287,7 @@ export default {
return fieldsOptions
},
fieldSideBarOpened() {
return this.adminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
return this.isAdminPreview && (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
}
},
@@ -292,19 +298,19 @@ export default {
methods: {
editFieldOptions() {
if (!this.adminPreview) return
if (!this.formModeStrategy.admin.showAdminControls) return
this.workingFormStore.openSettingsForField(this.field)
},
setFieldAsSelected () {
if (!this.adminPreview || !this.workingFormStore.showEditFieldSidebar) return
if (!this.formModeStrategy.admin.showAdminControls || !this.workingFormStore.showEditFieldSidebar) return
this.workingFormStore.openSettingsForField(this.field)
},
openAddFieldSidebar() {
if (!this.adminPreview) return
if (!this.formModeStrategy.admin.showAdminControls) return
this.workingFormStore.openAddFieldSidebar(this.field)
},
removeField () {
if (!this.adminPreview) return
if (!this.formModeStrategy.admin.showAdminControls) return
this.workingFormStore.removeField(this.field)
},
getFieldWidthClasses(field) {

View File

@@ -24,15 +24,6 @@
</h3>
</div>
</template>
<toggle-switch-input
:model-value="modelValue.hide_title"
name="hide_title"
class="mt-4"
label="Hide Form Title"
:disabled="form.hide_title === true ? true : null"
:help="hideTitleHelp"
@update:model-value="onChangeHideTitle"
/>
<toggle-switch-input
:model-value="modelValue.auto_submit"
name="auto_submit"
@@ -64,22 +55,7 @@ export default {
return {}
},
computed: {
hideTitleHelp() {
return this.form.hide_title
? "This option is disabled because the form title is already hidden"
: null
},
},
watch: {},
mounted() {},
methods: {
onChangeHideTitle(val) {
this.modelValue.hide_title = val
},
onChangeAutoSubmit(val) {
this.modelValue.auto_submit = val
},

View File

@@ -148,11 +148,6 @@
label="Color image"
help="Not visible when form is embedded"
/>
<toggle-switch-input
name="hide_title"
:form="form"
label="Hide Title"
/>
<toggle-switch-input
name="no_branding"
:form="form"
@@ -173,11 +168,6 @@
icon="heroicons:cog-6-tooth-16-solid"
title="Advanced Options"
/>
<toggle-switch-input
name="hide_title"
:form="form"
label="Hide Form Title"
/>
<toggle-switch-input
name="show_progress_bar"
:form="form"

View File

@@ -92,11 +92,9 @@
ref="formPreview"
class="w-full mx-auto py-5"
:class="{'max-w-lg': form && (form.width === 'centered'),'px-7': !isExpanded, 'px-3': isExpanded}"
:creating="creating"
:form="form"
:dark-mode="darkMode"
:admin-preview="!isExpanded"
:show-cleanings="false"
:mode="formMode"
@restarted="previewFormSubmitted=false"
@submitted="previewFormSubmitted=true"
/>
@@ -113,6 +111,7 @@ import { default as _has } from 'lodash/has'
import { useRecordsStore } from '~/stores/records'
import { useWorkingFormStore } from '~/stores/working_form'
import { storeToRefs } from 'pinia'
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
const recordsStore = useRecordsStore()
const workingFormStore = useWorkingFormStore()
@@ -126,7 +125,8 @@ const { content: form } = storeToRefs(workingFormStore)
const recordLoading = computed(() => recordsStore.loading)
const darkMode = useDarkMode(parent)
const creating = computed(() => !_has(form.value, 'id'))
// Use PREVIEW mode when not expanded, TEST mode when expanded
const formMode = computed(() => isExpanded.value ? FormMode.TEST : FormMode.PREVIEW)
defineShortcuts({
escape: {

View File

@@ -108,14 +108,6 @@
</div>
</template>
<div class="border-t mt-4 -mx-4" />
<toggle-switch-input
v-model="advancedOptions.hide_title"
name="hide_title"
class="mt-4"
label="Hide Form Title"
:disabled="form.hide_title === true ? true : null"
:help="hideTitleHelp"
/>
<color-input
v-model="advancedOptions.bgcolor"
name="bgcolor"
@@ -175,22 +167,14 @@ const props = defineProps({
const embedScriptUrl = "/widgets/embed-min.js"
const showEmbedFormAsPopupModal = ref(false)
const advancedOptions = ref({
hide_title: false,
emoji: "💬",
position: "right",
bgcolor: "#3B82F6",
width: "500",
})
const hideTitleHelp = computed(() => {
return props.form.hide_title
? "This option is disabled because the form title is already hidden"
: null
})
const shareUrl = computed(() => {
return advancedOptions.value.hide_title
? props.form.share_url + "?hide_title=true"
: props.form.share_url
return props.form.share_url
})
const embedPopupCode = computed(() => {
const nfData = {

View File

@@ -75,10 +75,9 @@
v-if="form"
:theme="theme"
:loading="false"
:show-hidden="true"
:form="form"
:fields="form.properties"
:url-prefill-preview="true"
:mode="FormMode.PREFILL"
@submit="generateUrl"
>
<template #submit-btn="{ submitForm }">
@@ -111,6 +110,7 @@
import ThemeBuilder from "~/lib/forms/themes/ThemeBuilder"
import FormUrlPrefill from "../../../open/forms/components/FormUrlPrefill.vue"
import OpenForm from "../../../open/forms/OpenForm.vue"
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
export default {
name: "UrlFormPrefill",
@@ -132,6 +132,9 @@ export default {
borderRadius: this.form.border_radius
}).getAllComponents()
},
FormMode() {
return FormMode
}
},
methods: {