Enhance Form Builder with Conditional Logic and Dynamic List Features
- Introduced a new ConditionalLogicEngine component to manage and execute conditional logic for form fields, allowing for dynamic visibility and behavior based on user input. - Added conditional logic properties to various form components, enabling users to define conditions for showing, hiding, enabling, or disabling fields. - Enhanced the dynamic list component with new settings for item validation, uniqueness, and import/export functionality, improving data management capabilities. - Updated FormBuilderFieldSettingsModal to include a visual condition builder interface for easier configuration of conditional logic. - Improved documentation to reflect the new features and provide guidance on using conditional logic within forms.
This commit is contained in:
parent
fa90c234c5
commit
db3b00ce11
@ -15,6 +15,6 @@ export default {
|
|||||||
props: ["accept", "multiple", "maxSize", "minSize", "maxFiles", "disabled"],
|
props: ["accept", "multiple", "maxSize", "minSize", "maxFiles", "disabled"],
|
||||||
}),
|
}),
|
||||||
switch: createInput(Switch, {
|
switch: createInput(Switch, {
|
||||||
props: ["value"],
|
props: ["value", "disabled", "name", "id"],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="component-preview form-field" :style="componentStyle" :data-name="component.props.name"
|
||||||
class="component-preview form-field"
|
:data-type="component.type">
|
||||||
:style="componentStyle"
|
|
||||||
:data-name="component.props.name"
|
|
||||||
:data-type="component.type"
|
|
||||||
>
|
|
||||||
<!-- Hidden Field Special Display -->
|
<!-- Hidden Field Special Display -->
|
||||||
<div v-if="component.type === 'hidden' && isPreview" class="py-2">
|
<div v-if="component.type === 'hidden' && isPreview" class="py-2">
|
||||||
<div class="hidden-field-preview border-2 border-dashed border-gray-300 bg-gray-50 rounded-lg p-3">
|
<div class="hidden-field-preview border-2 border-dashed border-gray-300 bg-gray-50 rounded-lg p-3">
|
||||||
@ -27,91 +23,64 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Basic Input Types (including radio and checkbox) -->
|
<!-- Basic Input Types (including radio and checkbox) -->
|
||||||
<FormKit
|
<FormKit v-else-if="isInputType" :id="`preview-${component.id}`" :type="component.type" :name="component.props.name"
|
||||||
v-else-if="isInputType"
|
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
||||||
:id="`preview-${component.id}`"
|
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
||||||
:type="component.type"
|
:readonly="isPreview" :options="component.props.options || undefined" :value="component.props.value || undefined"
|
||||||
:name="component.props.name"
|
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
||||||
:label="component.props.label"
|
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
||||||
:help="component.props.help"
|
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
||||||
:placeholder="component.props.placeholder"
|
:maxFiles="component.props.maxFiles || undefined" :classes="component.type === 'checkbox' ? {
|
||||||
:validation="component.props.validation"
|
|
||||||
:validation-visibility="isPreview ? 'live' : 'blur'"
|
|
||||||
:readonly="isPreview"
|
|
||||||
:options="component.props.options || undefined"
|
|
||||||
:value="component.props.value || undefined"
|
|
||||||
:accept="component.props.accept || undefined"
|
|
||||||
:max="component.props.max || undefined"
|
|
||||||
:mask="component.props.mask || undefined"
|
|
||||||
:digits="component.props.digits || undefined"
|
|
||||||
:multiple="component.props.multiple || undefined"
|
|
||||||
:maxSize="component.props.maxSize || undefined"
|
|
||||||
:maxFiles="component.props.maxFiles || undefined"
|
|
||||||
:classes="component.type === 'checkbox' ? {
|
|
||||||
wrapper: 'mb-1',
|
wrapper: 'mb-1',
|
||||||
options: 'space-y-0.5'
|
options: 'space-y-0.5'
|
||||||
} : {}"
|
} : {}" :class="{ 'canvas-component': isPreview }" />
|
||||||
:class="{ 'canvas-component': isPreview }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<div v-else-if="component.type === 'heading'" class="py-2">
|
<div v-else-if="component.type === 'heading'" class="py-2">
|
||||||
<component
|
<component :is="`h${component.props.level || 2}`" class="font-semibold" :class="{
|
||||||
:is="`h${component.props.level || 2}`"
|
'text-2xl': component.props.level === 2,
|
||||||
class="font-semibold"
|
'text-xl': component.props.level === 3,
|
||||||
:class="{
|
'text-lg': component.props.level === 4
|
||||||
'text-2xl': component.props.level === 2,
|
}">
|
||||||
'text-xl': component.props.level === 3,
|
|
||||||
'text-lg': component.props.level === 4
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ component.props.value || 'Heading Text' }}
|
{{ component.props.value || 'Heading Text' }}
|
||||||
</component>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Paragraph -->
|
<!-- Paragraph -->
|
||||||
<div v-else-if="component.type === 'paragraph'" class="py-2">
|
<div v-else-if="component.type === 'paragraph'" class="py-2">
|
||||||
<p class="text-gray-600">{{ component.props.value || 'Paragraph text goes here' }}</p>
|
<p class="text-gray-600">{{ component.props.value || 'Paragraph text goes here' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Information Display -->
|
<!-- Information Display -->
|
||||||
<div v-else-if="component.type === 'info-display'" class="py-2">
|
<div v-else-if="component.type === 'info-display'" class="py-2">
|
||||||
<div
|
<div class="info-display-container rounded" :style="{
|
||||||
class="info-display-container rounded"
|
backgroundColor: component.props.backgroundColor || '#f9fafb',
|
||||||
:style="{
|
border: component.props.showBorder ? '1px solid #e5e7eb' : 'none'
|
||||||
backgroundColor: component.props.backgroundColor || '#f9fafb',
|
}">
|
||||||
border: component.props.showBorder ? '1px solid #e5e7eb' : 'none'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div v-if="component.props.title" class="p-3 border-b border-gray-200 bg-white">
|
<div v-if="component.props.title" class="p-3 border-b border-gray-200 bg-white">
|
||||||
<h4 class="text-base font-medium text-gray-800">{{ component.props.title }}</h4>
|
<h4 class="text-base font-medium text-gray-800">{{ component.props.title }}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fields Display -->
|
<!-- Fields Display -->
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div
|
<div v-if="component.props.layout === 'grid'" class="grid grid-cols-2 gap-4">
|
||||||
v-if="component.props.layout === 'grid'"
|
|
||||||
class="grid grid-cols-2 gap-4"
|
|
||||||
>
|
|
||||||
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
|
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
|
||||||
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
|
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
|
||||||
<dd class="text-sm text-gray-900 mt-1">{{ field.value }}</dd>
|
<dd class="text-sm text-gray-900 mt-1">{{ field.value }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-else-if="component.props.layout === 'horizontal'" class="space-y-2">
|
||||||
v-else-if="component.props.layout === 'horizontal'"
|
<div v-for="(field, index) in component.props.fields" :key="index"
|
||||||
class="space-y-2"
|
class="flex justify-between items-center">
|
||||||
>
|
|
||||||
<div v-for="(field, index) in component.props.fields" :key="index" class="flex justify-between items-center">
|
|
||||||
<dt class="text-sm font-medium text-gray-600">{{ field.label }}:</dt>
|
<dt class="text-sm font-medium text-gray-600">{{ field.label }}:</dt>
|
||||||
<dd class="text-sm text-gray-900">{{ field.value }}</dd>
|
<dd class="text-sm text-gray-900">{{ field.value }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
|
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
|
||||||
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
|
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
|
||||||
@ -121,57 +90,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<div v-else-if="component.type === 'divider'" class="py-4">
|
<div v-else-if="component.type === 'divider'" class="py-4">
|
||||||
<hr class="border-gray-200" />
|
<hr class="border-gray-200" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Preview Component -->
|
<!-- Image Preview Component -->
|
||||||
<div v-else-if="component.type === 'image-preview'" class="py-2">
|
<div v-else-if="component.type === 'image-preview'" class="py-2">
|
||||||
<div class="image-preview-container" :class="{ 'has-caption': component.props.showCaption && component.props.caption }">
|
<div class="image-preview-container"
|
||||||
|
:class="{ 'has-caption': component.props.showCaption && component.props.caption }">
|
||||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
{{ component.props.label }}
|
{{ component.props.label }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div
|
<div class="image-container relative" :class="{ 'cursor-zoom-in': component.props.showZoom }"
|
||||||
class="image-container relative"
|
@click="handleImageClick">
|
||||||
:class="{ 'cursor-zoom-in': component.props.showZoom }"
|
<img :src="component.props.imageUrl" :alt="component.props.altText || 'Image preview'"
|
||||||
@click="handleImageClick"
|
class="max-w-full rounded" :style="{
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="component.props.imageUrl"
|
|
||||||
:alt="component.props.altText || 'Image preview'"
|
|
||||||
class="max-w-full rounded"
|
|
||||||
:style="{
|
|
||||||
maxWidth: component.props.maxWidth || '100%',
|
maxWidth: component.props.maxWidth || '100%',
|
||||||
height: component.props.height || 'auto',
|
height: component.props.height || 'auto',
|
||||||
}"
|
}" />
|
||||||
/>
|
<div v-if="component.props.showZoom"
|
||||||
<div v-if="component.props.showZoom" class="absolute bottom-2 right-2 bg-black bg-opacity-60 text-white p-1 rounded">
|
class="absolute bottom-2 right-2 bg-black bg-opacity-60 text-white p-1 rounded">
|
||||||
<Icon name="material-symbols:zoom-in" class="w-4 h-4" />
|
<Icon name="material-symbols:zoom-in" class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="component.props.showCaption && component.props.caption" class="mt-1 text-sm text-gray-500 text-center">
|
<div v-if="component.props.showCaption && component.props.caption"
|
||||||
|
class="mt-1 text-sm text-gray-500 text-center">
|
||||||
{{ component.props.caption }}
|
{{ component.props.caption }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
||||||
{{ component.props.help }}
|
{{ component.props.help }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Repeating Group Component -->
|
<!-- Repeating Group Component -->
|
||||||
<div v-else-if="component.type === 'repeating-group'" class="py-2">
|
<div v-else-if="component.type === 'repeating-group'" class="py-2">
|
||||||
<fieldset class="border rounded-md p-4">
|
<fieldset class="border rounded-md p-4">
|
||||||
<legend class="text-sm font-medium px-2">{{ component.props.label || 'Group' }}</legend>
|
<legend class="text-sm font-medium px-2">{{ component.props.label || 'Group' }}</legend>
|
||||||
|
|
||||||
<div v-if="component.props.help" class="mb-3 text-xs text-gray-500">
|
<div v-if="component.props.help" class="mb-3 text-xs text-gray-500">
|
||||||
{{ component.props.help }}
|
{{ component.props.help }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Default group preview (in edit mode) -->
|
<!-- Default group preview (in edit mode) -->
|
||||||
<div v-if="isPreview" class="repeating-groups space-y-4">
|
<div v-if="isPreview" class="repeating-groups space-y-4">
|
||||||
<div class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
<div class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
||||||
@ -181,172 +146,193 @@
|
|||||||
{{ component.props.removeText || 'Remove' }}
|
{{ component.props.removeText || 'Remove' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
||||||
<FormKit
|
<FormKit :type="field.type" :label="field.label" :placeholder="field.placeholder"
|
||||||
:type="field.type"
|
:name="`${field.name}_1`" :options="field.options" disabled />
|
||||||
:label="field.label"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:name="`${field.name}_1`"
|
|
||||||
:options="field.options"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm">
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||||
{{ component.props.buttonText || 'Add Item' }}
|
{{ component.props.buttonText || 'Add Item' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Functional groups (in form view) -->
|
<!-- Functional groups (in form view) -->
|
||||||
<div v-else class="repeating-groups space-y-4">
|
<div v-else class="repeating-groups space-y-4">
|
||||||
<div
|
<div v-for="(group, groupIndex) in (previewFormData[component.props.name] || [])" :key="groupIndex"
|
||||||
v-for="(group, groupIndex) in (previewFormData[component.props.name] || [])"
|
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
|
||||||
:key="groupIndex"
|
|
||||||
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50"
|
|
||||||
>
|
|
||||||
<div class="flex justify-between items-center mb-3">
|
<div class="flex justify-between items-center mb-3">
|
||||||
<h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4>
|
<h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4>
|
||||||
<button
|
<button v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 1)"
|
||||||
v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 1)"
|
type="button" class="text-red-500 hover:text-red-700 text-sm" @click="removeGroupItem(groupIndex)">
|
||||||
type="button"
|
|
||||||
class="text-red-500 hover:text-red-700 text-sm"
|
|
||||||
@click="removeGroupItem(groupIndex)"
|
|
||||||
>
|
|
||||||
{{ component.props.removeText || 'Remove' }}
|
{{ component.props.removeText || 'Remove' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
<template v-for="(field, fieldIndex) in component.props.fields" :key="fieldIndex">
|
||||||
<FormKit
|
<FormKit :type="field.type" :label="field.label" :placeholder="field.placeholder"
|
||||||
:type="field.type"
|
:name="`${component.props.name}.${groupIndex}.${field.name}`" :options="field.options"
|
||||||
:label="field.label"
|
v-model="group[field.name]" />
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:name="`${component.props.name}.${groupIndex}.${field.name}`"
|
|
||||||
:options="field.options"
|
|
||||||
v-model="group[field.name]"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 10)"
|
||||||
v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 10)"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
||||||
@click="addGroupItem"
|
@click="addGroupItem">
|
||||||
>
|
|
||||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||||
{{ component.props.buttonText || 'Add Item' }}
|
{{ component.props.buttonText || 'Add Item' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dynamic List Component -->
|
<!-- Dynamic List Component -->
|
||||||
<div v-else-if="component.type === 'dynamic-list'" class="py-2">
|
<div v-else-if="component.type === 'dynamic-list'" class="py-2">
|
||||||
<div class="dynamic-list-container">
|
<div class="dynamic-list-container">
|
||||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
{{ component.props.label }}
|
{{ component.props.label }}
|
||||||
|
<span v-if="component.props.showItemCounter" class="text-xs text-gray-500 ml-2">
|
||||||
|
({{ (previewFormData[component.props.name] || []).length }}/{{ component.props.maxItems || 20 }})
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div v-if="component.props.help" class="mb-3 text-xs text-gray-500">
|
<div v-if="component.props.help" class="mb-3 text-xs text-gray-500">
|
||||||
{{ component.props.help }}
|
{{ component.props.help }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Box (if enabled) -->
|
||||||
|
<div v-if="component.props.enableSearch" class="mb-3">
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" v-model="searchQuery[component.props.name]" placeholder="Search items..."
|
||||||
|
class="w-full px-3 py-2 pl-9 border border-gray-300 rounded-md text-sm" />
|
||||||
|
<Icon name="material-symbols:search"
|
||||||
|
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bulk Operations Toolbar (if enabled) -->
|
||||||
|
<div v-if="component.props.bulkOperations && (previewFormData[component.props.name] || []).length > 0"
|
||||||
|
class="mb-3 flex items-center justify-between bg-gray-50 p-2 rounded">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" :checked="isAllSelected(component.props.name)"
|
||||||
|
@change="toggleSelectAll(component.props.name)" class="h-4 w-4 rounded border-gray-300" />
|
||||||
|
<span class="text-xs text-gray-600">
|
||||||
|
{{ selectedItems[component.props.name]?.length || 0 }} selected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button v-if="(selectedItems[component.props.name]?.length || 0) > 0"
|
||||||
|
@click="deleteSelectedItems(component.props.name)"
|
||||||
|
class="text-xs text-red-600 hover:text-red-800 flex items-center">
|
||||||
|
<Icon name="material-symbols:delete-outline" class="w-4 h-4 mr-1" />
|
||||||
|
Delete Selected
|
||||||
|
</button>
|
||||||
|
<button v-if="component.props.importEnabled" @click="showImportModal(component.props.name)"
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-800 flex items-center">
|
||||||
|
<Icon name="material-symbols:upload-file-outline" class="w-4 h-4 mr-1" />
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-if="isPreview" class="list-items space-y-2">
|
<div v-if="isPreview" class="list-items space-y-2">
|
||||||
<div v-for="(item, index) in component.props.defaultItems" :key="index" class="flex items-center">
|
<div v-for="(item, index) in component.props.defaultItems" :key="index" class="flex items-center">
|
||||||
<input
|
<input :type="component.props.itemType || 'text'" :value="item" :placeholder="component.props.placeholder"
|
||||||
type="text"
|
|
||||||
:value="item"
|
|
||||||
:placeholder="component.props.placeholder"
|
|
||||||
disabled
|
disabled
|
||||||
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700 disabled:bg-gray-50"
|
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700 disabled:bg-gray-50" />
|
||||||
/>
|
<div v-if="component.props.enableSorting" class="ml-2 cursor-move text-gray-400">
|
||||||
|
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
|
||||||
|
</div>
|
||||||
<button type="button" class="ml-2 text-red-500 hover:text-red-700">
|
<button type="button" class="ml-2 text-red-500 hover:text-red-700">
|
||||||
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add button for preview -->
|
<!-- Add button for preview -->
|
||||||
<button
|
<button type="button"
|
||||||
type="button"
|
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm">
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||||
{{ component.props.buttonText || 'Add Item' }}
|
{{ component.props.buttonText || 'Add Item' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="list-items space-y-2">
|
<div v-else class="list-items space-y-2">
|
||||||
<div
|
<div v-for="(item, index) in getFilteredItems(component.props.name)" :key="index"
|
||||||
v-for="(item, index) in (previewFormData[component.props.name] || [])"
|
class="flex items-center group">
|
||||||
:key="index"
|
<input v-if="component.props.bulkOperations" type="checkbox"
|
||||||
class="flex items-center"
|
:checked="isItemSelected(component.props.name, index)"
|
||||||
>
|
@change="toggleItemSelection(component.props.name, index)"
|
||||||
<input
|
class="mr-2 h-4 w-4 rounded border-gray-300" />
|
||||||
type="text"
|
<input :type="component.props.itemType || 'text'" v-model="previewFormData[component.props.name][index]"
|
||||||
v-model="previewFormData[component.props.name][index]"
|
|
||||||
:placeholder="component.props.placeholder"
|
:placeholder="component.props.placeholder"
|
||||||
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-gray-700"
|
:class="getItemInputClasses(component.props.name, index, item)"
|
||||||
/>
|
@blur="validateItem(component.props.name, index, item)"
|
||||||
<button
|
@input="handleItemInput(component.props.name, index, $event.target.value)" />
|
||||||
v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 0)"
|
<div v-if="component.props.enableSorting"
|
||||||
type="button"
|
class="ml-2 cursor-move text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
class="ml-2 text-red-500 hover:text-red-700"
|
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
|
||||||
@click="removeListItem(index)"
|
</div>
|
||||||
>
|
<button v-if="(previewFormData[component.props.name]?.length || 0) > (component.props.minItems || 0)"
|
||||||
|
type="button" class="ml-2 text-red-500 hover:text-red-700" @click="removeListItem(index)">
|
||||||
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<!-- Validation Messages -->
|
||||||
v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 20)"
|
<div v-if="validationErrors[component.props.name]" class="text-xs text-red-600 mt-1">
|
||||||
|
{{ validationErrors[component.props.name] }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="(previewFormData[component.props.name]?.length || 0) < (component.props.maxItems || 20)"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
||||||
@click="addListItem"
|
@click="addListItem">
|
||||||
>
|
|
||||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||||
{{ component.props.buttonText || 'Add Item' }}
|
{{ component.props.buttonText || 'Add Item' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Export Button (if enabled) -->
|
||||||
|
<div v-if="component.props.exportFormat && (previewFormData[component.props.name] || []).length > 0"
|
||||||
|
class="mt-2">
|
||||||
|
<button @click="exportItems(component.props.name, component.props.exportFormat)"
|
||||||
|
class="text-xs text-green-600 hover:text-green-800 flex items-center">
|
||||||
|
<Icon name="material-symbols:download" class="w-4 h-4 mr-1" />
|
||||||
|
Export as {{ component.props.exportFormat.toUpperCase() }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Button Component -->
|
<!-- Button Component -->
|
||||||
<div v-else-if="component.type === 'button'" class="py-2">
|
<div v-else-if="component.type === 'button'" class="py-2">
|
||||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-2">
|
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{{ component.props.label }}
|
{{ component.props.label }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<RsButton
|
<RsButton :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
|
||||||
:type="component.props.buttonType || 'button'"
|
:size="component.props.size || 'md'" :disabled="component.props.disabled || false" @click="handleButtonClick"
|
||||||
:variant="component.props.variant || 'primary'"
|
class="button-component">
|
||||||
:size="component.props.size || 'md'"
|
|
||||||
:disabled="component.props.disabled || false"
|
|
||||||
@click="handleButtonClick"
|
|
||||||
class="button-component"
|
|
||||||
>
|
|
||||||
{{ component.props.label || 'Button' }}
|
{{ component.props.label || 'Button' }}
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
|
||||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
||||||
{{ component.props.help }}
|
{{ component.props.help }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Unknown Component Type Fallback -->
|
<!-- Unknown Component Type Fallback -->
|
||||||
<div v-else class="p-4 border border-dashed border-gray-300 rounded">
|
<div v-else class="p-4 border border-dashed border-gray-300 rounded">
|
||||||
<div class="text-gray-500">Unknown component type: {{ component.type }}</div>
|
<div class="text-gray-500">Unknown component type: {{ component.type }}</div>
|
||||||
@ -373,29 +359,70 @@ const props = defineProps({
|
|||||||
const formStore = useFormBuilderStore();
|
const formStore = useFormBuilderStore();
|
||||||
const previewFormData = computed(() => formStore.previewFormData || {});
|
const previewFormData = computed(() => formStore.previewFormData || {});
|
||||||
|
|
||||||
|
// Enhanced dynamic list functionality
|
||||||
|
const searchQuery = ref({});
|
||||||
|
const selectedItems = ref({});
|
||||||
|
const validationErrors = ref({});
|
||||||
|
|
||||||
|
// Initialize data for components
|
||||||
|
onMounted(() => {
|
||||||
|
// Initialize search and selection data for dynamic lists
|
||||||
|
if (props.component.type === 'dynamic-list') {
|
||||||
|
const listName = props.component.props.name;
|
||||||
|
if (listName) {
|
||||||
|
searchQuery.value[listName] = '';
|
||||||
|
selectedItems.value[listName] = [];
|
||||||
|
validationErrors.value[listName] = '';
|
||||||
|
|
||||||
|
// Initialize form data with default items if they exist and form data is empty
|
||||||
|
const currentFormData = formStore.previewFormData[listName];
|
||||||
|
const defaultItems = props.component.props.defaultItems;
|
||||||
|
|
||||||
|
if ((!currentFormData || currentFormData.length === 0) && defaultItems && defaultItems.length > 0) {
|
||||||
|
const updatedData = { ...formStore.previewFormData, [listName]: [...defaultItems] };
|
||||||
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes to component props, especially defaultItems
|
||||||
|
watch(() => props.component.props.defaultItems, (newDefaultItems, oldDefaultItems) => {
|
||||||
|
if (props.component.type === 'dynamic-list') {
|
||||||
|
const listName = props.component.props.name;
|
||||||
|
if (!listName) return;
|
||||||
|
|
||||||
|
// Always update when defaultItems change, regardless of current form data
|
||||||
|
const items = newDefaultItems || [];
|
||||||
|
|
||||||
|
const updatedData = { ...formStore.previewFormData, [listName]: [...items] };
|
||||||
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
}
|
||||||
|
}, { deep: true, immediate: true });
|
||||||
|
|
||||||
// Repeating group and dynamic list functionality
|
// Repeating group and dynamic list functionality
|
||||||
const addGroupItem = () => {
|
const addGroupItem = () => {
|
||||||
if (props.isPreview) return;
|
if (props.isPreview) return;
|
||||||
|
|
||||||
const groupName = props.component.props.name;
|
const groupName = props.component.props.name;
|
||||||
if (!groupName) return;
|
if (!groupName) return;
|
||||||
|
|
||||||
// Get current groups or initialize empty array
|
// Get current groups or initialize empty array
|
||||||
const currentGroups = formStore.previewFormData[groupName] || [];
|
const currentGroups = formStore.previewFormData[groupName] || [];
|
||||||
|
|
||||||
// Create a new empty group
|
// Create a new empty group
|
||||||
const newGroup = {};
|
const newGroup = {};
|
||||||
|
|
||||||
// Add fields from configuration
|
// Add fields from configuration
|
||||||
if (props.component.props.fields) {
|
if (props.component.props.fields) {
|
||||||
props.component.props.fields.forEach(field => {
|
props.component.props.fields.forEach(field => {
|
||||||
newGroup[field.name] = '';
|
newGroup[field.name] = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new group to the list
|
// Add the new group to the list
|
||||||
currentGroups.push(newGroup);
|
currentGroups.push(newGroup);
|
||||||
|
|
||||||
// Update the form data
|
// Update the form data
|
||||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||||
formStore.updatePreviewFormData(updatedData);
|
formStore.updatePreviewFormData(updatedData);
|
||||||
@ -403,16 +430,16 @@ const addGroupItem = () => {
|
|||||||
|
|
||||||
const removeGroupItem = (index) => {
|
const removeGroupItem = (index) => {
|
||||||
if (props.isPreview) return;
|
if (props.isPreview) return;
|
||||||
|
|
||||||
const groupName = props.component.props.name;
|
const groupName = props.component.props.name;
|
||||||
if (!groupName) return;
|
if (!groupName) return;
|
||||||
|
|
||||||
// Get current groups
|
// Get current groups
|
||||||
const currentGroups = [...(formStore.previewFormData[groupName] || [])];
|
const currentGroups = [...(formStore.previewFormData[groupName] || [])];
|
||||||
|
|
||||||
// Remove the group at the specified index
|
// Remove the group at the specified index
|
||||||
currentGroups.splice(index, 1);
|
currentGroups.splice(index, 1);
|
||||||
|
|
||||||
// Update the form data
|
// Update the form data
|
||||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||||
formStore.updatePreviewFormData(updatedData);
|
formStore.updatePreviewFormData(updatedData);
|
||||||
@ -420,16 +447,16 @@ const removeGroupItem = (index) => {
|
|||||||
|
|
||||||
const addListItem = () => {
|
const addListItem = () => {
|
||||||
if (props.isPreview) return;
|
if (props.isPreview) return;
|
||||||
|
|
||||||
const listName = props.component.props.name;
|
const listName = props.component.props.name;
|
||||||
if (!listName) return;
|
if (!listName) return;
|
||||||
|
|
||||||
// Get current items or initialize empty array
|
// Get current items or initialize empty array
|
||||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
||||||
|
|
||||||
// Add an empty item
|
// Add an empty item
|
||||||
currentItems.push('');
|
currentItems.push('');
|
||||||
|
|
||||||
// Update the form data
|
// Update the form data
|
||||||
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
|
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
|
||||||
formStore.updatePreviewFormData(updatedData);
|
formStore.updatePreviewFormData(updatedData);
|
||||||
@ -437,25 +464,275 @@ const addListItem = () => {
|
|||||||
|
|
||||||
const removeListItem = (index) => {
|
const removeListItem = (index) => {
|
||||||
if (props.isPreview) return;
|
if (props.isPreview) return;
|
||||||
|
|
||||||
const listName = props.component.props.name;
|
const listName = props.component.props.name;
|
||||||
if (!listName) return;
|
if (!listName) return;
|
||||||
|
|
||||||
// Get current items
|
// Get current items
|
||||||
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
||||||
|
|
||||||
// Remove the item at the specified index
|
// Remove the item at the specified index
|
||||||
currentItems.splice(index, 1);
|
currentItems.splice(index, 1);
|
||||||
|
|
||||||
// Update the form data
|
// Update the form data
|
||||||
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
|
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
|
||||||
formStore.updatePreviewFormData(updatedData);
|
formStore.updatePreviewFormData(updatedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Enhanced Dynamic List Methods
|
||||||
|
|
||||||
|
// Validate individual item based on component settings
|
||||||
|
const validateItem = (listName, index, value) => {
|
||||||
|
const component = props.component;
|
||||||
|
if (!component.props.itemValidation) return true;
|
||||||
|
|
||||||
|
// Simple validation for now - can be enhanced with more complex rules
|
||||||
|
const rules = component.props.itemValidation.split('|');
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule === 'required' && (!value || value.trim() === '')) {
|
||||||
|
validationErrors.value[listName] = `Item ${index + 1} is required`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.startsWith('min:')) {
|
||||||
|
const minLength = parseInt(rule.split(':')[1]);
|
||||||
|
if (value.length < minLength) {
|
||||||
|
validationErrors.value[listName] = `Item ${index + 1} must be at least ${minLength} characters`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.startsWith('max:')) {
|
||||||
|
const maxLength = parseInt(rule.split(':')[1]);
|
||||||
|
if (value.length > maxLength) {
|
||||||
|
validationErrors.value[listName] = `Item ${index + 1} cannot exceed ${maxLength} characters`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule === 'email' && component.props.itemType === 'email') {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(value)) {
|
||||||
|
validationErrors.value[listName] = `Item ${index + 1} must be a valid email address`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule === 'url' && component.props.itemType === 'url') {
|
||||||
|
try {
|
||||||
|
new URL(value);
|
||||||
|
} catch {
|
||||||
|
validationErrors.value[listName] = `Item ${index + 1} must be a valid URL`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear validation error if validation passes
|
||||||
|
validationErrors.value[listName] = '';
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for duplicate items
|
||||||
|
const checkDuplicates = (listName, newValue, currentIndex) => {
|
||||||
|
if (props.component.props.allowDuplicates) return true;
|
||||||
|
|
||||||
|
const currentItems = formStore.previewFormData[listName] || [];
|
||||||
|
const duplicateIndex = currentItems.findIndex((item, index) =>
|
||||||
|
index !== currentIndex && item.toLowerCase() === newValue.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicateIndex !== -1) {
|
||||||
|
validationErrors.value[listName] = `Duplicate item detected. Item "${newValue}" already exists.`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationErrors.value[listName] = '';
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle item input with validation
|
||||||
|
const handleItemInput = (listName, index, value) => {
|
||||||
|
// Check for duplicates if not allowed
|
||||||
|
if (!checkDuplicates(listName, value, index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the item
|
||||||
|
validateItem(listName, index, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get CSS classes for item input based on validation state
|
||||||
|
const getItemInputClasses = (listName, index, value) => {
|
||||||
|
const baseClasses = "block w-full border rounded-md shadow-sm py-2 px-3 text-gray-700";
|
||||||
|
const hasError = validationErrors.value[listName];
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return `${baseClasses} border-red-300 focus:border-red-500`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClasses} border-gray-300 focus:border-blue-500 focus:ring-blue-500`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter items based on search query
|
||||||
|
const getFilteredItems = (listName) => {
|
||||||
|
const items = formStore.previewFormData[listName] || [];
|
||||||
|
const query = searchQuery.value[listName];
|
||||||
|
|
||||||
|
if (!query || !props.component.props.enableSearch) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.filter(item =>
|
||||||
|
item.toLowerCase().includes(query.toLowerCase())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
const isItemSelected = (listName, index) => {
|
||||||
|
return selectedItems.value[listName]?.includes(index) || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleItemSelection = (listName, index) => {
|
||||||
|
if (!selectedItems.value[listName]) {
|
||||||
|
selectedItems.value[listName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedList = selectedItems.value[listName];
|
||||||
|
const selectedIndex = selectedList.indexOf(index);
|
||||||
|
|
||||||
|
if (selectedIndex > -1) {
|
||||||
|
selectedList.splice(selectedIndex, 1);
|
||||||
|
} else {
|
||||||
|
selectedList.push(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAllSelected = (listName) => {
|
||||||
|
const items = formStore.previewFormData[listName] || [];
|
||||||
|
const selected = selectedItems.value[listName] || [];
|
||||||
|
return items.length > 0 && selected.length === items.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectAll = (listName) => {
|
||||||
|
const items = formStore.previewFormData[listName] || [];
|
||||||
|
|
||||||
|
if (isAllSelected(listName)) {
|
||||||
|
selectedItems.value[listName] = [];
|
||||||
|
} else {
|
||||||
|
selectedItems.value[listName] = items.map((_, index) => index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSelectedItems = (listName) => {
|
||||||
|
if (!props.component.props.confirmDelete || confirm('Are you sure you want to delete the selected items?')) {
|
||||||
|
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
||||||
|
const selected = selectedItems.value[listName] || [];
|
||||||
|
|
||||||
|
// Sort indices in descending order to avoid index shifting issues
|
||||||
|
selected.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
// Remove selected items
|
||||||
|
selected.forEach(index => {
|
||||||
|
currentItems.splice(index, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update form data
|
||||||
|
const updatedData = { ...formStore.previewFormData, [listName]: currentItems };
|
||||||
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
selectedItems.value[listName] = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import/Export functionality
|
||||||
|
const exportItems = (listName, format) => {
|
||||||
|
const items = formStore.previewFormData[listName] || [];
|
||||||
|
let content = '';
|
||||||
|
let filename = `${listName}_items`;
|
||||||
|
let mimeType = 'text/plain';
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'json':
|
||||||
|
content = JSON.stringify(items, null, 2);
|
||||||
|
filename += '.json';
|
||||||
|
mimeType = 'application/json';
|
||||||
|
break;
|
||||||
|
case 'csv':
|
||||||
|
content = items.map(item => `"${item}"`).join('\n');
|
||||||
|
filename += '.csv';
|
||||||
|
mimeType = 'text/csv';
|
||||||
|
break;
|
||||||
|
default: // txt
|
||||||
|
content = items.join('\n');
|
||||||
|
filename += '.txt';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and trigger download
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showImportModal = (listName) => {
|
||||||
|
// This would open a modal for importing items
|
||||||
|
// For now, let's use a simple file input
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.txt,.csv,.json';
|
||||||
|
input.onchange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target.result;
|
||||||
|
let importedItems = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (file.name.endsWith('.json')) {
|
||||||
|
importedItems = JSON.parse(content);
|
||||||
|
} else if (file.name.endsWith('.csv')) {
|
||||||
|
importedItems = content.split('\n').map(line => line.replace(/"/g, '').trim()).filter(Boolean);
|
||||||
|
} else {
|
||||||
|
importedItems = content.split('\n').filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add imported items to current list
|
||||||
|
const currentItems = [...(formStore.previewFormData[listName] || [])];
|
||||||
|
const newItems = [...currentItems, ...importedItems];
|
||||||
|
|
||||||
|
// Respect max items limit
|
||||||
|
const maxItems = props.component.props.maxItems || 20;
|
||||||
|
if (newItems.length > maxItems) {
|
||||||
|
newItems.splice(maxItems);
|
||||||
|
alert(`Import limited to ${maxItems} items total.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update form data
|
||||||
|
const updatedData = { ...formStore.previewFormData, [listName]: newItems };
|
||||||
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error importing file. Please check the file format.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
// Handle button click
|
// Handle button click
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = () => {
|
||||||
if (props.isPreview) return;
|
if (props.isPreview) return;
|
||||||
|
|
||||||
// Execute custom onClick code if provided
|
// Execute custom onClick code if provided
|
||||||
if (props.component.props.onClick) {
|
if (props.component.props.onClick) {
|
||||||
try {
|
try {
|
||||||
@ -465,7 +742,7 @@ const handleButtonClick = () => {
|
|||||||
componentName: props.component.props.name,
|
componentName: props.component.props.name,
|
||||||
buttonLabel: props.component.props.label
|
buttonLabel: props.component.props.label
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute the custom code
|
// Execute the custom code
|
||||||
const func = new Function('context', props.component.props.onClick);
|
const func = new Function('context', props.component.props.onClick);
|
||||||
func(context);
|
func(context);
|
||||||
@ -473,13 +750,10 @@ const handleButtonClick = () => {
|
|||||||
console.error('Error executing button onClick:', error);
|
console.error('Error executing button onClick:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default behavior - log the button click
|
// Default behavior - log the button click
|
||||||
console.log(`Button clicked: ${props.component.props.label || 'Unnamed Button'}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if the component is a standard FormKit input type (excluding specialized components)
|
// Check if the component is a standard FormKit input type (excluding specialized components)
|
||||||
const isStandardInputType = computed(() => {
|
const isStandardInputType = computed(() => {
|
||||||
const standardInputTypes = [
|
const standardInputTypes = [
|
||||||
@ -487,7 +761,7 @@ const isStandardInputType = computed(() => {
|
|||||||
'date', 'time', 'datetime-local', 'url', 'tel',
|
'date', 'time', 'datetime-local', 'url', 'tel',
|
||||||
'select', 'radio', 'file', 'range', 'color', 'hidden'
|
'select', 'radio', 'file', 'range', 'color', 'hidden'
|
||||||
];
|
];
|
||||||
|
|
||||||
return standardInputTypes.includes(props.component.type);
|
return standardInputTypes.includes(props.component.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -499,7 +773,7 @@ const isInputType = computed(() => {
|
|||||||
'select', 'checkbox', 'radio', 'file', 'range',
|
'select', 'checkbox', 'radio', 'file', 'range',
|
||||||
'color', 'hidden', 'mask', 'otp', 'dropzone', 'switch'
|
'color', 'hidden', 'mask', 'otp', 'dropzone', 'switch'
|
||||||
];
|
];
|
||||||
|
|
||||||
return inputTypes.includes(props.component.type);
|
return inputTypes.includes(props.component.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -507,11 +781,11 @@ const isInputType = computed(() => {
|
|||||||
const handleImageClick = (event) => {
|
const handleImageClick = (event) => {
|
||||||
// Only do something if zoom is enabled
|
// Only do something if zoom is enabled
|
||||||
if (!props.component.props.showZoom) return;
|
if (!props.component.props.showZoom) return;
|
||||||
|
|
||||||
// Get the image URL
|
// Get the image URL
|
||||||
const imageUrl = props.component.props.imageUrl;
|
const imageUrl = props.component.props.imageUrl;
|
||||||
if (!imageUrl) return;
|
if (!imageUrl) return;
|
||||||
|
|
||||||
// Create a lightbox effect to view the image
|
// Create a lightbox effect to view the image
|
||||||
const lightbox = document.createElement('div');
|
const lightbox = document.createElement('div');
|
||||||
lightbox.style.position = 'fixed';
|
lightbox.style.position = 'fixed';
|
||||||
@ -525,24 +799,24 @@ const handleImageClick = (event) => {
|
|||||||
lightbox.style.justifyContent = 'center';
|
lightbox.style.justifyContent = 'center';
|
||||||
lightbox.style.zIndex = '1000';
|
lightbox.style.zIndex = '1000';
|
||||||
lightbox.style.cursor = 'zoom-out';
|
lightbox.style.cursor = 'zoom-out';
|
||||||
|
|
||||||
// Create the image element
|
// Create the image element
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = imageUrl;
|
img.src = imageUrl;
|
||||||
img.style.maxWidth = '90%';
|
img.style.maxWidth = '90%';
|
||||||
img.style.maxHeight = '90%';
|
img.style.maxHeight = '90%';
|
||||||
img.style.objectFit = 'contain';
|
img.style.objectFit = 'contain';
|
||||||
|
|
||||||
// Add close functionality
|
// Add close functionality
|
||||||
lightbox.onclick = () => {
|
lightbox.onclick = () => {
|
||||||
document.body.removeChild(lightbox);
|
document.body.removeChild(lightbox);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent image click from closing the lightbox
|
// Prevent image click from closing the lightbox
|
||||||
img.onclick = (e) => {
|
img.onclick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add to DOM
|
// Add to DOM
|
||||||
lightbox.appendChild(img);
|
lightbox.appendChild(img);
|
||||||
document.body.appendChild(lightbox);
|
document.body.appendChild(lightbox);
|
||||||
@ -554,10 +828,10 @@ const componentStyle = computed(() => {
|
|||||||
if (props.isPreview) {
|
if (props.isPreview) {
|
||||||
return {}; // Styling is handled by parent in canvas mode
|
return {}; // Styling is handled by parent in canvas mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply grid column in preview mode
|
// Apply grid column in preview mode
|
||||||
const gridColumn = props.component.props.gridColumn || 'span 12';
|
const gridColumn = props.component.props.gridColumn || 'span 12';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gridColumn: gridColumn,
|
gridColumn: gridColumn,
|
||||||
width: '100%', // Always use 100% within the grid cell
|
width: '100%', // Always use 100% within the grid cell
|
||||||
@ -633,4 +907,4 @@ const componentStyle = computed(() => {
|
|||||||
.list-items {
|
.list-items {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
150
components/ConditionalLogicEngine.vue
Normal file
150
components/ConditionalLogicEngine.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="conditional-logic-engine">
|
||||||
|
<!-- This component handles conditional logic execution -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formComponents: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['script-generated'])
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
const generatedScript = ref('')
|
||||||
|
const isInitialized = ref(false)
|
||||||
|
|
||||||
|
// Generate conditional logic script from form components
|
||||||
|
const generateConditionalLogicScript = () => {
|
||||||
|
const scriptsArray = []
|
||||||
|
|
||||||
|
// Process each component that has conditional logic enabled
|
||||||
|
props.formComponents.forEach(component => {
|
||||||
|
const conditionalLogic = component.props.conditionalLogic
|
||||||
|
|
||||||
|
if (!conditionalLogic || !conditionalLogic.enabled || !conditionalLogic.conditions.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { conditions, action, operator } = conditionalLogic
|
||||||
|
const fieldName = component.props.name
|
||||||
|
|
||||||
|
if (!fieldName) return
|
||||||
|
|
||||||
|
// Generate condition checks
|
||||||
|
const conditionChecks = conditions.map(condition => {
|
||||||
|
const { field, operator: condOp, value } = condition
|
||||||
|
|
||||||
|
switch (condOp) {
|
||||||
|
case 'equals':
|
||||||
|
return `getField('${field}') === '${value}'`
|
||||||
|
case 'not_equals':
|
||||||
|
return `getField('${field}') !== '${value}'`
|
||||||
|
case 'contains':
|
||||||
|
return `String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())`
|
||||||
|
case 'not_contains':
|
||||||
|
return `!String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())`
|
||||||
|
case 'is_empty':
|
||||||
|
return `!getField('${field}') || getField('${field}') === ''`
|
||||||
|
case 'is_not_empty':
|
||||||
|
return `getField('${field}') && getField('${field}') !== ''`
|
||||||
|
case 'greater_than':
|
||||||
|
return `Number(getField('${field}')) > ${Number(value) || 0}`
|
||||||
|
case 'less_than':
|
||||||
|
return `Number(getField('${field}')) < ${Number(value) || 0}`
|
||||||
|
default:
|
||||||
|
return `getField('${field}') === '${value}'`
|
||||||
|
}
|
||||||
|
}).join(` ${operator} `)
|
||||||
|
|
||||||
|
// Generate action functions
|
||||||
|
const actionCode = action === 'show' ? `showField('${fieldName}')` :
|
||||||
|
action === 'hide' ? `hideField('${fieldName}')` :
|
||||||
|
action === 'enable' ? `enableField('${fieldName}')` :
|
||||||
|
action === 'disable' ? `disableField('${fieldName}')` :
|
||||||
|
`showField('${fieldName}')`
|
||||||
|
|
||||||
|
const inverseActionCode = action === 'show' ? `hideField('${fieldName}')` :
|
||||||
|
action === 'hide' ? `showField('${fieldName}')` :
|
||||||
|
action === 'enable' ? `disableField('${fieldName}')` :
|
||||||
|
action === 'disable' ? `enableField('${fieldName}')` :
|
||||||
|
`hideField('${fieldName}')`
|
||||||
|
|
||||||
|
// Generate field change listeners
|
||||||
|
const watchedFields = [...new Set(conditions.map(c => c.field).filter(Boolean))]
|
||||||
|
|
||||||
|
watchedFields.forEach(watchField => {
|
||||||
|
const script = `
|
||||||
|
// Conditional logic for field: ${fieldName}
|
||||||
|
onFieldChange('${watchField}', function() {
|
||||||
|
if (${conditionChecks}) {
|
||||||
|
${actionCode};
|
||||||
|
} else {
|
||||||
|
${inverseActionCode};
|
||||||
|
}
|
||||||
|
});`
|
||||||
|
scriptsArray.push(script)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also add initial evaluation
|
||||||
|
const initialScript = `
|
||||||
|
// Initial evaluation for field: ${fieldName}
|
||||||
|
(function() {
|
||||||
|
if (${conditionChecks}) {
|
||||||
|
${actionCode};
|
||||||
|
} else {
|
||||||
|
${inverseActionCode};
|
||||||
|
}
|
||||||
|
})();`
|
||||||
|
scriptsArray.push(initialScript)
|
||||||
|
})
|
||||||
|
|
||||||
|
return scriptsArray.join('\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update generated script when components change
|
||||||
|
watch(() => props.formComponents, () => {
|
||||||
|
if (isInitialized.value) {
|
||||||
|
updateConditionalLogic()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// Update conditional logic
|
||||||
|
const updateConditionalLogic = () => {
|
||||||
|
const newScript = generateConditionalLogicScript()
|
||||||
|
generatedScript.value = newScript
|
||||||
|
|
||||||
|
// Emit the generated script to parent component
|
||||||
|
emit('script-generated', newScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on mount
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
updateConditionalLogic()
|
||||||
|
isInitialized.value = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Expose the generated script for debugging
|
||||||
|
defineExpose({
|
||||||
|
generatedScript,
|
||||||
|
updateConditionalLogic
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.conditional-logic-engine {
|
||||||
|
display: none; /* This component is purely functional */
|
||||||
|
}
|
||||||
|
</style>
|
@ -81,7 +81,7 @@
|
|||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<component-preview
|
<component-preview
|
||||||
:component="element"
|
:component="element"
|
||||||
:is-preview="true"
|
:is-preview="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -129,13 +129,20 @@ const availableComponents = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'Text Field',
|
name: 'Text Field',
|
||||||
category: 'Basic Inputs',
|
category: 'Basic Inputs',
|
||||||
icon: 'material-symbols:text-fields',
|
icon: 'heroicons:document-text',
|
||||||
description: 'Single line text input',
|
description: 'Single line text input',
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'Enter text...',
|
placeholder: 'Enter text...',
|
||||||
help: '',
|
help: '',
|
||||||
validation: ''
|
validation: '',
|
||||||
|
// Conditional Logic Properties
|
||||||
|
conditionalLogic: {
|
||||||
|
enabled: false,
|
||||||
|
conditions: [], // Array of condition objects
|
||||||
|
action: 'show', // 'show', 'hide', 'enable', 'disable'
|
||||||
|
operator: 'and' // 'and', 'or'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -148,7 +155,14 @@ const availableComponents = [
|
|||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
placeholder: 'Enter text...',
|
placeholder: 'Enter text...',
|
||||||
help: '',
|
help: '',
|
||||||
validation: ''
|
validation: '',
|
||||||
|
// Conditional Logic Properties
|
||||||
|
conditionalLogic: {
|
||||||
|
enabled: false,
|
||||||
|
conditions: [],
|
||||||
|
action: 'show',
|
||||||
|
operator: 'and'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -161,7 +175,14 @@ const availableComponents = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
placeholder: '0',
|
placeholder: '0',
|
||||||
help: '',
|
help: '',
|
||||||
validation: ''
|
validation: '',
|
||||||
|
// Conditional Logic Properties
|
||||||
|
conditionalLogic: {
|
||||||
|
enabled: false,
|
||||||
|
conditions: [],
|
||||||
|
action: 'show',
|
||||||
|
operator: 'and'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -174,7 +195,14 @@ const availableComponents = [
|
|||||||
type: 'email',
|
type: 'email',
|
||||||
placeholder: 'email@example.com',
|
placeholder: 'email@example.com',
|
||||||
help: '',
|
help: '',
|
||||||
validation: 'email'
|
validation: 'email',
|
||||||
|
// Conditional Logic Properties
|
||||||
|
conditionalLogic: {
|
||||||
|
enabled: false,
|
||||||
|
conditions: [],
|
||||||
|
action: 'show',
|
||||||
|
operator: 'and'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -187,7 +215,14 @@ const availableComponents = [
|
|||||||
type: 'password',
|
type: 'password',
|
||||||
placeholder: 'Enter password...',
|
placeholder: 'Enter password...',
|
||||||
help: '',
|
help: '',
|
||||||
validation: ''
|
validation: '',
|
||||||
|
// Conditional Logic Properties
|
||||||
|
conditionalLogic: {
|
||||||
|
enabled: false,
|
||||||
|
conditions: [],
|
||||||
|
action: 'show',
|
||||||
|
operator: 'and'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -259,7 +294,14 @@ const availableComponents = [
|
|||||||
{ label: 'Option 2', value: 'option_2' },
|
{ label: 'Option 2', value: 'option_2' },
|
||||||
{ label: 'Option 3', value: 'option_3' }
|
{ label: 'Option 3', value: 'option_3' }
|
||||||
],
|
],
|
||||||
validation: ''
|
validation: '',
|
||||||
|
// Conditional Logic Properties
|
||||||
|
conditionalLogic: {
|
||||||
|
enabled: false,
|
||||||
|
conditions: [],
|
||||||
|
action: 'show',
|
||||||
|
operator: 'and'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -449,7 +491,17 @@ const availableComponents = [
|
|||||||
buttonText: 'Add Item',
|
buttonText: 'Add Item',
|
||||||
minItems: 0,
|
minItems: 0,
|
||||||
maxItems: 20,
|
maxItems: 20,
|
||||||
defaultItems: ['Item 1', 'Item 2']
|
defaultItems: ['Item 1', 'Item 2'],
|
||||||
|
itemValidation: '',
|
||||||
|
allowDuplicates: true,
|
||||||
|
enableSorting: false,
|
||||||
|
enableSearch: false,
|
||||||
|
itemType: 'text',
|
||||||
|
showItemCounter: true,
|
||||||
|
confirmDelete: false,
|
||||||
|
bulkOperations: false,
|
||||||
|
exportFormat: 'json',
|
||||||
|
importEnabled: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -210,6 +210,131 @@
|
|||||||
min="1"
|
min="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New Enhanced Settings -->
|
||||||
|
<div class="space-y-4 border-t pt-4 mt-4">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Item Settings</h4>
|
||||||
|
|
||||||
|
<!-- Item Type -->
|
||||||
|
<FormKit
|
||||||
|
v-if="showField('itemType')"
|
||||||
|
type="select"
|
||||||
|
label="Item Type"
|
||||||
|
name="itemType"
|
||||||
|
v-model="configModel.itemType"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Text', value: 'text' },
|
||||||
|
{ label: 'Number', value: 'number' },
|
||||||
|
{ label: 'Email', value: 'email' },
|
||||||
|
{ label: 'URL', value: 'url' }
|
||||||
|
]"
|
||||||
|
help="Type of data for list items"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Item Validation -->
|
||||||
|
<FormKit
|
||||||
|
v-if="showField('itemValidation')"
|
||||||
|
type="text"
|
||||||
|
label="Item Validation Rules"
|
||||||
|
name="itemValidation"
|
||||||
|
v-model="configModel.itemValidation"
|
||||||
|
help="Validation rules for individual items (e.g., 'required|min:3|max:50')"
|
||||||
|
placeholder="required|min:3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Behavior Settings -->
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div v-if="showField('allowDuplicates')" class="space-y-2">
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Allow Duplicates"
|
||||||
|
name="allowDuplicates"
|
||||||
|
v-model="configModel.allowDuplicates"
|
||||||
|
help="Allow duplicate items in the list"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showField('enableSorting')" class="space-y-2">
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Enable Sorting"
|
||||||
|
name="enableSorting"
|
||||||
|
v-model="configModel.enableSorting"
|
||||||
|
help="Allow drag & drop reordering of items"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showField('enableSearch')" class="space-y-2">
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Enable Search"
|
||||||
|
name="enableSearch"
|
||||||
|
v-model="configModel.enableSearch"
|
||||||
|
help="Add search/filter functionality"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showField('showItemCounter')" class="space-y-2">
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Show Item Counter"
|
||||||
|
name="showItemCounter"
|
||||||
|
v-model="configModel.showItemCounter"
|
||||||
|
help="Display current item count"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showField('confirmDelete')" class="space-y-2">
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Confirm Delete"
|
||||||
|
name="confirmDelete"
|
||||||
|
v-model="configModel.confirmDelete"
|
||||||
|
help="Require confirmation before deleting items"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showField('bulkOperations')" class="space-y-2">
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Bulk Operations"
|
||||||
|
name="bulkOperations"
|
||||||
|
v-model="configModel.bulkOperations"
|
||||||
|
help="Enable bulk select/delete operations"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import/Export Settings -->
|
||||||
|
<div class="space-y-3 border-t pt-3">
|
||||||
|
<h5 class="text-xs font-medium text-gray-600 uppercase tracking-wider">Import/Export</h5>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<FormKit
|
||||||
|
v-if="showField('importEnabled')"
|
||||||
|
type="checkbox"
|
||||||
|
label="Enable Import"
|
||||||
|
name="importEnabled"
|
||||||
|
v-model="configModel.importEnabled"
|
||||||
|
help="Allow importing items from file"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
v-if="showField('exportFormat')"
|
||||||
|
type="select"
|
||||||
|
label="Export Format"
|
||||||
|
name="exportFormat"
|
||||||
|
v-model="configModel.exportFormat"
|
||||||
|
:options="[
|
||||||
|
{ label: 'JSON', value: 'json' },
|
||||||
|
{ label: 'CSV', value: 'csv' },
|
||||||
|
{ label: 'Plain Text', value: 'txt' }
|
||||||
|
]"
|
||||||
|
help="Default export format for items"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="showField('defaultItems')">
|
<div v-if="showField('defaultItems')">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Default Items</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Default Items</label>
|
||||||
@ -919,6 +1044,17 @@ const showField = (fieldName) => {
|
|||||||
return componentType === 'repeating-group' || componentType === 'dynamic-list';
|
return componentType === 'repeating-group' || componentType === 'dynamic-list';
|
||||||
case 'defaultItems':
|
case 'defaultItems':
|
||||||
return componentType === 'dynamic-list';
|
return componentType === 'dynamic-list';
|
||||||
|
case 'itemValidation':
|
||||||
|
case 'allowDuplicates':
|
||||||
|
case 'enableSorting':
|
||||||
|
case 'enableSearch':
|
||||||
|
case 'itemType':
|
||||||
|
case 'showItemCounter':
|
||||||
|
case 'confirmDelete':
|
||||||
|
case 'bulkOperations':
|
||||||
|
case 'exportFormat':
|
||||||
|
case 'importEnabled':
|
||||||
|
return componentType === 'dynamic-list';
|
||||||
case 'fields':
|
case 'fields':
|
||||||
return componentType === 'info-display' || componentType === 'repeating-group';
|
return componentType === 'info-display' || componentType === 'repeating-group';
|
||||||
case 'layout':
|
case 'layout':
|
||||||
@ -1024,7 +1160,7 @@ const fieldsToShow = {
|
|||||||
button: ['label', 'name', 'help', 'buttonType', 'variant', 'size', 'disabled', 'onClick', 'width'],
|
button: ['label', 'name', 'help', 'buttonType', 'variant', 'size', 'disabled', 'onClick', 'width'],
|
||||||
'image-preview': ['label', 'name', 'help', 'imageUrl', 'altText', 'caption', 'showZoom', 'showCaption', 'maxWidth', 'height', 'width'],
|
'image-preview': ['label', 'name', 'help', 'imageUrl', 'altText', 'caption', 'showZoom', 'showCaption', 'maxWidth', 'height', 'width'],
|
||||||
'repeating-group': ['label', 'name', 'help', 'minItems', 'maxItems', 'buttonText', 'removeText', 'fields', 'width'],
|
'repeating-group': ['label', 'name', 'help', 'minItems', 'maxItems', 'buttonText', 'removeText', 'fields', 'width'],
|
||||||
'dynamic-list': ['label', 'name', 'help', 'placeholder', 'buttonText', 'minItems', 'maxItems', 'defaultItems', 'width'],
|
'dynamic-list': ['label', 'name', 'help', 'placeholder', 'buttonText', 'minItems', 'maxItems', 'defaultItems', 'itemValidation', 'allowDuplicates', 'enableSorting', 'enableSearch', 'itemType', 'showItemCounter', 'confirmDelete', 'bulkOperations', 'exportFormat', 'importEnabled', 'width'],
|
||||||
|
|
||||||
// Layout elements
|
// Layout elements
|
||||||
heading: ['value', 'level', 'width'],
|
heading: ['value', 'level', 'width'],
|
||||||
|
@ -305,14 +305,14 @@
|
|||||||
placeholder="5"
|
placeholder="5"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="field-wrapper">
|
<FormKit
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Allow Multiple Files</label>
|
type="switch"
|
||||||
<label class="toggle-switch">
|
label="Allow Multiple Files"
|
||||||
<input type="checkbox" v-model="configModel.multiple" />
|
name="multiple"
|
||||||
<span class="toggle-slider"></span>
|
v-model="configModel.multiple"
|
||||||
<span class="toggle-label">{{ configModel.multiple ? 'Yes' : 'No' }}</span>
|
help="Enable uploading multiple files at once"
|
||||||
</label>
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -417,6 +417,197 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Dynamic List Configuration -->
|
||||||
|
<template v-if="component.type === 'dynamic-list'">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Basic List Settings -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Add Button Text"
|
||||||
|
name="buttonText"
|
||||||
|
v-model="configModel.buttonText"
|
||||||
|
help="Text for the add item button"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="Add Item"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Item Type"
|
||||||
|
name="itemType"
|
||||||
|
v-model="configModel.itemType"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Text', value: 'text' },
|
||||||
|
{ label: 'Number', value: 'number' },
|
||||||
|
{ label: 'Email', value: 'email' },
|
||||||
|
{ label: 'URL', value: 'url' }
|
||||||
|
]"
|
||||||
|
help="Type of data for list items"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item Limits -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="number"
|
||||||
|
label="Minimum Items"
|
||||||
|
name="minItems"
|
||||||
|
v-model="configModel.minItems"
|
||||||
|
help="Minimum number of items required"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
:min="0"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="number"
|
||||||
|
label="Maximum Items"
|
||||||
|
name="maxItems"
|
||||||
|
v-model="configModel.maxItems"
|
||||||
|
help="Maximum number of items allowed"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
:min="1"
|
||||||
|
placeholder="10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item Validation -->
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Item Validation Rules"
|
||||||
|
name="itemValidation"
|
||||||
|
v-model="configModel.itemValidation"
|
||||||
|
help="Validation rules for individual items (e.g., 'required|min:3|max:50')"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="required|min:3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Behavior Settings -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Behavior Settings</h5>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Allow Duplicates"
|
||||||
|
name="allowDuplicates"
|
||||||
|
v-model="configModel.allowDuplicates"
|
||||||
|
help="Allow duplicate items in the list"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Enable Sorting"
|
||||||
|
name="enableSorting"
|
||||||
|
v-model="configModel.enableSorting"
|
||||||
|
help="Allow drag & drop reordering"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Enable Search"
|
||||||
|
name="enableSearch"
|
||||||
|
v-model="configModel.enableSearch"
|
||||||
|
help="Add search/filter functionality"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Show Item Counter"
|
||||||
|
name="showItemCounter"
|
||||||
|
v-model="configModel.showItemCounter"
|
||||||
|
help="Display current item count"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Confirm Delete"
|
||||||
|
name="confirmDelete"
|
||||||
|
v-model="configModel.confirmDelete"
|
||||||
|
help="Require confirmation before deleting"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Bulk Operations"
|
||||||
|
name="bulkOperations"
|
||||||
|
v-model="configModel.bulkOperations"
|
||||||
|
help="Enable bulk select/delete operations"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import/Export Settings -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Import/Export Settings</h5>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Enable Import"
|
||||||
|
name="importEnabled"
|
||||||
|
v-model="configModel.importEnabled"
|
||||||
|
help="Allow importing items from files"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Export Format"
|
||||||
|
name="exportFormat"
|
||||||
|
v-model="configModel.exportFormat"
|
||||||
|
:options="[
|
||||||
|
{ label: 'JSON', value: 'json' },
|
||||||
|
{ label: 'CSV', value: 'csv' },
|
||||||
|
{ label: 'Text', value: 'txt' }
|
||||||
|
]"
|
||||||
|
help="Default format for exporting items"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default Items -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Default Items</h5>
|
||||||
|
|
||||||
|
<div class="border rounded-md p-3 bg-gray-50 space-y-2">
|
||||||
|
<div v-for="(item, index) in (configModel.defaultItems || [])" :key="index" class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="configModel.defaultItems[index]"
|
||||||
|
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
||||||
|
placeholder="Enter default item"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeDefaultItem(index)"
|
||||||
|
class="ml-2 text-red-500 hover:text-red-700"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="addDefaultItem"
|
||||||
|
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||||
|
Add Default Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Content for Text Components -->
|
<!-- Content for Text Components -->
|
||||||
<template v-if="showField('value')">
|
<template v-if="showField('value')">
|
||||||
<FormKit
|
<FormKit
|
||||||
@ -516,7 +707,7 @@
|
|||||||
<div class="validation-options">
|
<div class="validation-options">
|
||||||
<!-- Required Field -->
|
<!-- Required Field -->
|
||||||
<div class="validation-item">
|
<div class="validation-item">
|
||||||
<label class="validation-toggle">
|
<label class="validation-toggle" @click.stop>
|
||||||
<input type="checkbox" v-model="isRequired" />
|
<input type="checkbox" v-model="isRequired" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
<div class="validation-info">
|
<div class="validation-info">
|
||||||
@ -528,7 +719,7 @@
|
|||||||
|
|
||||||
<!-- Email Validation -->
|
<!-- Email Validation -->
|
||||||
<div v-if="component.type === 'email' || component.type === 'text'" class="validation-item">
|
<div v-if="component.type === 'email' || component.type === 'text'" class="validation-item">
|
||||||
<label class="validation-toggle">
|
<label class="validation-toggle" @click.stop>
|
||||||
<input type="checkbox" v-model="isEmailValidation" />
|
<input type="checkbox" v-model="isEmailValidation" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
<div class="validation-info">
|
<div class="validation-info">
|
||||||
@ -540,7 +731,7 @@
|
|||||||
|
|
||||||
<!-- URL Validation -->
|
<!-- URL Validation -->
|
||||||
<div v-if="component.type === 'url' || component.type === 'text'" class="validation-item">
|
<div v-if="component.type === 'url' || component.type === 'text'" class="validation-item">
|
||||||
<label class="validation-toggle">
|
<label class="validation-toggle" @click.stop>
|
||||||
<input type="checkbox" v-model="isUrlValidation" />
|
<input type="checkbox" v-model="isUrlValidation" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
<div class="validation-info">
|
<div class="validation-info">
|
||||||
@ -552,7 +743,7 @@
|
|||||||
|
|
||||||
<!-- Length Validation -->
|
<!-- Length Validation -->
|
||||||
<div v-if="isTextBasedField" class="validation-item">
|
<div v-if="isTextBasedField" class="validation-item">
|
||||||
<label class="validation-toggle">
|
<label class="validation-toggle" @click.stop>
|
||||||
<input type="checkbox" v-model="hasLengthValidation" />
|
<input type="checkbox" v-model="hasLengthValidation" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
<div class="validation-info">
|
<div class="validation-info">
|
||||||
@ -583,7 +774,7 @@
|
|||||||
|
|
||||||
<!-- Number Range Validation -->
|
<!-- Number Range Validation -->
|
||||||
<div v-if="component.type === 'number'" class="validation-item">
|
<div v-if="component.type === 'number'" class="validation-item">
|
||||||
<label class="validation-toggle">
|
<label class="validation-toggle" @click.stop>
|
||||||
<input type="checkbox" v-model="hasNumberValidation" />
|
<input type="checkbox" v-model="hasNumberValidation" />
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
<div class="validation-info">
|
<div class="validation-info">
|
||||||
@ -618,10 +809,286 @@
|
|||||||
label="Custom Validation Rules"
|
label="Custom Validation Rules"
|
||||||
name="validation"
|
name="validation"
|
||||||
v-model="configModel.validation"
|
v-model="configModel.validation"
|
||||||
help="Advanced: Enter custom validation rules (e.g., 'required|email|length:5,50')"
|
help="Advanced: Enter custom validation rules using the format below"
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
placeholder="Leave empty for basic validation"
|
placeholder="e.g., required|email|length:3,50"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Validation Rules Guide -->
|
||||||
|
<div class="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<h5 class="text-sm font-medium text-blue-800 mb-3 flex items-center">
|
||||||
|
<Icon name="heroicons:information-circle" class="w-4 h-4 mr-2" />
|
||||||
|
Validation Rules Guide
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div class="space-y-4 text-sm">
|
||||||
|
<!-- Basic Rules -->
|
||||||
|
<div>
|
||||||
|
<h6 class="font-medium text-blue-700 mb-2">Basic Rules</h6>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">required</code>
|
||||||
|
<span class="rule-desc">Field must be filled</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">email</code>
|
||||||
|
<span class="rule-desc">Must be valid email</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">url</code>
|
||||||
|
<span class="rule-desc">Must be valid URL</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">number</code>
|
||||||
|
<span class="rule-desc">Must be numeric</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Length Rules -->
|
||||||
|
<div>
|
||||||
|
<h6 class="font-medium text-blue-700 mb-2">Length Rules</h6>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">length:5</code>
|
||||||
|
<span class="rule-desc">Exactly 5 characters</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">length:3,20</code>
|
||||||
|
<span class="rule-desc">Between 3-20 characters</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">min:3</code>
|
||||||
|
<span class="rule-desc">Minimum 3 characters</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">max:50</code>
|
||||||
|
<span class="rule-desc">Maximum 50 characters</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Number Rules -->
|
||||||
|
<div>
|
||||||
|
<h6 class="font-medium text-blue-700 mb-2">Number Rules</h6>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">between:1,100</code>
|
||||||
|
<span class="rule-desc">Between 1 and 100</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">min_value:0</code>
|
||||||
|
<span class="rule-desc">Minimum value 0</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">max_value:999</code>
|
||||||
|
<span class="rule-desc">Maximum value 999</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">integer</code>
|
||||||
|
<span class="rule-desc">Must be whole number</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pattern Rules -->
|
||||||
|
<div>
|
||||||
|
<h6 class="font-medium text-blue-700 mb-2">Pattern Rules</h6>
|
||||||
|
<div class="grid grid-cols-1 gap-2 text-xs">
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">alpha</code>
|
||||||
|
<span class="rule-desc">Only letters (A-Z, a-z)</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">alpha_numeric</code>
|
||||||
|
<span class="rule-desc">Letters and numbers only</span>
|
||||||
|
</div>
|
||||||
|
<div class="validation-rule">
|
||||||
|
<code class="rule-code">alpha_spaces</code>
|
||||||
|
<span class="rule-desc">Letters and spaces only</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Examples Section -->
|
||||||
|
<div class="mt-4 pt-4 border-t border-blue-200">
|
||||||
|
<h6 class="font-medium text-blue-700 mb-2">Examples</h6>
|
||||||
|
<div class="space-y-2 text-xs">
|
||||||
|
<div class="example-item">
|
||||||
|
<code class="example-code">required|email</code>
|
||||||
|
<span class="example-desc">Required email field</span>
|
||||||
|
</div>
|
||||||
|
<div class="example-item">
|
||||||
|
<code class="example-code">required|length:3,50</code>
|
||||||
|
<span class="example-desc">Required text, 3-50 characters</span>
|
||||||
|
</div>
|
||||||
|
<div class="example-item">
|
||||||
|
<code class="example-code">number|between:1,100</code>
|
||||||
|
<span class="example-desc">Number between 1-100</span>
|
||||||
|
</div>
|
||||||
|
<div class="example-item">
|
||||||
|
<code class="example-code">required|alpha_numeric|min:5</code>
|
||||||
|
<span class="example-desc">Required alphanumeric, min 5 chars</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Tips -->
|
||||||
|
<div class="mt-3 pt-3 border-t border-blue-200">
|
||||||
|
<p class="text-xs text-blue-600">
|
||||||
|
<strong>💡 Tips:</strong> Separate rules with | (pipe) • Order doesn't matter • Leave empty for no validation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conditional Logic Section -->
|
||||||
|
<div v-if="showField('conditionalLogic')" class="section-container mt-6">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-wrapper">
|
||||||
|
<div class="section-header">
|
||||||
|
<h4 class="section-title">
|
||||||
|
<Icon name="heroicons:adjustments-horizontal" class="w-5 h-5 mr-2" />
|
||||||
|
Conditional Logic
|
||||||
|
</h4>
|
||||||
|
<p class="section-description">Show, hide, or disable this field based on other field values</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-content">
|
||||||
|
<!-- Enable Conditional Logic -->
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Enable Conditional Logic"
|
||||||
|
name="conditionalLogicEnabled"
|
||||||
|
v-model="configModel.conditionalLogic.enabled"
|
||||||
|
help="Show or hide this field based on other fields"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Conditional Logic Configuration -->
|
||||||
|
<div v-if="configModel.conditionalLogic.enabled" class="mt-4 space-y-4">
|
||||||
|
<!-- Action Selection -->
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Action"
|
||||||
|
name="conditionalAction"
|
||||||
|
v-model="configModel.conditionalLogic.action"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Show this field', value: 'show' },
|
||||||
|
{ label: 'Hide this field', value: 'hide' },
|
||||||
|
{ label: 'Enable this field', value: 'enable' },
|
||||||
|
{ label: 'Disable this field', value: 'disable' }
|
||||||
|
]"
|
||||||
|
help="What should happen when conditions are met"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Operator Selection (when multiple conditions) -->
|
||||||
|
<FormKit
|
||||||
|
v-if="configModel.conditionalLogic.conditions.length > 1"
|
||||||
|
type="select"
|
||||||
|
label="Logic Operator"
|
||||||
|
name="conditionalOperator"
|
||||||
|
v-model="configModel.conditionalLogic.operator"
|
||||||
|
:options="[
|
||||||
|
{ label: 'All conditions must be true (AND)', value: 'and' },
|
||||||
|
{ label: 'Any condition can be true (OR)', value: 'or' }
|
||||||
|
]"
|
||||||
|
help="How multiple conditions should be evaluated"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Conditions -->
|
||||||
|
<div class="conditions-section">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 mb-3">Conditions</h5>
|
||||||
|
|
||||||
|
<div v-if="configModel.conditionalLogic.conditions.length === 0" class="text-center py-4 text-gray-500 border-2 border-dashed border-gray-300 rounded-lg">
|
||||||
|
<Icon name="heroicons:plus-circle" class="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
||||||
|
<p class="text-sm">No conditions set. Add a condition to get started.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(condition, index) in configModel.conditionalLogic.conditions"
|
||||||
|
:key="index"
|
||||||
|
class="condition-item bg-gray-50 p-4 rounded-lg border"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Field Name"
|
||||||
|
:name="`condition_field_${index}`"
|
||||||
|
v-model="condition.field"
|
||||||
|
placeholder="field_name"
|
||||||
|
help="Name of the field to check"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Operator"
|
||||||
|
:name="`condition_operator_${index}`"
|
||||||
|
v-model="condition.operator"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Equals', value: 'equals' },
|
||||||
|
{ label: 'Not equals', value: 'not_equals' },
|
||||||
|
{ label: 'Contains', value: 'contains' },
|
||||||
|
{ label: 'Does not contain', value: 'not_contains' },
|
||||||
|
{ label: 'Is empty', value: 'is_empty' },
|
||||||
|
{ label: 'Is not empty', value: 'is_not_empty' },
|
||||||
|
{ label: 'Greater than', value: 'greater_than' },
|
||||||
|
{ label: 'Less than', value: 'less_than' }
|
||||||
|
]"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Value"
|
||||||
|
:name="`condition_value_${index}`"
|
||||||
|
v-model="condition.value"
|
||||||
|
placeholder="Value to compare"
|
||||||
|
help="Leave empty for 'is empty' conditions"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-3">
|
||||||
|
<button
|
||||||
|
@click="removeCondition(index)"
|
||||||
|
type="button"
|
||||||
|
class="text-red-600 hover:text-red-800 text-sm flex items-center"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:trash" class="w-4 h-4 mr-1" />
|
||||||
|
Remove Condition
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="addCondition"
|
||||||
|
type="button"
|
||||||
|
class="mt-3 inline-flex items-center px-3 py-2 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:plus" class="w-4 h-4 mr-1" />
|
||||||
|
Add Condition
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview JavaScript Code -->
|
||||||
|
<div v-if="configModel.conditionalLogic.conditions.length > 0" class="mt-4">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 mb-2">Generated Code Preview</h5>
|
||||||
|
<div class="bg-gray-900 text-green-400 p-3 rounded text-xs font-mono">
|
||||||
|
{{ generateConditionalLogicCode() }}
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">This JavaScript will be automatically executed to handle the conditional logic</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -713,7 +1180,8 @@ const getComponentIcon = (type) => {
|
|||||||
heading: 'heroicons:h1',
|
heading: 'heroicons:h1',
|
||||||
paragraph: 'heroicons:document-text',
|
paragraph: 'heroicons:document-text',
|
||||||
divider: 'heroicons:minus',
|
divider: 'heroicons:minus',
|
||||||
'info-display': 'heroicons:information-circle'
|
'info-display': 'heroicons:information-circle',
|
||||||
|
'dynamic-list': 'heroicons:list-bullet'
|
||||||
}
|
}
|
||||||
return icons[type] || 'heroicons:square-3-stack-3d'
|
return icons[type] || 'heroicons:square-3-stack-3d'
|
||||||
}
|
}
|
||||||
@ -745,7 +1213,8 @@ const getComponentTypeName = (type) => {
|
|||||||
heading: 'Heading Text',
|
heading: 'Heading Text',
|
||||||
paragraph: 'Paragraph Text',
|
paragraph: 'Paragraph Text',
|
||||||
divider: 'Divider Line',
|
divider: 'Divider Line',
|
||||||
'info-display': 'Information Display'
|
'info-display': 'Information Display',
|
||||||
|
'dynamic-list': 'Dynamic List'
|
||||||
}
|
}
|
||||||
return names[type] || 'Form Field'
|
return names[type] || 'Form Field'
|
||||||
}
|
}
|
||||||
@ -777,7 +1246,8 @@ const getComponentDescription = (type) => {
|
|||||||
heading: 'Section heading to organize form content',
|
heading: 'Section heading to organize form content',
|
||||||
paragraph: 'Text content for instructions and descriptions',
|
paragraph: 'Text content for instructions and descriptions',
|
||||||
divider: 'Visual separator to organize form sections',
|
divider: 'Visual separator to organize form sections',
|
||||||
'info-display': 'Read-only information display in organized format'
|
'info-display': 'Read-only information display in organized format',
|
||||||
|
'dynamic-list': 'Dynamic list for displaying and managing items'
|
||||||
}
|
}
|
||||||
return descriptions[type] || 'Configure this form field'
|
return descriptions[type] || 'Configure this form field'
|
||||||
}
|
}
|
||||||
@ -802,13 +1272,14 @@ const showField = (fieldName) => {
|
|||||||
if (!props.component) return false
|
if (!props.component) return false
|
||||||
|
|
||||||
const fieldConfig = {
|
const fieldConfig = {
|
||||||
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
|
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'dynamic-list'],
|
||||||
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
|
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'dynamic-list'],
|
||||||
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select'],
|
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'dynamic-list'],
|
||||||
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
|
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'dynamic-list'],
|
||||||
value: ['heading', 'paragraph', 'hidden'],
|
value: ['heading', 'paragraph', 'hidden'],
|
||||||
width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'info-display'],
|
width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'heading', 'paragraph', 'info-display', 'dynamic-list'],
|
||||||
options: ['select', 'checkbox', 'radio']
|
options: ['select', 'checkbox', 'radio'],
|
||||||
|
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'dynamic-list']
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldConfig[fieldName]?.includes(props.component.type) || false
|
return fieldConfig[fieldName]?.includes(props.component.type) || false
|
||||||
@ -817,7 +1288,7 @@ const showField = (fieldName) => {
|
|||||||
const hasOptions = computed(() => showField('options'))
|
const hasOptions = computed(() => showField('options'))
|
||||||
const hasSpecificSettings = computed(() => {
|
const hasSpecificSettings = computed(() => {
|
||||||
if (!props.component) return false
|
if (!props.component) return false
|
||||||
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'text', 'textarea', 'paragraph', 'button']
|
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'paragraph', 'button', 'dynamic-list']
|
||||||
return specificTypes.includes(props.component.type)
|
return specificTypes.includes(props.component.type)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -936,7 +1407,8 @@ const getRecommendedWidth = (fieldType) => {
|
|||||||
'range': 'full',
|
'range': 'full',
|
||||||
'switch': 'full',
|
'switch': 'full',
|
||||||
'button': 'full',
|
'button': 'full',
|
||||||
'info-display': 'full'
|
'info-display': 'full',
|
||||||
|
'dynamic-list': 'full'
|
||||||
}
|
}
|
||||||
|
|
||||||
return recommendations[fieldType] || 'full'
|
return recommendations[fieldType] || 'full'
|
||||||
@ -1011,6 +1483,108 @@ const removeOption = (index) => {
|
|||||||
configModel.value.options.splice(index, 1)
|
configModel.value.options.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default items management for dynamic-list
|
||||||
|
const addDefaultItem = () => {
|
||||||
|
if (!configModel.value.defaultItems) {
|
||||||
|
configModel.value.defaultItems = []
|
||||||
|
}
|
||||||
|
configModel.value.defaultItems.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeDefaultItem = (index) => {
|
||||||
|
if (configModel.value.defaultItems) {
|
||||||
|
configModel.value.defaultItems.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditional logic management
|
||||||
|
const addCondition = () => {
|
||||||
|
if (!configModel.value.conditionalLogic) {
|
||||||
|
configModel.value.conditionalLogic = {
|
||||||
|
enabled: true,
|
||||||
|
conditions: [],
|
||||||
|
action: 'show',
|
||||||
|
operator: 'and'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configModel.value.conditionalLogic.conditions) {
|
||||||
|
configModel.value.conditionalLogic.conditions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
configModel.value.conditionalLogic.conditions.push({
|
||||||
|
field: '',
|
||||||
|
operator: 'equals',
|
||||||
|
value: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCondition = (index) => {
|
||||||
|
if (configModel.value.conditionalLogic?.conditions) {
|
||||||
|
configModel.value.conditionalLogic.conditions.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateConditionalLogicCode = () => {
|
||||||
|
if (!configModel.value.conditionalLogic || !configModel.value.conditionalLogic.conditions.length) {
|
||||||
|
return '// No conditions defined'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { conditions, action, operator } = configModel.value.conditionalLogic
|
||||||
|
const fieldName = configModel.value.name || 'this_field'
|
||||||
|
|
||||||
|
// Generate condition checks
|
||||||
|
const conditionChecks = conditions.map(condition => {
|
||||||
|
const { field, operator: condOp, value } = condition
|
||||||
|
|
||||||
|
switch (condOp) {
|
||||||
|
case 'equals':
|
||||||
|
return `getField('${field}') === '${value}'`
|
||||||
|
case 'not_equals':
|
||||||
|
return `getField('${field}') !== '${value}'`
|
||||||
|
case 'contains':
|
||||||
|
return `String(getField('${field}')).includes('${value}')`
|
||||||
|
case 'not_contains':
|
||||||
|
return `!String(getField('${field}')).includes('${value}')`
|
||||||
|
case 'is_empty':
|
||||||
|
return `!getField('${field}') || getField('${field}') === ''`
|
||||||
|
case 'is_not_empty':
|
||||||
|
return `getField('${field}') && getField('${field}') !== ''`
|
||||||
|
case 'greater_than':
|
||||||
|
return `Number(getField('${field}')) > ${Number(value) || 0}`
|
||||||
|
case 'less_than':
|
||||||
|
return `Number(getField('${field}')) < ${Number(value) || 0}`
|
||||||
|
default:
|
||||||
|
return `getField('${field}') === '${value}'`
|
||||||
|
}
|
||||||
|
}).join(` ${operator} `)
|
||||||
|
|
||||||
|
// Generate action
|
||||||
|
const actionCode = action === 'show' ? `showField('${fieldName}')` :
|
||||||
|
action === 'hide' ? `hideField('${fieldName}')` :
|
||||||
|
action === 'enable' ? `enableField('${fieldName}')` :
|
||||||
|
action === 'disable' ? `disableField('${fieldName}')` :
|
||||||
|
`showField('${fieldName}')`
|
||||||
|
|
||||||
|
// Generate field change listeners
|
||||||
|
const watchedFields = [...new Set(conditions.map(c => c.field).filter(Boolean))]
|
||||||
|
const listenerCode = watchedFields.map(field =>
|
||||||
|
`onFieldChange('${field}', function() {
|
||||||
|
if (${conditionChecks}) {
|
||||||
|
${actionCode};
|
||||||
|
} else {
|
||||||
|
${action === 'show' ? `hideField('${fieldName}')` :
|
||||||
|
action === 'hide' ? `showField('${fieldName}')` :
|
||||||
|
action === 'enable' ? `disableField('${fieldName}')` :
|
||||||
|
action === 'disable' ? `enableField('${fieldName}')` :
|
||||||
|
`hideField('${fieldName}')`};
|
||||||
|
}
|
||||||
|
});`
|
||||||
|
).join('\n\n')
|
||||||
|
|
||||||
|
return listenerCode || '// Invalid configuration'
|
||||||
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
emit('update-component', { ...props.component, props: configModel.value })
|
emit('update-component', { ...props.component, props: configModel.value })
|
||||||
handleClose()
|
handleClose()
|
||||||
@ -1316,4 +1890,29 @@ const handleClose = () => {
|
|||||||
.example-button {
|
.example-button {
|
||||||
@apply text-left p-2 text-xs bg-white border border-gray-200 rounded hover:border-blue-300 hover:bg-blue-50 transition-colors;
|
@apply text-left p-2 text-xs bg-white border border-gray-200 rounded hover:border-blue-300 hover:bg-blue-50 transition-colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Validation Guide Styles */
|
||||||
|
.validation-rule {
|
||||||
|
@apply flex flex-col space-y-1 p-2 bg-white rounded border border-blue-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-code {
|
||||||
|
@apply font-mono text-xs font-semibold text-blue-800 bg-blue-100 px-2 py-1 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-desc {
|
||||||
|
@apply text-gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-item {
|
||||||
|
@apply flex flex-col space-y-1 p-2 bg-white rounded border border-blue-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-code {
|
||||||
|
@apply font-mono text-xs font-semibold text-green-800 bg-green-100 px-2 py-1 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-desc {
|
||||||
|
@apply text-gray-600;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -9,33 +9,86 @@ function handleChange(event) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-start">
|
<div class="switch-container">
|
||||||
<div class="relative inline-block w-11 h-6">
|
<div class="switch-wrapper">
|
||||||
<input
|
<input
|
||||||
:id="context.id"
|
:id="context.id"
|
||||||
|
:name="context.name"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="context._value"
|
:checked="context.value"
|
||||||
:disabled="context.disabled"
|
:disabled="context.disabled"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
class="sr-only peer"
|
class="switch-input"
|
||||||
/>
|
/>
|
||||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
<label :for="context.id" class="switch-label">
|
||||||
|
<span class="switch-track">
|
||||||
|
<span class="switch-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Additional styles for better visual feedback */
|
.switch-container {
|
||||||
.peer:checked + div {
|
@apply flex items-center justify-start;
|
||||||
background-color: #3b82f6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer:disabled + div {
|
.switch-wrapper {
|
||||||
opacity: 0.5;
|
@apply relative;
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer:focus + div {
|
.switch-input {
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
@apply sr-only;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-track {
|
||||||
|
@apply relative inline-block w-11 h-6 bg-gray-200 rounded-full transition-colors duration-200 ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input:checked + .switch-label .switch-track {
|
||||||
|
@apply bg-blue-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input:disabled + .switch-label .switch-track {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input:disabled + .switch-label {
|
||||||
|
@apply cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-thumb {
|
||||||
|
@apply absolute top-[2px] left-[2px] w-5 h-5 bg-white border border-gray-300 rounded-full transition-transform duration-200 ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input:checked + .switch-label .switch-thumb {
|
||||||
|
@apply transform translate-x-5 border-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input:focus + .switch-label .switch-track {
|
||||||
|
@apply ring-4 ring-blue-300 ring-opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mac-specific fixes */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.switch-track,
|
||||||
|
.switch-thumb {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force hardware acceleration on Mac for smoother animations */
|
||||||
|
.switch-thumb {
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input:checked + .switch-label .switch-thumb {
|
||||||
|
transform: translateX(1.25rem) translateZ(0);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -21,6 +21,7 @@ The documentation is organized into the following sections:
|
|||||||
- [Technical Guide](./form-builder/TECHNICAL_GUIDE.md) - Technical details of the Form Builder implementation
|
- [Technical Guide](./form-builder/TECHNICAL_GUIDE.md) - Technical details of the Form Builder implementation
|
||||||
- [JavaScript API Reference](./form-builder/JAVASCRIPT_API.md) - Complete JavaScript API for dynamic forms and calculations
|
- [JavaScript API Reference](./form-builder/JAVASCRIPT_API.md) - Complete JavaScript API for dynamic forms and calculations
|
||||||
- [Grid System Guide](./form-builder/grid-system.md) - Visual grid system and layout documentation
|
- [Grid System Guide](./form-builder/grid-system.md) - Visual grid system and layout documentation
|
||||||
|
- [Roadmap](./form-builder/ROADMAP.md) - Development roadmap and missing essential features checklist
|
||||||
|
|
||||||
### Process Execution
|
### Process Execution
|
||||||
- [User Guide](./process-execution/USER_GUIDE.md) - How to use the Process Execution interface
|
- [User Guide](./process-execution/USER_GUIDE.md) - How to use the Process Execution interface
|
||||||
|
358
doc/form-builder/ROADMAP.md
Normal file
358
doc/form-builder/ROADMAP.md
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
# Form Builder Development Roadmap
|
||||||
|
|
||||||
|
This document outlines the planned improvements and missing essential features for the Corrad ProcessMaker Form Builder. It serves as a development checklist to track progress on enhancing form building capabilities.
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
**Last Updated**: December 2024
|
||||||
|
|
||||||
|
**Current Version**: v2.0 with JavaScript API and dynamic calculations
|
||||||
|
|
||||||
|
### Recently Completed Features ✅
|
||||||
|
- JavaScript Execution Engine (`FormScriptEngine`) for real-time calculations
|
||||||
|
- Dynamic field updates with `onFieldChange` and `onLoad` event handlers
|
||||||
|
- Enhanced debugging with comprehensive logging
|
||||||
|
- API functions: `setField()`, `getField()` for programmatic form control
|
||||||
|
- Grid system for responsive layouts
|
||||||
|
- Form versioning and management
|
||||||
|
- Security improvements with sandboxed JavaScript execution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Missing Essential Settings Analysis
|
||||||
|
|
||||||
|
### 🔴 Critical Priority (Must Have)
|
||||||
|
|
||||||
|
#### **1. Conditional Logic System**
|
||||||
|
**Status**: ✅ Completed December 2024
|
||||||
|
**Components Affected**: All form components
|
||||||
|
**Description**: Essential for dynamic forms that show/hide fields based on user input
|
||||||
|
|
||||||
|
**Implemented Features**:
|
||||||
|
- ✅ Field visibility rules (show/hide based on other field values)
|
||||||
|
- ✅ Field enabling/disabling based on conditions
|
||||||
|
- ✅ Multi-field conditional logic (AND/OR operators)
|
||||||
|
- ✅ Visual condition builder interface
|
||||||
|
- ✅ Field dependency chains
|
||||||
|
- ✅ JavaScript code generation and execution
|
||||||
|
- ✅ Integration with existing FormScriptEngine
|
||||||
|
- ✅ Real-time condition evaluation
|
||||||
|
|
||||||
|
**Impact**: High - Critical for advanced form workflows
|
||||||
|
|
||||||
|
**Implementation Details**:
|
||||||
|
- Added `conditionalLogic` properties to all form components
|
||||||
|
- Created visual condition builder in FormBuilderFieldSettingsModal
|
||||||
|
- Built ConditionalLogicEngine to generate and execute JavaScript
|
||||||
|
- Integrated with existing FormScriptEngine for seamless execution
|
||||||
|
- Supports multiple operators: equals, not_equals, contains, not_contains, is_empty, is_not_empty, greater_than, less_than
|
||||||
|
- Supports multiple actions: show, hide, enable, disable fields
|
||||||
|
- Real-time preview of generated JavaScript code
|
||||||
|
- Automatic script generation and injection
|
||||||
|
|
||||||
|
**Files Modified**:
|
||||||
|
- `components/FormBuilderComponents.vue` - Added conditional logic properties
|
||||||
|
- `components/FormBuilderFieldSettingsModal.vue` - Added condition builder interface
|
||||||
|
- `components/ConditionalLogicEngine.vue` - New component for script generation
|
||||||
|
- `pages/form-builder/index.vue` - Integrated conditional logic engine
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **2. Enhanced Validation System**
|
||||||
|
**Status**: 🟡 Partially Implemented
|
||||||
|
**Current**: Basic required, email, URL, min/max validations
|
||||||
|
**Components Affected**: All input components
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] Pattern matching validation (regex support)
|
||||||
|
- [ ] Custom validation functions
|
||||||
|
- [ ] Real-time validation feedback
|
||||||
|
- [ ] Cross-field validation (confirm password, date ranges)
|
||||||
|
- [ ] Conditional validation rules
|
||||||
|
- [ ] Custom error message templates
|
||||||
|
- [ ] Validation groups and schemas
|
||||||
|
|
||||||
|
**Impact**: High - Essential for data quality and user experience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **3. Accessibility Features**
|
||||||
|
**Status**: ❌ Not Implemented
|
||||||
|
**Components Affected**: All components
|
||||||
|
**Description**: WCAG compliance and screen reader support
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] ARIA labels and descriptions
|
||||||
|
- [ ] Keyboard navigation support
|
||||||
|
- [ ] Screen reader announcements
|
||||||
|
- [ ] High contrast mode support
|
||||||
|
- [ ] Focus management and indicators
|
||||||
|
- [ ] Alternative text for visual elements
|
||||||
|
- [ ] Accessibility validation tool
|
||||||
|
|
||||||
|
**Impact**: High - Required for compliance and inclusivity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟡 High Priority (Should Have)
|
||||||
|
|
||||||
|
#### **4. Dynamic List Component Enhancements**
|
||||||
|
**Status**: ✅ Completed December 2024
|
||||||
|
**Current Settings**: `['label', 'name', 'help', 'placeholder', 'buttonText', 'minItems', 'maxItems', 'defaultItems', 'width']`
|
||||||
|
|
||||||
|
**Implemented Essential Settings**:
|
||||||
|
- [x] Item validation (validate individual list items)
|
||||||
|
- [x] Uniqueness validation (prevent duplicate items)
|
||||||
|
- [x] Item type support (text, number, email, url)
|
||||||
|
- [x] Search/filter within list items
|
||||||
|
- [x] Bulk operations (select all, delete selected)
|
||||||
|
- [x] Import/Export functionality (JSON, CSV, TXT)
|
||||||
|
- [x] Item counter display
|
||||||
|
- [x] Delete confirmation
|
||||||
|
- [x] Visual sorting indicators (drag handles)
|
||||||
|
- [x] Enhanced UI with validation feedback
|
||||||
|
|
||||||
|
**Missing Features** (moved to future roadmap):
|
||||||
|
- [ ] Drag & drop reordering functionality (requires vue-draggable)
|
||||||
|
- [ ] Custom item templates (rich formatting)
|
||||||
|
|
||||||
|
**Impact**: Medium-High - Important for data collection workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **5. Enhanced Select Component**
|
||||||
|
**Status**: 🟡 Partially Implemented
|
||||||
|
**Current Settings**: `['label', 'name', 'placeholder', 'help', 'options', 'width']`
|
||||||
|
|
||||||
|
**Missing Essential Settings**:
|
||||||
|
- [ ] Searchable/filterable dropdown
|
||||||
|
- [ ] Multi-select with chips display
|
||||||
|
- [ ] Option groups/categories (optgroups)
|
||||||
|
- [ ] Dynamic options loading from API endpoints
|
||||||
|
- [ ] Custom option templates (with icons, descriptions)
|
||||||
|
- [ ] Maximum selections limit for multi-select
|
||||||
|
- [ ] Async option loading with loading states
|
||||||
|
|
||||||
|
**Impact**: Medium-High - Common requirement for complex forms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **6. Rich Text and Enhanced Text Components**
|
||||||
|
**Status**: ❌ Not Implemented
|
||||||
|
**Components Affected**: textarea, text components
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] WYSIWYG rich text editor integration
|
||||||
|
- [ ] Character counter display
|
||||||
|
- [ ] Auto-resize for textarea
|
||||||
|
- [ ] Auto-complete/suggestions
|
||||||
|
- [ ] Advanced input masks for specific formats
|
||||||
|
- [ ] Markdown support and preview
|
||||||
|
- [ ] Spell check integration
|
||||||
|
|
||||||
|
**Impact**: Medium-High - Needed for content-heavy forms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🟢 Medium Priority (Could Have)
|
||||||
|
|
||||||
|
#### **7. Enhanced File Upload Components**
|
||||||
|
**Status**: 🟡 Partially Implemented
|
||||||
|
**Current**: Basic file and dropzone with size/type restrictions
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] Progress indicators for upload status
|
||||||
|
- [ ] File preview thumbnails (images, PDFs)
|
||||||
|
- [ ] Drag reordering for multiple files
|
||||||
|
- [ ] File description/tagging capabilities
|
||||||
|
- [ ] Cloud storage integration (S3, Google Drive)
|
||||||
|
- [ ] Virus scanning configuration
|
||||||
|
- [ ] Image editing capabilities (crop, resize)
|
||||||
|
|
||||||
|
**Impact**: Medium - Enhances file handling workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **8. Advanced Number Component**
|
||||||
|
**Status**: 🟡 Partially Implemented
|
||||||
|
**Current**: Basic min/max/step support
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] Currency formatting (with symbol, locale)
|
||||||
|
- [ ] Percentage mode
|
||||||
|
- [ ] Thousand separators configuration
|
||||||
|
- [ ] Decimal precision control
|
||||||
|
- [ ] Scientific notation support
|
||||||
|
- [ ] Number formatting templates
|
||||||
|
- [ ] Unit of measurement support
|
||||||
|
|
||||||
|
**Impact**: Medium - Important for financial and scientific forms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **9. Enhanced Date/Time Components**
|
||||||
|
**Status**: 🟡 Partially Implemented
|
||||||
|
**Current**: Basic date, time, datetime-local
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] Date range restrictions (min/max dates)
|
||||||
|
- [ ] Disabled dates (weekends, holidays)
|
||||||
|
- [ ] Custom date format display
|
||||||
|
- [ ] Timezone selection and conversion
|
||||||
|
- [ ] Relative date options (today, tomorrow, etc.)
|
||||||
|
- [ ] Date picker themes and localization
|
||||||
|
- [ ] Recurring date patterns
|
||||||
|
|
||||||
|
**Impact**: Medium - Useful for scheduling and booking forms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔵 Low Priority (Nice to Have)
|
||||||
|
|
||||||
|
#### **10. Advanced Layout and Styling**
|
||||||
|
**Status**: ❌ Not Implemented
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] Custom CSS classes for advanced styling
|
||||||
|
- [ ] Component themes and appearance presets
|
||||||
|
- [ ] Advanced grid layout controls
|
||||||
|
- [ ] Responsive breakpoint settings
|
||||||
|
- [ ] Animation and transition effects
|
||||||
|
- [ ] Dark mode support
|
||||||
|
- [ ] Print-friendly layouts
|
||||||
|
|
||||||
|
**Impact**: Low - Aesthetic and branding enhancements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **11. Integration and API Features**
|
||||||
|
**Status**: ❌ Not Implemented
|
||||||
|
|
||||||
|
**Missing Features**:
|
||||||
|
- [ ] Webhook integrations for form submissions
|
||||||
|
- [ ] External API data binding
|
||||||
|
- [ ] Real-time collaboration features
|
||||||
|
- [ ] Form analytics and usage tracking
|
||||||
|
- [ ] A/B testing capabilities
|
||||||
|
- [ ] Multi-language support
|
||||||
|
- [ ] Form embedding options
|
||||||
|
|
||||||
|
**Impact**: Low - Advanced enterprise features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Timeline
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Q1 2025)
|
||||||
|
- [ ] Implement conditional logic system
|
||||||
|
- [ ] Enhance validation with pattern matching
|
||||||
|
- [ ] Add basic accessibility features
|
||||||
|
- [ ] Improve dynamic list component
|
||||||
|
|
||||||
|
### Phase 2: Enhancement (Q2 2025)
|
||||||
|
- [ ] Enhanced select component with search
|
||||||
|
- [ ] Rich text editor integration
|
||||||
|
- [ ] Advanced file upload features
|
||||||
|
- [ ] Number formatting enhancements
|
||||||
|
|
||||||
|
### Phase 3: Polish (Q3 2025)
|
||||||
|
- [ ] Date/time component improvements
|
||||||
|
- [ ] Advanced layout controls
|
||||||
|
- [ ] Performance optimizations
|
||||||
|
- [ ] Comprehensive testing
|
||||||
|
|
||||||
|
### Phase 4: Integration (Q4 2025)
|
||||||
|
- [ ] API integration features
|
||||||
|
- [ ] Analytics and reporting
|
||||||
|
- [ ] Multi-language support
|
||||||
|
- [ ] Advanced enterprise features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Technical Considerations
|
||||||
|
- Ensure backward compatibility with existing forms
|
||||||
|
- Maintain performance with large forms
|
||||||
|
- Follow Vue 3 composition API patterns
|
||||||
|
- Implement proper TypeScript definitions
|
||||||
|
- Add comprehensive unit and integration tests
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
- Keep the interface intuitive and user-friendly
|
||||||
|
- Maintain consistency with existing UI patterns
|
||||||
|
- Ensure mobile responsiveness
|
||||||
|
- Follow accessibility best practices
|
||||||
|
- Provide clear documentation and examples
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
Use the following format to track completed features:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### ✅ [Feature Name] - Completed [Date]
|
||||||
|
**Implemented by**: [Developer Name]
|
||||||
|
**PR/Commit**: [Link to implementation]
|
||||||
|
**Notes**: [Any important implementation details]
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ JavaScript API Integration - Completed December 2024
|
||||||
|
**Implemented by**: Development Team
|
||||||
|
**Notes**: Added FormScriptEngine with real-time calculations and field manipulation
|
||||||
|
|
||||||
|
### ✅ Dynamic List Component Enhancements - Completed December 2024
|
||||||
|
**Implemented by**: AI Assistant
|
||||||
|
**Components Modified**:
|
||||||
|
- `components/FormBuilderComponents.vue` - Enhanced component definition with new properties
|
||||||
|
- `components/FormBuilderConfiguration.vue` - Added comprehensive configuration interface
|
||||||
|
- `components/ComponentPreview.vue` - Implemented advanced functionality
|
||||||
|
**Features Added**:
|
||||||
|
- Item validation with custom rules (required, min/max length, email, URL)
|
||||||
|
- Uniqueness validation to prevent duplicate items
|
||||||
|
- Multiple item types support (text, number, email, URL)
|
||||||
|
- Search/filter functionality within list items
|
||||||
|
- Bulk operations (select all, delete selected items)
|
||||||
|
- Import/Export capabilities (JSON, CSV, TXT formats)
|
||||||
|
- Item counter display showing current/max items
|
||||||
|
- Delete confirmation dialog
|
||||||
|
- Enhanced UI with validation feedback and error messages
|
||||||
|
- Visual sorting indicators (drag handles for future drag & drop)
|
||||||
|
**Notes**: Significantly enhanced the dynamic list component from basic functionality to a feature-rich, professional-grade list management system
|
||||||
|
|
||||||
|
### ✅ Conditional Logic System - Completed December 2024
|
||||||
|
**Implemented by**: AI Assistant
|
||||||
|
**Components Modified**:
|
||||||
|
- `components/FormBuilderComponents.vue` - Added conditional logic properties to component definitions
|
||||||
|
- `components/FormBuilderFieldSettingsModal.vue` - Created visual condition builder interface
|
||||||
|
- `components/ConditionalLogicEngine.vue` - New component for JavaScript generation and execution
|
||||||
|
- `pages/form-builder/index.vue` - Integrated conditional logic engine with form builder
|
||||||
|
**Features Added**:
|
||||||
|
- Visual condition builder with drag-and-drop interface for setting up field dependencies
|
||||||
|
- Support for multiple condition operators (equals, not_equals, contains, is_empty, etc.)
|
||||||
|
- Multiple action types (show, hide, enable, disable fields)
|
||||||
|
- AND/OR logic operators for complex condition chains
|
||||||
|
- Real-time JavaScript code generation and preview
|
||||||
|
- Integration with existing FormScriptEngine for seamless execution
|
||||||
|
- Field dependency validation and error handling
|
||||||
|
- Auto-generated event listeners for field changes
|
||||||
|
- Initial condition evaluation on form load
|
||||||
|
**Impact**: Enables dynamic forms with sophisticated business logic and user experience flows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When implementing features from this roadmap:
|
||||||
|
|
||||||
|
1. Update the status from ❌ to 🟡 (In Progress) when starting
|
||||||
|
2. Move to ✅ when completed with implementation details
|
||||||
|
3. Update the main README.md if new documentation is added
|
||||||
|
4. Ensure all new features have corresponding tests
|
||||||
|
5. Update the user guide with new functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Next Review**: March 2025
|
@ -206,6 +206,13 @@
|
|||||||
@field-validate="handleScriptFieldValidate"
|
@field-validate="handleScriptFieldValidate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConditionalLogicEngine
|
||||||
|
ref="conditionalLogicEngine"
|
||||||
|
:form-components="formStore.formComponents"
|
||||||
|
:form-data="previewFormData"
|
||||||
|
@script-generated="handleConditionalLogicGenerated"
|
||||||
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
ref="previewForm"
|
ref="previewForm"
|
||||||
type="form"
|
type="form"
|
||||||
@ -863,6 +870,8 @@ import { nextTick } from 'vue';
|
|||||||
import FormBuilderHistory from "~/components/FormBuilderHistory.vue";
|
import FormBuilderHistory from "~/components/FormBuilderHistory.vue";
|
||||||
import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
|
import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
|
||||||
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
||||||
|
import FormScriptEngine from '~/components/FormScriptEngine.vue';
|
||||||
|
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
title: "Form Builder",
|
title: "Form Builder",
|
||||||
@ -904,6 +913,7 @@ const showFieldSettings = ref(false);
|
|||||||
const showFieldSettingsPanel = ref(false);
|
const showFieldSettingsPanel = ref(false);
|
||||||
const previewForm = ref(null);
|
const previewForm = ref(null);
|
||||||
const formScriptEngine = ref(null);
|
const formScriptEngine = ref(null);
|
||||||
|
const conditionalLogicEngine = ref(null);
|
||||||
|
|
||||||
// Settings tabs configuration
|
// Settings tabs configuration
|
||||||
const settingsTabs = [
|
const settingsTabs = [
|
||||||
@ -2427,6 +2437,12 @@ const deleteComponent = () => {
|
|||||||
toast.success('Component deleted successfully');
|
toast.success('Component deleted successfully');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConditionalLogicGenerated = (script) => {
|
||||||
|
// Add the generated script to the form's custom script
|
||||||
|
formStore.formCustomScript += `\n// Conditional Logic Script\n${script}`;
|
||||||
|
toast.success('Conditional logic script added successfully');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user