Add Info Display Component and Enhance Form Settings
- Introduced a new 'Info Display' component for displaying read-only information in a key-value format, with customizable layouts and styles. - Removed the 'Repeater' and 'Group' components from the available components list to streamline options. - Enhanced the Form Builder configuration to support custom scripts, CSS, and event handling, allowing for more dynamic form behavior. - Added a modal for Form Settings, enabling users to manage form properties, custom scripts, and event triggers effectively. - Updated the database schema to accommodate new fields for custom scripts, CSS, and event configurations.
This commit is contained in:
parent
48309d716f
commit
9ea4e18672
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="component-preview" :style="componentStyle">
|
<div
|
||||||
|
class="component-preview form-field"
|
||||||
|
:style="componentStyle"
|
||||||
|
:data-name="component.props.name"
|
||||||
|
:data-type="component.type"
|
||||||
|
>
|
||||||
<!-- Basic Input Types (including radio and checkbox) -->
|
<!-- Basic Input Types (including radio and checkbox) -->
|
||||||
<FormKit
|
<FormKit
|
||||||
v-if="isInputType"
|
v-if="isInputType"
|
||||||
@ -26,28 +31,7 @@
|
|||||||
:class="{ 'canvas-component': isPreview }"
|
:class="{ 'canvas-component': isPreview }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Group Component -->
|
|
||||||
<div v-else-if="component.type === 'group'" class="py-2 border border-gray-200 p-3 rounded">
|
|
||||||
<div class="text-sm font-medium mb-2">{{ component.props.label || 'Group' }}</div>
|
|
||||||
<div class="text-xs text-gray-500">{{ component.props.help || 'A group can contain other form elements' }}</div>
|
|
||||||
<div class="mt-3 p-3 bg-gray-50 border border-dashed border-gray-300 rounded text-center text-xs text-gray-400">
|
|
||||||
Empty group - add components here in the final form
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Repeater Component -->
|
|
||||||
<div v-else-if="component.type === 'repeater'" class="py-2 border border-gray-200 p-3 rounded">
|
|
||||||
<div class="text-sm font-medium mb-2">{{ component.props.label || 'Repeater' }}</div>
|
|
||||||
<div class="text-xs text-gray-500">{{ component.props.help || 'A repeater allows adding multiple instances of fields' }}</div>
|
|
||||||
<div class="mt-3 p-3 bg-gray-50 border border-dashed border-gray-300 rounded text-center text-xs text-gray-400">
|
|
||||||
Empty repeater - add components here in the final form
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 flex justify-end">
|
|
||||||
<button class="text-xs px-2 py-1 bg-gray-100 text-gray-500 rounded disabled:opacity-50" disabled>
|
|
||||||
Add Item
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<div v-else-if="component.type === 'heading'" class="py-2">
|
<div v-else-if="component.type === 'heading'" class="py-2">
|
||||||
@ -69,6 +53,52 @@
|
|||||||
<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 -->
|
||||||
|
<div v-else-if="component.type === 'info-display'" class="py-2">
|
||||||
|
<div
|
||||||
|
class="info-display-container rounded"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: component.props.backgroundColor || '#f9fafb',
|
||||||
|
border: component.props.showBorder ? '1px solid #e5e7eb' : 'none'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Title -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fields Display -->
|
||||||
|
<div class="p-4">
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
|
||||||
|
<dd class="text-sm text-gray-900 mt-1">{{ field.value }}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="component.props.layout === 'horizontal'"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<dd class="text-sm text-gray-900">{{ field.value }}</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<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>
|
||||||
|
<dd class="text-sm text-gray-900 mt-1">{{ field.value }}</dd>
|
||||||
|
</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" />
|
||||||
|
@ -294,35 +294,26 @@ const availableComponents = [
|
|||||||
validation: ''
|
validation: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'repeater',
|
|
||||||
name: 'Repeater',
|
|
||||||
category: 'Advanced',
|
|
||||||
icon: 'material-symbols:add-box-outline',
|
|
||||||
description: 'Repeatable group of fields',
|
|
||||||
defaultProps: {
|
|
||||||
type: 'repeater',
|
|
||||||
label: 'Repeater Section',
|
|
||||||
name: 'repeater',
|
|
||||||
help: 'Add multiple items',
|
|
||||||
max: 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'group',
|
|
||||||
name: 'Group',
|
|
||||||
category: 'Advanced',
|
|
||||||
icon: 'material-symbols:folder-outline',
|
|
||||||
description: 'Group of related fields',
|
|
||||||
defaultProps: {
|
|
||||||
type: 'group',
|
|
||||||
label: 'Field Group',
|
|
||||||
name: 'group',
|
|
||||||
help: 'Group of related fields'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
|
{
|
||||||
|
type: 'info-display',
|
||||||
|
name: 'Info Display',
|
||||||
|
category: 'Layout',
|
||||||
|
icon: 'material-symbols:info-outline',
|
||||||
|
description: 'Display read-only information in key-value format',
|
||||||
|
defaultProps: {
|
||||||
|
title: 'Information',
|
||||||
|
fields: [
|
||||||
|
{ label: 'Customer Name', value: 'John Doe', key: 'customer_name' },
|
||||||
|
{ label: 'Email', value: 'john@example.com', key: 'customer_email' },
|
||||||
|
{ label: 'Phone', value: '+1-234-567-8900', key: 'customer_phone' }
|
||||||
|
],
|
||||||
|
layout: 'vertical', // vertical, horizontal, grid
|
||||||
|
showBorder: true,
|
||||||
|
backgroundColor: '#f9fafb'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'heading',
|
type: 'heading',
|
||||||
name: 'Heading',
|
name: 'Heading',
|
||||||
|
@ -174,16 +174,105 @@
|
|||||||
help="File types, e.g. '.jpg,.png,image/*'"
|
help="File types, e.g. '.jpg,.png,image/*'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Max items for repeater -->
|
|
||||||
<FormKit
|
|
||||||
v-if="showField('max')"
|
<!-- Information Display Configuration -->
|
||||||
type="number"
|
<template v-if="component.type === 'info-display'">
|
||||||
label="Maximum Items"
|
<FormKit
|
||||||
name="max"
|
type="text"
|
||||||
v-model="configModel.max"
|
label="Title"
|
||||||
help="Maximum number of repeatable items"
|
name="title"
|
||||||
min="1"
|
v-model="configModel.title"
|
||||||
/>
|
help="Title displayed at the top"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Layout"
|
||||||
|
name="layout"
|
||||||
|
v-model="configModel.layout"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Vertical (Label above value)', value: 'vertical' },
|
||||||
|
{ label: 'Horizontal (Label: Value)', value: 'horizontal' },
|
||||||
|
{ label: 'Grid (2 columns)', value: 'grid' }
|
||||||
|
]"
|
||||||
|
help="How to display the information fields"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="checkbox"
|
||||||
|
label="Show Border"
|
||||||
|
name="showBorder"
|
||||||
|
v-model="configModel.showBorder"
|
||||||
|
help="Show border around the information display"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
label="Background Color"
|
||||||
|
name="backgroundColor"
|
||||||
|
v-model="configModel.backgroundColor"
|
||||||
|
help="Background color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Information Fields Management -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<label class="text-sm font-medium">Information Fields</label>
|
||||||
|
<button
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-700 flex items-center"
|
||||||
|
@click="addInfoField"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:add-circle-outline" class="w-3.5 h-3.5 mr-0.5" />
|
||||||
|
Add Field
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border rounded bg-gray-50 divide-y max-h-64 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="(field, index) in configModel.fields"
|
||||||
|
:key="index"
|
||||||
|
class="p-3"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-2 mb-2">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
placeholder="Label (e.g., Customer Name)"
|
||||||
|
v-model="field.label"
|
||||||
|
:classes="{ outer: 'mb-0' }"
|
||||||
|
/>
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
placeholder="Key (e.g., customer_name)"
|
||||||
|
v-model="field.key"
|
||||||
|
:classes="{ outer: 'mb-0' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
placeholder="Value (e.g., John Doe)"
|
||||||
|
v-model="field.value"
|
||||||
|
:classes="{ outer: 'mb-0 flex-1' }"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="ml-2 p-1 text-gray-400 hover:text-red-500 rounded"
|
||||||
|
@click="removeInfoField(index)"
|
||||||
|
title="Remove field"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!configModel.fields || configModel.fields.length === 0" class="p-3 text-center text-gray-500 text-xs">
|
||||||
|
No information fields added yet
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Validation Tab -->
|
<!-- Validation Tab -->
|
||||||
@ -514,12 +603,16 @@ const showField = (fieldName) => {
|
|||||||
return ['select', 'radio', 'checkbox'].includes(componentType);
|
return ['select', 'radio', 'checkbox'].includes(componentType);
|
||||||
case 'accept':
|
case 'accept':
|
||||||
return componentType === 'file';
|
return componentType === 'file';
|
||||||
case 'max':
|
|
||||||
return componentType === 'repeater';
|
|
||||||
case 'id':
|
case 'id':
|
||||||
return true; // Always show component ID in advanced tab
|
return true; // Always show component ID in advanced tab
|
||||||
case 'width':
|
case 'width':
|
||||||
return true; // Always show width in basic tab
|
return true; // Always show width in basic tab
|
||||||
|
case 'title':
|
||||||
|
return componentType === 'info-display';
|
||||||
|
case 'layout':
|
||||||
|
case 'showBorder':
|
||||||
|
case 'fields':
|
||||||
|
return componentType === 'info-display';
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -542,6 +635,24 @@ const removeOption = (index) => {
|
|||||||
configModel.value.options.splice(index, 1);
|
configModel.value.options.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add a new information field for info-display component
|
||||||
|
const addInfoField = () => {
|
||||||
|
if (!configModel.value.fields) {
|
||||||
|
configModel.value.fields = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
configModel.value.fields.push({
|
||||||
|
label: `Field ${configModel.value.fields.length + 1}`,
|
||||||
|
value: 'Value',
|
||||||
|
key: `field_${configModel.value.fields.length + 1}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove an information field from info-display component
|
||||||
|
const removeInfoField = (index) => {
|
||||||
|
configModel.value.fields.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
// Inside the <script setup> section
|
// Inside the <script setup> section
|
||||||
// Add width to fieldsToShow
|
// Add width to fieldsToShow
|
||||||
const fieldsToShow = {
|
const fieldsToShow = {
|
||||||
@ -564,13 +675,12 @@ const fieldsToShow = {
|
|||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
file: ['label', 'name', 'help', 'accept', 'width'],
|
file: ['label', 'name', 'help', 'accept', 'width'],
|
||||||
repeater: ['label', 'name', 'help', 'max', 'width'],
|
|
||||||
group: ['label', 'name', 'help', 'width'],
|
|
||||||
|
|
||||||
// Layout elements
|
// Layout elements
|
||||||
heading: ['value', 'level', 'width'],
|
heading: ['value', 'level', 'width'],
|
||||||
paragraph: ['value', 'width'],
|
paragraph: ['value', 'width'],
|
||||||
divider: ['width']
|
divider: ['width'],
|
||||||
|
'info-display': ['title', 'layout', 'showBorder', 'backgroundColor', 'fields', 'width']
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add these methods
|
// Add these methods
|
||||||
@ -585,7 +695,7 @@ const getComponentWidthPercent = () => {
|
|||||||
|
|
||||||
// Computed property to determine if validation tab should be shown
|
// Computed property to determine if validation tab should be shown
|
||||||
const showValidationTab = computed(() => {
|
const showValidationTab = computed(() => {
|
||||||
const nonValidationComponents = ['section', 'page', 'wizard', 'checkbox', 'repeater', 'group'];
|
const nonValidationComponents = ['heading', 'paragraph', 'divider', 'info-display'];
|
||||||
return !nonValidationComponents.includes(props.component.type);
|
return !nonValidationComponents.includes(props.component.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
271
components/FormScriptEngine.vue
Normal file
271
components/FormScriptEngine.vue
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Custom CSS Injection -->
|
||||||
|
<component :is="'style'" v-if="customCSS" scoped>
|
||||||
|
{{ customCSS }}
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
customScript: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
customCSS: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
formEvents: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
onLoad: true,
|
||||||
|
onFieldChange: true,
|
||||||
|
onSubmit: false,
|
||||||
|
onValidation: false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scriptMode: {
|
||||||
|
type: String,
|
||||||
|
default: 'safe' // 'safe' or 'advanced'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['field-change', 'form-submit', 'field-validate']);
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
const scriptContext = ref(null);
|
||||||
|
const fieldChangeHandlers = ref(new Map());
|
||||||
|
const isScriptInitialized = ref(false);
|
||||||
|
|
||||||
|
// Create safe execution context
|
||||||
|
const createScriptContext = () => {
|
||||||
|
const context = {
|
||||||
|
// Form field interaction methods
|
||||||
|
getField: (fieldName) => {
|
||||||
|
return props.formData[fieldName];
|
||||||
|
},
|
||||||
|
|
||||||
|
setField: (fieldName, value) => {
|
||||||
|
emit('field-change', { fieldName, value });
|
||||||
|
},
|
||||||
|
|
||||||
|
hideField: (fieldName) => {
|
||||||
|
const fieldElement = document.querySelector(`[data-name="${fieldName}"]`);
|
||||||
|
if (fieldElement) {
|
||||||
|
fieldElement.style.display = 'none';
|
||||||
|
fieldElement.setAttribute('data-hidden', 'true');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showField: (fieldName) => {
|
||||||
|
const fieldElement = document.querySelector(`[data-name="${fieldName}"]`);
|
||||||
|
if (fieldElement) {
|
||||||
|
fieldElement.style.display = '';
|
||||||
|
fieldElement.removeAttribute('data-hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disableField: (fieldName) => {
|
||||||
|
const fieldElement = document.querySelector(`[data-name="${fieldName}"] input, [data-name="${fieldName}"] select, [data-name="${fieldName}"] textarea`);
|
||||||
|
if (fieldElement) {
|
||||||
|
fieldElement.disabled = true;
|
||||||
|
fieldElement.closest('[data-name]')?.setAttribute('data-disabled', 'true');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enableField: (fieldName) => {
|
||||||
|
const fieldElement = document.querySelector(`[data-name="${fieldName}"] input, [data-name="${fieldName}"] select, [data-name="${fieldName}"] textarea`);
|
||||||
|
if (fieldElement) {
|
||||||
|
fieldElement.disabled = false;
|
||||||
|
fieldElement.closest('[data-name]')?.removeAttribute('data-disabled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validateField: (fieldName) => {
|
||||||
|
emit('field-validate', { fieldName });
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllFieldValues: () => {
|
||||||
|
return { ...props.formData };
|
||||||
|
},
|
||||||
|
|
||||||
|
onFieldChange: (fieldNames, callback) => {
|
||||||
|
const fields = Array.isArray(fieldNames) ? fieldNames : [fieldNames];
|
||||||
|
fields.forEach(fieldName => {
|
||||||
|
if (!fieldChangeHandlers.value.has(fieldName)) {
|
||||||
|
fieldChangeHandlers.value.set(fieldName, []);
|
||||||
|
}
|
||||||
|
fieldChangeHandlers.value.get(fieldName).push(callback);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
console: {
|
||||||
|
log: (...args) => console.log('[Form Script]', ...args),
|
||||||
|
warn: (...args) => console.warn('[Form Script]', ...args),
|
||||||
|
error: (...args) => console.error('[Form Script]', ...args)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Math utilities
|
||||||
|
Math: Math,
|
||||||
|
|
||||||
|
// Date utilities
|
||||||
|
Date: Date,
|
||||||
|
|
||||||
|
// Safe DOM manipulation (limited)
|
||||||
|
querySelector: (selector) => {
|
||||||
|
// Only allow querying within form container
|
||||||
|
const formContainer = document.querySelector('.form-container');
|
||||||
|
return formContainer ? formContainer.querySelector(selector) : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Array utilities
|
||||||
|
Array: Array,
|
||||||
|
|
||||||
|
// String utilities
|
||||||
|
String: String,
|
||||||
|
|
||||||
|
// Number utilities
|
||||||
|
Number: Number
|
||||||
|
};
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safe script execution
|
||||||
|
const executeScript = (script, context) => {
|
||||||
|
if (!script || !script.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (props.scriptMode === 'safe') {
|
||||||
|
// Create a safe execution environment
|
||||||
|
const allowedGlobals = Object.keys(context);
|
||||||
|
const globalProxy = new Proxy({}, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (allowedGlobals.includes(prop)) {
|
||||||
|
return context[prop];
|
||||||
|
}
|
||||||
|
throw new Error(`Access to '${prop}' is not allowed in safe mode`);
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
throw new Error('Setting global variables is not allowed in safe mode');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create function with limited scope
|
||||||
|
const scriptFunction = new Function(
|
||||||
|
...allowedGlobals,
|
||||||
|
`
|
||||||
|
"use strict";
|
||||||
|
${script}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute with only allowed context
|
||||||
|
scriptFunction.apply(globalProxy, allowedGlobals.map(key => context[key]));
|
||||||
|
} else {
|
||||||
|
// Advanced mode - more permissive but still sandboxed
|
||||||
|
const scriptFunction = new Function('context', `
|
||||||
|
"use strict";
|
||||||
|
with(context) {
|
||||||
|
${script}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
scriptFunction.call(null, context);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Script execution error:', error);
|
||||||
|
// Could emit an error event here for user feedback
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize script engine
|
||||||
|
const initializeScript = async () => {
|
||||||
|
if (!props.customScript || isScriptInitialized.value) return;
|
||||||
|
|
||||||
|
await nextTick(); // Ensure DOM is ready
|
||||||
|
|
||||||
|
scriptContext.value = createScriptContext();
|
||||||
|
|
||||||
|
if (props.formEvents.onLoad) {
|
||||||
|
executeScript(props.customScript, scriptContext.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
isScriptInitialized.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle field changes
|
||||||
|
const handleFieldChange = (fieldName, newValue, oldValue) => {
|
||||||
|
if (!fieldChangeHandlers.value.has(fieldName)) return;
|
||||||
|
|
||||||
|
const handlers = fieldChangeHandlers.value.get(fieldName);
|
||||||
|
handlers.forEach(handler => {
|
||||||
|
try {
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
handler.call(scriptContext.value, newValue, oldValue);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in field change handler for '${fieldName}':`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for form data changes
|
||||||
|
watch(() => props.formData, (newData, oldData) => {
|
||||||
|
if (!isScriptInitialized.value || !props.formEvents.onFieldChange) return;
|
||||||
|
|
||||||
|
// Compare and trigger handlers for changed fields
|
||||||
|
Object.keys(newData).forEach(fieldName => {
|
||||||
|
if (newData[fieldName] !== oldData?.[fieldName]) {
|
||||||
|
handleFieldChange(fieldName, newData[fieldName], oldData?.[fieldName]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// Watch for script changes and reinitialize
|
||||||
|
watch(() => props.customScript, () => {
|
||||||
|
if (isScriptInitialized.value) {
|
||||||
|
isScriptInitialized.value = false;
|
||||||
|
fieldChangeHandlers.value.clear();
|
||||||
|
initializeScript();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
|
onMounted(() => {
|
||||||
|
initializeScript();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
fieldChangeHandlers.value.clear();
|
||||||
|
isScriptInitialized.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose methods for parent component
|
||||||
|
defineExpose({
|
||||||
|
executeScript: (script) => {
|
||||||
|
if (scriptContext.value) {
|
||||||
|
executeScript(script, scriptContext.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getContext: () => scriptContext.value,
|
||||||
|
reinitialize: () => {
|
||||||
|
isScriptInitialized.value = false;
|
||||||
|
fieldChangeHandlers.value.clear();
|
||||||
|
initializeScript();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Component doesn't render visible content */
|
||||||
|
</style>
|
@ -42,6 +42,11 @@
|
|||||||
Preview
|
Preview
|
||||||
</RsButton>
|
</RsButton>
|
||||||
|
|
||||||
|
<RsButton @click="showFormSettings = true" variant="secondary" size="sm">
|
||||||
|
<Icon name="material-symbols:code" class="mr-1" />
|
||||||
|
Form Settings
|
||||||
|
</RsButton>
|
||||||
|
|
||||||
<RsButton @click="navigateToManage" variant="secondary" size="sm">
|
<RsButton @click="navigateToManage" variant="secondary" size="sm">
|
||||||
<Icon name="material-symbols:settings" class="mr-1" />
|
<Icon name="material-symbols:settings" class="mr-1" />
|
||||||
Manage Forms
|
Manage Forms
|
||||||
@ -197,21 +202,225 @@
|
|||||||
<!-- Preview Modal -->
|
<!-- Preview Modal -->
|
||||||
<RsModal v-model="showPreview" title="Form Preview" size="xl">
|
<RsModal v-model="showPreview" title="Form Preview" size="xl">
|
||||||
<div class="max-h-[70vh] overflow-y-auto p-4">
|
<div class="max-h-[70vh] overflow-y-auto p-4">
|
||||||
<FormKit type="form" @submit="handlePreviewSubmit" :actions="false">
|
<div class="form-container">
|
||||||
<div class="grid-preview-container">
|
<!-- Form Script Engine -->
|
||||||
<template
|
<FormScriptEngine
|
||||||
v-for="(component, index) in formStore.formComponents"
|
:form-data="previewFormData"
|
||||||
:key="index"
|
:custom-script="formStore.formCustomScript"
|
||||||
>
|
:custom-css="formStore.formCustomCSS"
|
||||||
<component-preview :component="component" :is-preview="false" />
|
:form-events="formStore.formEvents"
|
||||||
</template>
|
:script-mode="formStore.scriptMode"
|
||||||
</div>
|
@field-change="handleScriptFieldChange"
|
||||||
<FormKit type="submit" label="Submit" />
|
@field-validate="handleScriptFieldValidate"
|
||||||
</FormKit>
|
/>
|
||||||
|
|
||||||
|
<FormKit type="form" @submit="handlePreviewSubmit" :actions="false" v-model="previewFormData">
|
||||||
|
<div class="grid-preview-container">
|
||||||
|
<template
|
||||||
|
v-for="(component, index) in formStore.formComponents"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<component-preview :component="component" :is-preview="false" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<FormKit type="submit" label="Submit" class="form-submit" />
|
||||||
|
</FormKit>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer> </template>
|
<template #footer> </template>
|
||||||
</RsModal>
|
</RsModal>
|
||||||
|
|
||||||
|
<!-- Form Settings Modal -->
|
||||||
|
<RsModal v-model="showFormSettings" title="Form Settings & Scripts" size="xl">
|
||||||
|
<div class="max-h-[70vh] overflow-y-auto">
|
||||||
|
<RsTab :tabs="settingsTabs" v-model="activeSettingsTab">
|
||||||
|
<!-- Form Info Tab -->
|
||||||
|
<template #info>
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Form Name"
|
||||||
|
v-model="formStore.formName"
|
||||||
|
help="Name of your form"
|
||||||
|
validation="required"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="textarea"
|
||||||
|
label="Form Description"
|
||||||
|
v-model="formStore.formDescription"
|
||||||
|
help="Brief description of what this form is for"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript Tab -->
|
||||||
|
<template #javascript>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-lg font-medium mb-2">Custom JavaScript</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
|
Write custom JavaScript to add dynamic behavior to your form.
|
||||||
|
Use <code class="bg-gray-100 px-1 rounded">this.getField('fieldName')</code> to access form fields.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Helper Functions Reference -->
|
||||||
|
<div class="mb-4 p-3 bg-blue-50 rounded border border-blue-200">
|
||||||
|
<details>
|
||||||
|
<summary class="text-sm font-medium text-blue-800 cursor-pointer">📚 Available Helper Functions</summary>
|
||||||
|
<div class="mt-2 text-xs text-blue-700 space-y-1">
|
||||||
|
<div><code>this.getField('name')</code> - Get field value</div>
|
||||||
|
<div><code>this.setField('name', value)</code> - Set field value</div>
|
||||||
|
<div><code>this.hideField('name')</code> - Hide field</div>
|
||||||
|
<div><code>this.showField('name')</code> - Show field</div>
|
||||||
|
<div><code>this.disableField('name')</code> - Disable field</div>
|
||||||
|
<div><code>this.enableField('name')</code> - Enable field</div>
|
||||||
|
<div><code>this.validateField('name')</code> - Trigger field validation</div>
|
||||||
|
<div><code>this.getAllFieldValues()</code> - Get all form values</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RsCodeMirror
|
||||||
|
v-model="formStore.formCustomScript"
|
||||||
|
language="javascript"
|
||||||
|
:height="400"
|
||||||
|
placeholder="// Example: Hide/show fields based on selection
|
||||||
|
// this.onFieldChange('customer_type', (value) => {
|
||||||
|
// if (value === 'business') {
|
||||||
|
// this.showField('company_name');
|
||||||
|
// this.showField('tax_id');
|
||||||
|
// } else {
|
||||||
|
// this.hideField('company_name');
|
||||||
|
// this.hideField('tax_id');
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Example: Calculate total
|
||||||
|
// this.onFieldChange(['quantity', 'price'], () => {
|
||||||
|
// const qty = this.getField('quantity') || 0;
|
||||||
|
// const price = this.getField('price') || 0;
|
||||||
|
// this.setField('total', qty * price);
|
||||||
|
// });"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Custom CSS Tab -->
|
||||||
|
<template #css>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-lg font-medium mb-2">Custom CSS</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
|
Add custom styles to enhance your form appearance. Use <code class="bg-gray-100 px-1 rounded">.form-container</code> to target the form.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- CSS Helper Reference -->
|
||||||
|
<div class="mb-4 p-3 bg-green-50 rounded border border-green-200">
|
||||||
|
<details>
|
||||||
|
<summary class="text-sm font-medium text-green-800 cursor-pointer">🎨 CSS Selectors</summary>
|
||||||
|
<div class="mt-2 text-xs text-green-700 space-y-1">
|
||||||
|
<div><code>.form-container</code> - Main form wrapper</div>
|
||||||
|
<div><code>.form-field</code> - Individual form fields</div>
|
||||||
|
<div><code>.form-field[data-name="fieldName"]</code> - Specific field</div>
|
||||||
|
<div><code>.form-section</code> - Form sections</div>
|
||||||
|
<div><code>.form-submit</code> - Submit button</div>
|
||||||
|
<div><code>.info-display</code> - Info display components</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RsCodeMirror
|
||||||
|
v-model="formStore.formCustomCSS"
|
||||||
|
language="css"
|
||||||
|
:height="400"
|
||||||
|
placeholder="/* Example: Custom field styling */
|
||||||
|
/* .form-field[data-name='customer_name'] {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 2px solid #0ea5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example: Hide field initially */
|
||||||
|
/* .form-field[data-name='hidden_field'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example: Custom info display styling */
|
||||||
|
/* .info-display {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
} */"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Script Events Tab -->
|
||||||
|
<template #events>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-lg font-medium mb-2">Form Events</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
|
Configure when your custom scripts should run.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="border rounded p-4">
|
||||||
|
<h4 class="font-medium mb-2">Event Triggers</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="formStore.formEvents.onLoad" class="mr-2">
|
||||||
|
<span class="text-sm">On Form Load</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="formStore.formEvents.onFieldChange" class="mr-2">
|
||||||
|
<span class="text-sm">On Field Change</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="formStore.formEvents.onSubmit" class="mr-2">
|
||||||
|
<span class="text-sm">Before Form Submit</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="formStore.formEvents.onValidation" class="mr-2">
|
||||||
|
<span class="text-sm">On Field Validation</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border rounded p-4">
|
||||||
|
<h4 class="font-medium mb-2">Script Execution Mode</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="radio" v-model="formStore.scriptMode" value="safe" class="mr-2">
|
||||||
|
<span class="text-sm">Safe Mode (Recommended) - Limited but secure</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="radio" v-model="formStore.scriptMode" value="advanced" class="mr-2">
|
||||||
|
<span class="text-sm">Advanced Mode - Full JavaScript access</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RsTab>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<RsButton @click="showFormSettings = false" variant="tertiary">
|
||||||
|
Cancel
|
||||||
|
</RsButton>
|
||||||
|
<RsButton @click="saveFormSettings" variant="primary">
|
||||||
|
Save Settings
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RsModal>
|
||||||
|
|
||||||
<!-- Unsaved Changes Modal -->
|
<!-- Unsaved Changes Modal -->
|
||||||
<RsModal v-model="showUnsavedChangesModal" title="Unsaved Changes" size="md" position="center">
|
<RsModal v-model="showUnsavedChangesModal" title="Unsaved Changes" size="md" position="center">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@ -272,6 +481,16 @@ const pendingNavigation = ref(null);
|
|||||||
const navigationTarget = ref(null);
|
const navigationTarget = ref(null);
|
||||||
const navigationConfirmed = ref(false);
|
const navigationConfirmed = ref(false);
|
||||||
const leftSidebarTab = ref('components');
|
const leftSidebarTab = ref('components');
|
||||||
|
const showFormSettings = ref(false);
|
||||||
|
const activeSettingsTab = ref('info');
|
||||||
|
|
||||||
|
// Settings tabs configuration
|
||||||
|
const settingsTabs = [
|
||||||
|
{ key: 'info', label: 'Form Info', icon: 'material-symbols:info-outline' },
|
||||||
|
{ key: 'javascript', label: 'JavaScript', icon: 'material-symbols:code' },
|
||||||
|
{ key: 'css', label: 'CSS', icon: 'material-symbols:palette-outline' },
|
||||||
|
{ key: 'events', label: 'Events', icon: 'material-symbols:event-outline' }
|
||||||
|
];
|
||||||
|
|
||||||
// Computed property for form name with getter and setter
|
// Computed property for form name with getter and setter
|
||||||
const formName = computed({
|
const formName = computed({
|
||||||
@ -462,6 +681,47 @@ const handlePreviewSubmit = (formData) => {
|
|||||||
toast.success("Form submitted successfully");
|
toast.success("Form submitted successfully");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveFormSettings = () => {
|
||||||
|
// Form settings are automatically saved through the store's reactive updates
|
||||||
|
showFormSettings.value = false;
|
||||||
|
toast.success("Form settings saved successfully");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preview form data for script interactions
|
||||||
|
const previewFormData = ref({});
|
||||||
|
|
||||||
|
// Initialize preview form data with default values
|
||||||
|
watchEffect(() => {
|
||||||
|
const newFormData = {};
|
||||||
|
formStore.formComponents.forEach(component => {
|
||||||
|
if (component.props.name) {
|
||||||
|
// Set default values based on component type
|
||||||
|
switch (component.type) {
|
||||||
|
case 'checkbox':
|
||||||
|
newFormData[component.props.name] = [];
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
newFormData[component.props.name] = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
newFormData[component.props.name] = component.props.value || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
previewFormData.value = newFormData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle script-driven field changes
|
||||||
|
const handleScriptFieldChange = ({ fieldName, value }) => {
|
||||||
|
previewFormData.value[fieldName] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle script-driven field validation
|
||||||
|
const handleScriptFieldValidate = ({ fieldName }) => {
|
||||||
|
// Could integrate with FormKit validation here
|
||||||
|
console.log(`Validating field: ${fieldName}`);
|
||||||
|
};
|
||||||
|
|
||||||
const navigateToManage = () => {
|
const navigateToManage = () => {
|
||||||
// If already confirmed or no unsaved changes, navigate directly
|
// If already confirmed or no unsaved changes, navigate directly
|
||||||
if (navigationConfirmed.value || !formStore.hasUnsavedChanges) {
|
if (navigationConfirmed.value || !formStore.hasUnsavedChanges) {
|
||||||
|
@ -86,6 +86,10 @@ model form {
|
|||||||
formName String @db.VarChar(255)
|
formName String @db.VarChar(255)
|
||||||
formDescription String? @db.Text
|
formDescription String? @db.Text
|
||||||
formComponents Json
|
formComponents Json
|
||||||
|
customScript String? @db.LongText
|
||||||
|
customCSS String? @db.Text
|
||||||
|
formEvents Json?
|
||||||
|
scriptMode String? @default("safe") @db.VarChar(20)
|
||||||
formStatus String @default("active") @db.VarChar(50)
|
formStatus String @default("active") @db.VarChar(50)
|
||||||
formCreatedBy Int?
|
formCreatedBy Int?
|
||||||
formCreatedDate DateTime @default(now()) @db.DateTime(0)
|
formCreatedDate DateTime @default(now()) @db.DateTime(0)
|
||||||
|
@ -25,7 +25,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
formDescription: body.formDescription || null,
|
formDescription: body.formDescription || null,
|
||||||
formComponents: body.components || [],
|
formComponents: body.components || [],
|
||||||
formStatus: body.status || 'active',
|
formStatus: body.status || 'active',
|
||||||
formCreatedBy: body.createdBy || null // In a real app, this would come from the authenticated user
|
formCreatedBy: body.createdBy || null, // In a real app, this would come from the authenticated user
|
||||||
|
customScript: body.customScript || null,
|
||||||
|
customCSS: body.customCSS || null,
|
||||||
|
formEvents: body.formEvents || null,
|
||||||
|
scriptMode: body.scriptMode || 'safe'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,7 +13,18 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
actionHistory: [],
|
actionHistory: [],
|
||||||
currentHistoryIndex: -1,
|
currentHistoryIndex: -1,
|
||||||
maxHistoryLength: 30, // Maximum number of history entries to keep
|
maxHistoryLength: 30, // Maximum number of history entries to keep
|
||||||
currentFormId: null
|
currentFormId: null,
|
||||||
|
|
||||||
|
// Custom scripts and styling
|
||||||
|
formCustomScript: '',
|
||||||
|
formCustomCSS: '',
|
||||||
|
formEvents: {
|
||||||
|
onLoad: true,
|
||||||
|
onFieldChange: true,
|
||||||
|
onSubmit: false,
|
||||||
|
onValidation: false
|
||||||
|
},
|
||||||
|
scriptMode: 'safe' // 'safe' or 'advanced'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -467,7 +478,11 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
components: this.formComponents.map(c => ({
|
components: this.formComponents.map(c => ({
|
||||||
type: c.type,
|
type: c.type,
|
||||||
props: c.props
|
props: c.props
|
||||||
}))
|
})),
|
||||||
|
customScript: this.formCustomScript,
|
||||||
|
customCSS: this.formCustomCSS,
|
||||||
|
formEvents: this.formEvents,
|
||||||
|
scriptMode: this.scriptMode
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine if this is a new form or an update
|
// Determine if this is a new form or an update
|
||||||
@ -544,6 +559,17 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
this.formDescription = result.form.formDescription || '';
|
this.formDescription = result.form.formDescription || '';
|
||||||
this.currentFormId = result.form.formUUID;
|
this.currentFormId = result.form.formUUID;
|
||||||
|
|
||||||
|
// Load custom scripts and settings
|
||||||
|
this.formCustomScript = result.form.customScript || '';
|
||||||
|
this.formCustomCSS = result.form.customCSS || '';
|
||||||
|
this.formEvents = result.form.formEvents || {
|
||||||
|
onLoad: true,
|
||||||
|
onFieldChange: true,
|
||||||
|
onSubmit: false,
|
||||||
|
onValidation: false
|
||||||
|
};
|
||||||
|
this.scriptMode = result.form.scriptMode || 'safe';
|
||||||
|
|
||||||
// Transform components from DB format to store format
|
// Transform components from DB format to store format
|
||||||
if (Array.isArray(result.form.formComponents)) {
|
if (Array.isArray(result.form.formComponents)) {
|
||||||
this.formComponents = result.form.formComponents.map(c => ({
|
this.formComponents = result.form.formComponents.map(c => ({
|
||||||
@ -607,6 +633,17 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
this.currentFormId = null;
|
this.currentFormId = null;
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
|
|
||||||
|
// Reset custom scripts and settings
|
||||||
|
this.formCustomScript = '';
|
||||||
|
this.formCustomCSS = '';
|
||||||
|
this.formEvents = {
|
||||||
|
onLoad: true,
|
||||||
|
onFieldChange: true,
|
||||||
|
onSubmit: false,
|
||||||
|
onValidation: false
|
||||||
|
};
|
||||||
|
this.scriptMode = 'safe';
|
||||||
|
|
||||||
// Clear history when starting a new form and add initial state
|
// Clear history when starting a new form and add initial state
|
||||||
this.actionHistory = [];
|
this.actionHistory = [];
|
||||||
this.currentHistoryIndex = -1;
|
this.currentHistoryIndex = -1;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user