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:
Afiq 2025-05-26 16:47:53 +08:00
parent 48309d716f
commit 9ea4e18672
8 changed files with 787 additions and 80 deletions

View File

@ -1,5 +1,10 @@
<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) -->
<FormKit
v-if="isInputType"
@ -26,28 +31,7 @@
: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 -->
<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>
</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 -->
<div v-else-if="component.type === 'divider'" class="py-4">
<hr class="border-gray-200" />

View File

@ -294,35 +294,26 @@ const availableComponents = [
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
{
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',
name: 'Heading',

View File

@ -174,16 +174,105 @@
help="File types, e.g. '.jpg,.png,image/*'"
/>
<!-- Max items for repeater -->
<FormKit
v-if="showField('max')"
type="number"
label="Maximum Items"
name="max"
v-model="configModel.max"
help="Maximum number of repeatable items"
min="1"
/>
<!-- Information Display Configuration -->
<template v-if="component.type === 'info-display'">
<FormKit
type="text"
label="Title"
name="title"
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>
<!-- Validation Tab -->
@ -514,12 +603,16 @@ const showField = (fieldName) => {
return ['select', 'radio', 'checkbox'].includes(componentType);
case 'accept':
return componentType === 'file';
case 'max':
return componentType === 'repeater';
case 'id':
return true; // Always show component ID in advanced tab
case 'width':
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:
return false;
}
@ -542,6 +635,24 @@ const removeOption = (index) => {
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
// Add width to fieldsToShow
const fieldsToShow = {
@ -564,13 +675,12 @@ const fieldsToShow = {
// Advanced
file: ['label', 'name', 'help', 'accept', 'width'],
repeater: ['label', 'name', 'help', 'max', 'width'],
group: ['label', 'name', 'help', 'width'],
// Layout elements
heading: ['value', 'level', 'width'],
paragraph: ['value', 'width'],
divider: ['width']
divider: ['width'],
'info-display': ['title', 'layout', 'showBorder', 'backgroundColor', 'fields', 'width']
};
// Add these methods
@ -585,7 +695,7 @@ const getComponentWidthPercent = () => {
// Computed property to determine if validation tab should be shown
const showValidationTab = computed(() => {
const nonValidationComponents = ['section', 'page', 'wizard', 'checkbox', 'repeater', 'group'];
const nonValidationComponents = ['heading', 'paragraph', 'divider', 'info-display'];
return !nonValidationComponents.includes(props.component.type);
});

View 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>

View File

@ -42,6 +42,11 @@
Preview
</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">
<Icon name="material-symbols:settings" class="mr-1" />
Manage Forms
@ -197,21 +202,225 @@
<!-- Preview Modal -->
<RsModal v-model="showPreview" title="Form Preview" size="xl">
<div class="max-h-[70vh] overflow-y-auto p-4">
<FormKit type="form" @submit="handlePreviewSubmit" :actions="false">
<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" />
</FormKit>
<div class="form-container">
<!-- Form Script Engine -->
<FormScriptEngine
:form-data="previewFormData"
:custom-script="formStore.formCustomScript"
:custom-css="formStore.formCustomCSS"
:form-events="formStore.formEvents"
:script-mode="formStore.scriptMode"
@field-change="handleScriptFieldChange"
@field-validate="handleScriptFieldValidate"
/>
<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>
<template #footer> </template>
</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 -->
<RsModal v-model="showUnsavedChangesModal" title="Unsaved Changes" size="md" position="center">
<div class="p-4">
@ -272,6 +481,16 @@ const pendingNavigation = ref(null);
const navigationTarget = ref(null);
const navigationConfirmed = ref(false);
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
const formName = computed({
@ -462,6 +681,47 @@ const handlePreviewSubmit = (formData) => {
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 = () => {
// If already confirmed or no unsaved changes, navigate directly
if (navigationConfirmed.value || !formStore.hasUnsavedChanges) {

View File

@ -86,6 +86,10 @@ model form {
formName String @db.VarChar(255)
formDescription String? @db.Text
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)
formCreatedBy Int?
formCreatedDate DateTime @default(now()) @db.DateTime(0)

View File

@ -25,7 +25,11 @@ export default defineEventHandler(async (event) => {
formDescription: body.formDescription || null,
formComponents: body.components || [],
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'
}
});

View File

@ -13,7 +13,18 @@ export const useFormBuilderStore = defineStore('formBuilder', {
actionHistory: [],
currentHistoryIndex: -1,
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: {
@ -467,7 +478,11 @@ export const useFormBuilderStore = defineStore('formBuilder', {
components: this.formComponents.map(c => ({
type: c.type,
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
@ -544,6 +559,17 @@ export const useFormBuilderStore = defineStore('formBuilder', {
this.formDescription = result.form.formDescription || '';
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
if (Array.isArray(result.form.formComponents)) {
this.formComponents = result.form.formComponents.map(c => ({
@ -607,6 +633,17 @@ export const useFormBuilderStore = defineStore('formBuilder', {
this.currentFormId = null;
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
this.actionHistory = [];
this.currentHistoryIndex = -1;