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:
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
8
client/composables/forms/initForm.js
vendored
8
client/composables/forms/initForm.js
vendored
@@ -17,7 +17,6 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
|
||||
layout_rtl: false,
|
||||
dark_mode: "auto",
|
||||
color: DEFAULT_COLOR,
|
||||
hide_title: false,
|
||||
no_branding: false,
|
||||
uppercase_labels: false,
|
||||
transparent_background: false,
|
||||
@@ -55,6 +54,12 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
|
||||
|
||||
function getDefaultProperties() {
|
||||
return [
|
||||
{
|
||||
type: "nf-text",
|
||||
content: "<h1>My Form</h1>",
|
||||
name: "Title",
|
||||
id: generateUUID(),
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
type: "text",
|
||||
@@ -96,7 +101,6 @@ export function setFormDefaults(formData) {
|
||||
border_radius: 'small',
|
||||
dark_mode: 'light',
|
||||
color: '#3B82F6',
|
||||
hide_title: false,
|
||||
uppercase_labels: false,
|
||||
no_branding: false,
|
||||
transparent_background: false,
|
||||
|
||||
92
client/lib/forms/FormModeStrategy.js
vendored
Normal file
92
client/lib/forms/FormModeStrategy.js
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Form modes for OpenForm components
|
||||
*/
|
||||
export const FormMode = {
|
||||
LIVE: 'live', // Real form with full validation and submission
|
||||
PREVIEW: 'preview', // Admin preview with no validation
|
||||
PREFILL: 'prefill', // URL prefill preview with no validation
|
||||
EDIT: 'edit', // Editing an existing submission
|
||||
TEST: 'test' // Test mode with validation but no actual submission
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a comprehensive strategy based on the form mode
|
||||
* This handles all mode-specific behaviors, not just validation
|
||||
*
|
||||
* @param {string} mode - One of the FormMode values
|
||||
* @returns {Object} - Strategy object with all mode-specific behaviors
|
||||
*/
|
||||
export function createFormModeStrategy(mode) {
|
||||
// Default configuration (LIVE mode)
|
||||
const defaultStrategy = {
|
||||
// Validation behaviors
|
||||
validation: {
|
||||
validateOnNextPage: true,
|
||||
validateOnSubmit: true,
|
||||
performActualSubmission: true
|
||||
},
|
||||
|
||||
// Display behaviors
|
||||
display: {
|
||||
showHiddenFields: false,
|
||||
showFormCleanings: true,
|
||||
showFontLink: false
|
||||
},
|
||||
|
||||
// Admin behaviors
|
||||
admin: {
|
||||
allowDragging: false,
|
||||
showAdminControls: false,
|
||||
isEditingMode: false
|
||||
}
|
||||
}
|
||||
|
||||
// Return default strategy for LIVE mode or unknown modes
|
||||
if (mode === FormMode.LIVE || !Object.values(FormMode).includes(mode)) {
|
||||
return defaultStrategy
|
||||
}
|
||||
|
||||
// Create a copy of the default strategy to modify
|
||||
const strategy = JSON.parse(JSON.stringify(defaultStrategy))
|
||||
|
||||
// Apply mode-specific overrides
|
||||
switch (mode) {
|
||||
case FormMode.PREVIEW:
|
||||
// Admin preview - no validation, show admin controls but NOT hidden fields
|
||||
strategy.validation.validateOnNextPage = false
|
||||
strategy.validation.validateOnSubmit = false
|
||||
strategy.validation.performActualSubmission = false
|
||||
|
||||
strategy.display.showHiddenFields = false
|
||||
strategy.display.showFormCleanings = false
|
||||
strategy.display.showFontLink = true
|
||||
|
||||
strategy.admin.allowDragging = true
|
||||
strategy.admin.showAdminControls = true
|
||||
break
|
||||
|
||||
case FormMode.PREFILL:
|
||||
// URL prefill - no validation, show hidden fields
|
||||
strategy.validation.validateOnNextPage = false
|
||||
strategy.validation.validateOnSubmit = false
|
||||
strategy.validation.performActualSubmission = false
|
||||
|
||||
strategy.display.showHiddenFields = true
|
||||
break
|
||||
|
||||
case FormMode.EDIT:
|
||||
// Editing submission - same validation as LIVE mode, but show hidden fields
|
||||
// This ensures edit mode behaves like live mode for validation
|
||||
strategy.display.showHiddenFields = true
|
||||
strategy.admin.isEditingMode = true
|
||||
break
|
||||
|
||||
case FormMode.TEST:
|
||||
// Test mode - validate on submit but don't submit, and don't validate on next page
|
||||
strategy.validation.performActualSubmission = false
|
||||
strategy.validation.validateOnNextPage = false
|
||||
break
|
||||
}
|
||||
|
||||
return strategy
|
||||
}
|
||||
@@ -54,6 +54,7 @@
|
||||
:form="form"
|
||||
class="mb-10"
|
||||
:dark-mode="darkMode"
|
||||
:mode="FormMode.LIVE"
|
||||
@password-entered="passwordEntered"
|
||||
/>
|
||||
</template>
|
||||
@@ -72,6 +73,7 @@ import {
|
||||
focusOnFirstFormElement,
|
||||
useDarkMode
|
||||
} from '~/lib/forms/public-page'
|
||||
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
|
||||
|
||||
const crisp = useCrisp()
|
||||
const formsStore = useFormsStore()
|
||||
|
||||
@@ -69,7 +69,6 @@ useOpnSeoMeta({
|
||||
})
|
||||
|
||||
const shareFormConfig = ref({
|
||||
hide_title: false,
|
||||
auto_submit: false,
|
||||
})
|
||||
|
||||
|
||||
@@ -57,17 +57,15 @@
|
||||
<section class="pt-12 bg-gray-50 sm:pt-16 border-b pb-[250px] relative">
|
||||
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center max-w-4xl gap-8 mx-auto md:gap-12 md:flex-row"
|
||||
class="flex flex-col items-center justify-center max-w-5xl gap-8 mx-auto md:gap-12 md:flex-row"
|
||||
>
|
||||
<div
|
||||
class="aspect-[4/3] shrink-0 rounded-lg shadow-sm overflow-hidden group max-w-sm"
|
||||
class="aspect-[4/3] shrink-0 rounded-lg shadow-sm overflow-hidden group w-full max-w-sm relative"
|
||||
>
|
||||
<img
|
||||
class="object-cover w-full transition-all duration-200 group-hover:scale-110 h-[240px]"
|
||||
class="object-cover w-full h-full transition-all duration-200 group-hover:scale-110 absolute inset-0"
|
||||
:src="template.image_url"
|
||||
alt="Template cover image"
|
||||
width="500px"
|
||||
height="380px"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -90,21 +88,19 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="relative px-4 mx-auto sm:px-6 lg:px-8 -mt-[210px]">
|
||||
<div class="max-w-7xl">
|
||||
<div
|
||||
class="max-w-2xl p-4 mx-auto bg-white shadow-lg sm:p-6 lg:p-8 rounded-xl ring-1 ring-inset ring-gray-200 isolate"
|
||||
>
|
||||
<p class="text-sm font-medium text-center text-gray-500 -mt-2 mb-2">
|
||||
Template Preview
|
||||
</p>
|
||||
<open-complete-form
|
||||
ref="open-complete-form"
|
||||
:form="form"
|
||||
:creating="true"
|
||||
class="mb-4 p-4 bg-gray-50 border border-gray-200 border-dashed rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<section class="w-full max-w-4xl relative px-4 mx-auto sm:px-6 lg:px-8 -mt-[210px]">
|
||||
<div
|
||||
class="p-4 mx-auto bg-white shadow-lg sm:p-6 lg:p-8 rounded-xl ring-1 ring-inset ring-gray-200 isolate"
|
||||
>
|
||||
<p class="text-sm font-medium text-center text-gray-500 -mt-2 mb-2">
|
||||
Template Preview
|
||||
</p>
|
||||
<open-complete-form
|
||||
ref="open-complete-form"
|
||||
:form="form"
|
||||
:mode="FormMode.TEST"
|
||||
class="mb-4 p-4 bg-gray-50 border border-gray-200 border-dashed rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 translate-y-full inset-x-0">
|
||||
@@ -132,7 +128,7 @@
|
||||
<section class="pt-20 pb-12 bg-white sm:pb-16">
|
||||
<div class="px-4 mx-auto sm:px-6 lg:px-8 max-w-7xl">
|
||||
<div
|
||||
class="max-w-2xl mx-auto mt-16 space-y-12 sm:mt-16 sm:space-y-16"
|
||||
class="max-w-4xl mx-auto mt-16 space-y-12 sm:mt-16 sm:space-y-16"
|
||||
>
|
||||
<div
|
||||
class="nf-text"
|
||||
@@ -216,7 +212,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 mt-12 md:grid-cols-2 gap-x-8 gap-y-12">
|
||||
<div class="grid grid-cols-1 mt-12 md:grid-cols-2 gap-x-8 gap-y-12 max-w-5xl mx-auto">
|
||||
<div
|
||||
class="flex flex-col items-center gap-4 text-center lg:items-start sm:text-left sm:items-start xl:flex-row"
|
||||
>
|
||||
@@ -276,6 +272,7 @@ import Breadcrumb from "~/components/global/Breadcrumb.vue"
|
||||
import SingleTemplate from "../../components/pages/templates/SingleTemplate.vue"
|
||||
import { fetchTemplate } from "~/stores/templates.js"
|
||||
import FormTemplateModal from "~/components/open/forms/components/templates/FormTemplateModal.vue"
|
||||
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
|
||||
|
||||
defineRouteRules({
|
||||
swr: 3600,
|
||||
|
||||
Reference in New Issue
Block a user