Add Repeating Table Component and Enhance Form Builder Functionality
- Introduced a new RepeatingTable component for structured data collection, allowing users to add, edit, and delete records dynamically. - Enhanced FormBuilderComponents to include the RepeatingTable in the available components list with default properties. - Updated FormBuilderFieldSettingsModal to support configuration options for the RepeatingTable, including button texts, record limits, and display settings. - Implemented data management functions for updating table data and handling dynamic list items in the FormBuilder. - Improved ComponentPreview to render the RepeatingTable component correctly in preview mode. - Enhanced user experience with intuitive UI elements and responsive design for the new table component. - Updated documentation to reflect the addition of the RepeatingTable and its configuration options.
This commit is contained in:
parent
33dc901107
commit
cf3a2c1a58
@ -35,8 +35,6 @@
|
||||
options: 'space-y-0.5'
|
||||
} : {}" :class="{ 'canvas-component': isPreview }" />
|
||||
|
||||
|
||||
|
||||
<!-- Heading -->
|
||||
<div v-else-if="component.type === 'heading'" class="py-2">
|
||||
<component :is="`h${component.props.level || 2}`" class="font-semibold" :class="{
|
||||
@ -314,6 +312,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repeating Table Component -->
|
||||
<div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper">
|
||||
<RepeatingTable
|
||||
:config="component.props"
|
||||
:model-value="previewFormData[component.props.name] || []"
|
||||
:is-preview="isPreview"
|
||||
@update:model-value="updateTableData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Button Component -->
|
||||
<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">
|
||||
@ -331,8 +339,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Unknown Component Type Fallback -->
|
||||
<div v-else class="p-4 border border-dashed border-gray-300 rounded">
|
||||
<div class="text-gray-500">Unknown component type: {{ component.type }}</div>
|
||||
@ -838,6 +844,21 @@ const componentStyle = computed(() => {
|
||||
boxSizing: 'border-box'
|
||||
};
|
||||
});
|
||||
|
||||
// Update dynamic list items
|
||||
const updateListItems = (listName, newItems) => {
|
||||
const updatedData = { ...formStore.previewFormData, [listName]: newItems };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
};
|
||||
|
||||
// Update table data for repeating-table component
|
||||
const updateTableData = (newData) => {
|
||||
const tableName = props.component.props.name;
|
||||
if (!tableName) return;
|
||||
|
||||
const updatedFormData = { ...formStore.previewFormData, [tableName]: newData };
|
||||
formStore.updatePreviewFormData(updatedFormData);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -907,4 +928,8 @@ const componentStyle = computed(() => {
|
||||
.list-items {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.repeating-table-wrapper{
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
</style>
|
@ -504,6 +504,55 @@ const availableComponents = [
|
||||
importEnabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'repeating-table',
|
||||
name: 'Data Table',
|
||||
category: 'Advanced',
|
||||
icon: 'material-symbols:table',
|
||||
description: 'Structured table for collecting multiple records',
|
||||
defaultProps: {
|
||||
label: 'Data Records',
|
||||
name: 'data_table',
|
||||
help: 'Add records using the form and view them in a table',
|
||||
buttonText: 'Add Record',
|
||||
editText: 'Edit',
|
||||
deleteText: 'Delete',
|
||||
minRecords: 0,
|
||||
maxRecords: 50,
|
||||
showRowNumbers: true,
|
||||
allowEdit: true,
|
||||
allowDelete: true,
|
||||
confirmDelete: true,
|
||||
enableSearch: false,
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter name',
|
||||
validation: 'required'
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
placeholder: 'Enter email',
|
||||
validation: 'required|email'
|
||||
},
|
||||
{
|
||||
name: 'phone',
|
||||
label: 'Phone',
|
||||
type: 'tel',
|
||||
required: false,
|
||||
placeholder: 'Enter phone',
|
||||
validation: ''
|
||||
}
|
||||
],
|
||||
defaultData: []
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'otp',
|
||||
name: 'OTP Input',
|
||||
|
@ -233,18 +233,18 @@
|
||||
<div class="mask-examples">
|
||||
<p class="text-sm font-medium text-gray-700 mb-2">Common patterns:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<button @click="configModel.mask = '###-###-####'" class="example-button">
|
||||
<RsButton @click="configModel.mask = '###-###-####'" variant="secondary-outline" size="sm">
|
||||
Phone: ###-###-####
|
||||
</button>
|
||||
<button @click="configModel.mask = '##/##/####'" class="example-button">
|
||||
</RsButton>
|
||||
<RsButton @click="configModel.mask = '##/##/####'" variant="secondary-outline" size="sm">
|
||||
Date: ##/##/####
|
||||
</button>
|
||||
<button @click="configModel.mask = 'AA-####'" class="example-button">
|
||||
</RsButton>
|
||||
<RsButton @click="configModel.mask = 'AA-####'" variant="secondary-outline" size="sm">
|
||||
Code: AA-####
|
||||
</button>
|
||||
<button @click="configModel.mask = '#### #### #### ####'" class="example-button">
|
||||
</RsButton>
|
||||
<RsButton @click="configModel.mask = '#### #### #### ####'" variant="secondary-outline" size="sm">
|
||||
Credit Card: #### #### #### ####
|
||||
</button>
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -608,6 +608,252 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Repeating Table Configuration -->
|
||||
<template v-if="component.type === 'repeating-table'">
|
||||
<div class="space-y-6">
|
||||
<!-- Basic Table 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 record button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="Add Record"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Edit Button Text"
|
||||
name="editText"
|
||||
v-model="configModel.editText"
|
||||
help="Text for the edit record button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="Edit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Record Limits -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Minimum Records"
|
||||
name="minRecords"
|
||||
v-model="configModel.minRecords"
|
||||
help="Minimum number of records required"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
:min="0"
|
||||
placeholder="0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Maximum Records"
|
||||
name="maxRecords"
|
||||
v-model="configModel.maxRecords"
|
||||
help="Maximum number of records allowed"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
:min="1"
|
||||
placeholder="50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Display Settings -->
|
||||
<div class="space-y-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Display Settings</h5>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Show Row Numbers"
|
||||
name="showRowNumbers"
|
||||
v-model="configModel.showRowNumbers"
|
||||
help="Display row numbers in the table"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Enable Search"
|
||||
name="enableSearch"
|
||||
v-model="configModel.enableSearch"
|
||||
help="Add search functionality to the table"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Allow Edit"
|
||||
name="allowEdit"
|
||||
v-model="configModel.allowEdit"
|
||||
help="Allow users to edit existing records"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Allow Delete"
|
||||
name="allowDelete"
|
||||
v-model="configModel.allowDelete"
|
||||
help="Allow users to delete records"
|
||||
: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' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Columns Configuration -->
|
||||
<div class="space-y-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Table Columns</h5>
|
||||
|
||||
<div v-if="configModel.columns && configModel.columns.length > 0" class="space-y-4">
|
||||
<div
|
||||
v-for="(column, index) in configModel.columns"
|
||||
:key="index"
|
||||
class="border rounded-lg p-4 bg-gray-50"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h6 class="text-sm font-medium text-gray-800">Column {{ index + 1 }}</h6>
|
||||
<button
|
||||
@click="removeTableColumn(index)"
|
||||
class="text-red-500 hover:text-red-700 text-sm"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Column Name"
|
||||
v-model="column.name"
|
||||
placeholder="field_name"
|
||||
help="Internal field name (no spaces)"
|
||||
:classes="{ outer: 'field-wrapper mb-0' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Display Label"
|
||||
v-model="column.label"
|
||||
placeholder="Display Name"
|
||||
help="What users will see"
|
||||
:classes="{ outer: 'field-wrapper mb-0' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Field Type"
|
||||
v-model="column.type"
|
||||
:options="[
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Phone', value: 'tel' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Dropdown', value: 'select' },
|
||||
{ label: 'Text Area', value: 'textarea' }
|
||||
]"
|
||||
:classes="{ outer: 'field-wrapper mb-0' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Placeholder"
|
||||
v-model="column.placeholder"
|
||||
placeholder="Enter value..."
|
||||
help="Hint text for the input"
|
||||
:classes="{ outer: 'field-wrapper mb-0' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-2 gap-3">
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Required Field"
|
||||
v-model="column.required"
|
||||
help="Make this field mandatory"
|
||||
:classes="{ outer: 'field-wrapper mb-0' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Validation Rules"
|
||||
v-model="column.validation"
|
||||
placeholder="required|email"
|
||||
help="Validation rules (e.g., required, email)"
|
||||
:classes="{ outer: 'field-wrapper mb-0' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="column.type === 'select'" class="mt-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Options</label>
|
||||
<div v-if="column.options && column.options.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="(option, optionIndex) in column.options"
|
||||
:key="optionIndex"
|
||||
class="flex items-center space-x-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="option.label"
|
||||
placeholder="Option label"
|
||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="option.value"
|
||||
placeholder="Option value"
|
||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
@click="removeColumnOption(index, optionIndex)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="addColumnOption(index)"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 flex items-center mt-2"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
|
||||
Add Option
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-8 text-gray-500 border-2 border-dashed border-gray-300 rounded-lg">
|
||||
<Icon name="heroicons:table-cells" class="w-8 h-8 mx-auto mb-2 text-gray-400" />
|
||||
<p class="text-sm mb-3">No columns defined yet</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="addTableColumn"
|
||||
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 Column
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Content for Text Components -->
|
||||
<template v-if="showField('value')">
|
||||
<FormKit
|
||||
@ -1099,29 +1345,29 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
<RsButton
|
||||
@click="handleClose"
|
||||
class="cancel-btn"
|
||||
variant="secondary-outline"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</RsButton>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
<RsButton
|
||||
@click="handleReset"
|
||||
class="reset-btn"
|
||||
variant="warning-outline"
|
||||
>
|
||||
<Icon name="heroicons:arrow-path" class="w-4 h-4 mr-1" />
|
||||
Reset to Default
|
||||
</button>
|
||||
</RsButton>
|
||||
|
||||
<button
|
||||
<RsButton
|
||||
@click="handleSave"
|
||||
class="save-btn"
|
||||
variant="primary"
|
||||
>
|
||||
<Icon name="heroicons:check" class="w-4 h-4 mr-1" />
|
||||
Apply Changes
|
||||
</button>
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -1145,7 +1391,14 @@ const isOpen = computed({
|
||||
})
|
||||
|
||||
const activeTab = ref('basic')
|
||||
const configModel = ref({})
|
||||
const configModel = ref({
|
||||
conditionalLogic: {
|
||||
enabled: false,
|
||||
conditions: [],
|
||||
action: 'show',
|
||||
operator: 'and'
|
||||
}
|
||||
})
|
||||
|
||||
// Component info helpers
|
||||
const modalTitle = computed(() => {
|
||||
@ -1181,7 +1434,8 @@ const getComponentIcon = (type) => {
|
||||
paragraph: 'heroicons:document-text',
|
||||
divider: 'heroicons:minus',
|
||||
'info-display': 'heroicons:information-circle',
|
||||
'dynamic-list': 'heroicons:list-bullet'
|
||||
'dynamic-list': 'heroicons:list-bullet',
|
||||
'repeating-table': 'heroicons:table-cells'
|
||||
}
|
||||
return icons[type] || 'heroicons:square-3-stack-3d'
|
||||
}
|
||||
@ -1214,7 +1468,8 @@ const getComponentTypeName = (type) => {
|
||||
paragraph: 'Paragraph Text',
|
||||
divider: 'Divider Line',
|
||||
'info-display': 'Information Display',
|
||||
'dynamic-list': 'Dynamic List'
|
||||
'dynamic-list': 'Dynamic List',
|
||||
'repeating-table': 'Data Table'
|
||||
}
|
||||
return names[type] || 'Form Field'
|
||||
}
|
||||
@ -1247,7 +1502,8 @@ const getComponentDescription = (type) => {
|
||||
paragraph: 'Text content for instructions and descriptions',
|
||||
divider: 'Visual separator to organize form sections',
|
||||
'info-display': 'Read-only information display in organized format',
|
||||
'dynamic-list': 'Dynamic list for displaying and managing items'
|
||||
'dynamic-list': 'Dynamic list for displaying and managing items',
|
||||
'repeating-table': 'Structured table for collecting multiple records with forms'
|
||||
}
|
||||
return descriptions[type] || 'Configure this form field'
|
||||
}
|
||||
@ -1272,14 +1528,14 @@ const showField = (fieldName) => {
|
||||
if (!props.component) return false
|
||||
|
||||
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', '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', 'dynamic-list'],
|
||||
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', 'repeating-table'],
|
||||
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', 'repeating-table'],
|
||||
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', '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', 'dynamic-list', 'repeating-table'],
|
||||
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', 'dynamic-list'],
|
||||
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', 'repeating-table'],
|
||||
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']
|
||||
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'dynamic-list', 'repeating-table']
|
||||
}
|
||||
|
||||
return fieldConfig[fieldName]?.includes(props.component.type) || false
|
||||
@ -1288,7 +1544,7 @@ const showField = (fieldName) => {
|
||||
const hasOptions = computed(() => showField('options'))
|
||||
const hasSpecificSettings = computed(() => {
|
||||
if (!props.component) return false
|
||||
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'paragraph', 'button', 'dynamic-list']
|
||||
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'paragraph', 'button', 'dynamic-list', 'repeating-table']
|
||||
return specificTypes.includes(props.component.type)
|
||||
})
|
||||
|
||||
@ -1408,7 +1664,8 @@ const getRecommendedWidth = (fieldType) => {
|
||||
'switch': 'full',
|
||||
'button': 'full',
|
||||
'info-display': 'full',
|
||||
'dynamic-list': 'full'
|
||||
'dynamic-list': 'full',
|
||||
'repeating-table': 'full'
|
||||
}
|
||||
|
||||
return recommendations[fieldType] || 'full'
|
||||
@ -1432,7 +1689,16 @@ const getCurrentGridColumns = () => {
|
||||
// Watch for component changes
|
||||
watch(() => props.component, (newComponent) => {
|
||||
if (newComponent) {
|
||||
configModel.value = { ...newComponent.props }
|
||||
configModel.value = {
|
||||
...newComponent.props,
|
||||
// Ensure conditionalLogic is properly initialized
|
||||
conditionalLogic: newComponent.props.conditionalLogic || {
|
||||
enabled: false,
|
||||
conditions: [],
|
||||
action: 'show',
|
||||
operator: 'and'
|
||||
}
|
||||
}
|
||||
activeTab.value = 'basic'
|
||||
resetValidationState()
|
||||
}
|
||||
@ -1480,7 +1746,9 @@ const addOption = () => {
|
||||
}
|
||||
|
||||
const removeOption = (index) => {
|
||||
configModel.value.options.splice(index, 1)
|
||||
if (configModel.value.options && configModel.value.options.length > index) {
|
||||
configModel.value.options.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Default items management for dynamic-list
|
||||
@ -1597,7 +1865,16 @@ const handleReset = () => {
|
||||
...props.component,
|
||||
props: props.component.defaultProps || {}
|
||||
}
|
||||
configModel.value = { ...defaultComponent.props }
|
||||
configModel.value = {
|
||||
...defaultComponent.props,
|
||||
// Ensure conditionalLogic is properly initialized
|
||||
conditionalLogic: defaultComponent.props.conditionalLogic || {
|
||||
enabled: false,
|
||||
conditions: [],
|
||||
action: 'show',
|
||||
operator: 'and'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1605,6 +1882,45 @@ const handleClose = () => {
|
||||
isOpen.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// Table column management for repeating-table
|
||||
const addTableColumn = () => {
|
||||
if (!configModel.value.columns) {
|
||||
configModel.value.columns = []
|
||||
}
|
||||
configModel.value.columns.push({
|
||||
name: `column_${configModel.value.columns.length + 1}`,
|
||||
label: `Column ${configModel.value.columns.length + 1}`,
|
||||
type: 'text',
|
||||
required: false,
|
||||
placeholder: 'Enter value...',
|
||||
validation: '',
|
||||
width: '200px',
|
||||
options: []
|
||||
})
|
||||
}
|
||||
|
||||
const removeTableColumn = (index) => {
|
||||
if (configModel.value.columns) {
|
||||
configModel.value.columns.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addColumnOption = (columnIndex) => {
|
||||
if (!configModel.value.columns[columnIndex].options) {
|
||||
configModel.value.columns[columnIndex].options = []
|
||||
}
|
||||
configModel.value.columns[columnIndex].options.push({
|
||||
label: `Option ${configModel.value.columns[columnIndex].options.length + 1}`,
|
||||
value: `option_${configModel.value.columns[columnIndex].options.length + 1}`
|
||||
})
|
||||
}
|
||||
|
||||
const removeColumnOption = (columnIndex, optionIndex) => {
|
||||
if (configModel.value.columns[columnIndex].options) {
|
||||
configModel.value.columns[columnIndex].options.splice(optionIndex, 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
1015
components/RepeatingTable.vue
Normal file
1015
components/RepeatingTable.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -430,6 +430,145 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Assignment Configuration (only if form is selected) -->
|
||||
<div v-if="localNodeData.formId" class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-6 h-6 rounded-full bg-emerald-100 flex items-center justify-center mr-2">
|
||||
<span class="text-xs font-semibold text-emerald-600">4</span>
|
||||
</div>
|
||||
<h4 class="font-medium">Task Assignment</h4>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
Configure who can complete this form task. Choose from specific users, roles, or dynamic assignment using variables.
|
||||
</p>
|
||||
|
||||
<div class="mb-5">
|
||||
<!-- Assignment Type Selection -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Assignment Type</label>
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="localNodeData.assignmentType"
|
||||
:options="[
|
||||
{ label: 'Anyone (Public)', value: 'public' },
|
||||
{ label: 'Specific Users', value: 'users' },
|
||||
{ label: 'Role-based', value: 'roles' },
|
||||
{ label: 'Dynamic (from Variable)', value: 'variable' }
|
||||
]"
|
||||
placeholder="Select assignment type"
|
||||
@input="handleAssignmentTypeChange"
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">How should this task be assigned</p>
|
||||
</div>
|
||||
|
||||
<!-- Specific Users Assignment -->
|
||||
<div v-if="localNodeData.assignmentType === 'users'" class="bg-blue-50 p-4 rounded-md border border-blue-200">
|
||||
<div class="flex items-center mb-3">
|
||||
<Icon name="material-symbols:person" class="text-blue-600 mr-2" />
|
||||
<h5 class="text-sm font-medium text-blue-900">Assign to Specific Users</h5>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="localNodeData.assignedUsers"
|
||||
:options="availableUsers"
|
||||
placeholder="Select users..."
|
||||
multiple
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
<p class="text-xs text-blue-700">Selected users will be able to complete this form task</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role-based Assignment -->
|
||||
<div v-if="localNodeData.assignmentType === 'roles'" class="bg-purple-50 p-4 rounded-md border border-purple-200">
|
||||
<div class="flex items-center mb-3">
|
||||
<Icon name="material-symbols:group" class="text-purple-600 mr-2" />
|
||||
<h5 class="text-sm font-medium text-purple-900">Assign to Roles</h5>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="localNodeData.assignedRoles"
|
||||
:options="availableRoles"
|
||||
placeholder="Select roles..."
|
||||
multiple
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
<p class="text-xs text-purple-700">Any user with the selected roles will be able to complete this form task</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variable-based Assignment -->
|
||||
<div v-if="localNodeData.assignmentType === 'variable'" class="bg-green-50 p-4 rounded-md border border-green-200">
|
||||
<div class="flex items-center mb-3">
|
||||
<Icon name="material-symbols:code" class="text-green-600 mr-2" />
|
||||
<h5 class="text-sm font-medium text-green-900">Dynamic Assignment from Variable</h5>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Assignment Variable</label>
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="localNodeData.assignmentVariable"
|
||||
:options="processVariableOptions"
|
||||
placeholder="Select variable containing assignment data"
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-green-700">Variable should contain user ID, username, role name, or email address</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Variable Content Type</label>
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="localNodeData.assignmentVariableType"
|
||||
:options="[
|
||||
{ label: 'User ID', value: 'user_id' },
|
||||
{ label: 'Username', value: 'username' },
|
||||
{ label: 'Email Address', value: 'email' },
|
||||
{ label: 'Role Name', value: 'role_name' }
|
||||
]"
|
||||
placeholder="What type of data does the variable contain"
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-green-700">How to interpret the variable value for assignment</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Assignment Info -->
|
||||
<div v-if="localNodeData.assignmentType === 'public'" class="bg-gray-50 p-4 rounded-md border border-gray-200">
|
||||
<div class="flex items-center mb-2">
|
||||
<Icon name="material-symbols:public" class="text-gray-600 mr-2" />
|
||||
<h5 class="text-sm font-medium text-gray-900">Public Task</h5>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600">Anyone with access to the process will be able to complete this form task</p>
|
||||
</div>
|
||||
|
||||
<!-- Assignment Summary -->
|
||||
<div v-if="localNodeData.assignmentType && localNodeData.assignmentType !== 'public'" class="mt-4 p-3 bg-white border border-gray-200 rounded-md">
|
||||
<h6 class="text-sm font-medium text-gray-900 mb-2">Assignment Summary</h6>
|
||||
<div class="text-xs text-gray-600">
|
||||
<div v-if="localNodeData.assignmentType === 'users' && localNodeData.assignedUsers?.length">
|
||||
Task assigned to {{ localNodeData.assignedUsers.length }} user(s)
|
||||
</div>
|
||||
<div v-if="localNodeData.assignmentType === 'roles' && localNodeData.assignedRoles?.length">
|
||||
Task assigned to {{ localNodeData.assignedRoles.length }} role(s)
|
||||
</div>
|
||||
<div v-if="localNodeData.assignmentType === 'variable' && localNodeData.assignmentVariable">
|
||||
Task dynamically assigned using variable: {{ localNodeData.assignmentVariable }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -447,6 +586,14 @@ const props = defineProps({
|
||||
availableVariables: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
availableUsers: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
availableRoles: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
@ -468,7 +615,12 @@ const localNodeData = ref({
|
||||
formUuid: null,
|
||||
inputMappings: [],
|
||||
outputMappings: [],
|
||||
fieldConditions: []
|
||||
fieldConditions: [],
|
||||
assignmentType: 'public',
|
||||
assignedUsers: [],
|
||||
assignedRoles: [],
|
||||
assignmentVariable: '',
|
||||
assignmentVariableType: 'user_id'
|
||||
});
|
||||
|
||||
// Watch for changes from parent props
|
||||
@ -489,7 +641,12 @@ watch(() => props.nodeData, async (newNodeData) => {
|
||||
: [],
|
||||
fieldConditions: Array.isArray(newNodeData.fieldConditions)
|
||||
? newNodeData.fieldConditions.map(condition => ({ ...condition }))
|
||||
: []
|
||||
: [],
|
||||
assignmentType: newNodeData.assignmentType || 'public',
|
||||
assignedUsers: Array.isArray(newNodeData.assignedUsers) ? newNodeData.assignedUsers.map(user => ({ ...user })) : [],
|
||||
assignedRoles: Array.isArray(newNodeData.assignedRoles) ? newNodeData.assignedRoles.map(role => ({ ...role })) : [],
|
||||
assignmentVariable: newNodeData.assignmentVariable || '',
|
||||
assignmentVariableType: newNodeData.assignmentVariableType || 'user_id'
|
||||
};
|
||||
|
||||
// Load form fields if form is already selected
|
||||
@ -510,7 +667,12 @@ async function handleFormSelection(form) {
|
||||
formUuid: form.formUUID,
|
||||
label: form.formName || 'Form Task',
|
||||
description: `Form: ${form.formName}`,
|
||||
fieldConditions: []
|
||||
fieldConditions: [],
|
||||
assignmentType: 'public',
|
||||
assignedUsers: [],
|
||||
assignedRoles: [],
|
||||
assignmentVariable: '',
|
||||
assignmentVariableType: 'user_id'
|
||||
};
|
||||
|
||||
// Load form fields for this form
|
||||
@ -530,7 +692,12 @@ function clearFormSelection() {
|
||||
description: 'Form submission task',
|
||||
inputMappings: [],
|
||||
outputMappings: [],
|
||||
fieldConditions: []
|
||||
fieldConditions: [],
|
||||
assignmentType: 'public',
|
||||
assignedUsers: [],
|
||||
assignedRoles: [],
|
||||
assignmentVariable: '',
|
||||
assignmentVariableType: 'user_id'
|
||||
};
|
||||
|
||||
// Clear form fields
|
||||
@ -688,6 +855,55 @@ const processVariableOptions = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
// Reactive state for users and roles
|
||||
const users = ref([]);
|
||||
const roles = ref([]);
|
||||
|
||||
// Computed property for available users (for FormKit select)
|
||||
const availableUsers = computed(() => {
|
||||
return users.value.map(user => ({
|
||||
label: user.userFullName ? `${user.userFullName} (${user.userUsername})` : user.userUsername,
|
||||
value: user.userID,
|
||||
username: user.userUsername,
|
||||
email: user.userEmail
|
||||
}));
|
||||
});
|
||||
|
||||
// Computed property for available roles (for FormKit select)
|
||||
const availableRoles = computed(() => {
|
||||
return roles.value.map(role => ({
|
||||
label: role.roleName,
|
||||
value: role.roleID,
|
||||
description: role.roleDescription
|
||||
}));
|
||||
});
|
||||
|
||||
// Fetch users and roles data
|
||||
async function fetchUsersAndRoles() {
|
||||
try {
|
||||
// Fetch users
|
||||
const usersResponse = await fetch('/api/users');
|
||||
const usersResult = await usersResponse.json();
|
||||
if (usersResult.success && usersResult.users) {
|
||||
users.value = usersResult.users;
|
||||
}
|
||||
|
||||
// Fetch roles
|
||||
const rolesResponse = await fetch('/api/roles');
|
||||
const rolesResult = await rolesResponse.json();
|
||||
if (rolesResult.success && rolesResult.roles) {
|
||||
roles.value = rolesResult.roles;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching users and roles:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch data when component mounts
|
||||
nextTick(() => {
|
||||
fetchUsersAndRoles();
|
||||
});
|
||||
|
||||
// Save changes by emitting them to parent
|
||||
function saveChanges() {
|
||||
// Create a clean copy of the data to avoid reactivity issues
|
||||
@ -698,7 +914,12 @@ function saveChanges() {
|
||||
outputMappings: localNodeData.value.outputMappings ?
|
||||
localNodeData.value.outputMappings.map(mapping => ({ ...mapping })) : [],
|
||||
fieldConditions: localNodeData.value.fieldConditions ?
|
||||
localNodeData.value.fieldConditions.map(condition => ({ ...condition })) : []
|
||||
localNodeData.value.fieldConditions.map(condition => ({ ...condition })) : [],
|
||||
assignmentType: localNodeData.value.assignmentType || 'public',
|
||||
assignedUsers: localNodeData.value.assignedUsers ? localNodeData.value.assignedUsers.map(user => ({ ...user })) : [],
|
||||
assignedRoles: localNodeData.value.assignedRoles ? localNodeData.value.assignedRoles.map(role => ({ ...role })) : [],
|
||||
assignmentVariable: localNodeData.value.assignmentVariable || '',
|
||||
assignmentVariableType: localNodeData.value.assignmentVariableType || 'user_id'
|
||||
};
|
||||
|
||||
// Emit the updated data to parent
|
||||
@ -752,6 +973,12 @@ function getStringValue(value) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle assignment type change
|
||||
function handleAssignmentTypeChange() {
|
||||
// Implementation of handleAssignmentTypeChange function
|
||||
saveChanges();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -382,7 +382,8 @@ const editVariable = (variable) => {
|
||||
|
||||
const deleteVariable = (variable) => {
|
||||
if (confirm(`Are you sure you want to delete the variable "${variable.name}"? This might affect parts of your process that use this variable.`)) {
|
||||
variableStore.deleteVariable(variable.name, 'global');
|
||||
const scope = variable.scope || 'global';
|
||||
variableStore.deleteVariable(variable.name, scope);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
|
||||
// Get all active roles
|
||||
const roles = await prisma.role.findMany({
|
||||
where: {
|
||||
roleStatus: 'active' // Assuming there's a status field to filter active roles
|
||||
roleStatus: 'ACTIVE' // Using ACTIVE to match database convention
|
||||
},
|
||||
select: {
|
||||
roleID: true,
|
||||
|
@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
|
||||
// Get all active users
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
userStatus: 'active' // Assuming there's a status field to filter active users
|
||||
userStatus: 'ACTIVE' // Using ACTIVE to match database convention
|
||||
},
|
||||
select: {
|
||||
userID: true,
|
||||
|
@ -242,6 +242,26 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
defaultProps.defaultItems = Array.isArray(defaultProps.defaultItems) ? defaultProps.defaultItems : ['Item 1', 'Item 2'];
|
||||
break;
|
||||
|
||||
case 'repeating-table':
|
||||
// Ensure all required repeating table properties
|
||||
defaultProps.buttonText = defaultProps.buttonText || 'Add Record';
|
||||
defaultProps.editText = defaultProps.editText || 'Edit';
|
||||
defaultProps.deleteText = defaultProps.deleteText || 'Delete';
|
||||
defaultProps.minRecords = defaultProps.minRecords !== undefined ? defaultProps.minRecords : 0;
|
||||
defaultProps.maxRecords = defaultProps.maxRecords !== undefined ? defaultProps.maxRecords : 50;
|
||||
defaultProps.showRowNumbers = defaultProps.showRowNumbers !== undefined ? defaultProps.showRowNumbers : true;
|
||||
defaultProps.allowEdit = defaultProps.allowEdit !== undefined ? defaultProps.allowEdit : true;
|
||||
defaultProps.allowDelete = defaultProps.allowDelete !== undefined ? defaultProps.allowDelete : true;
|
||||
defaultProps.confirmDelete = defaultProps.confirmDelete !== undefined ? defaultProps.confirmDelete : true;
|
||||
defaultProps.enableSearch = defaultProps.enableSearch !== undefined ? defaultProps.enableSearch : false;
|
||||
defaultProps.enableExport = defaultProps.enableExport !== undefined ? defaultProps.enableExport : false;
|
||||
defaultProps.columns = Array.isArray(defaultProps.columns) ? defaultProps.columns : [
|
||||
{ name: 'name', label: 'Name', type: 'text', required: true, placeholder: 'Enter name', validation: 'required', width: '200px' },
|
||||
{ name: 'email', label: 'Email', type: 'email', required: true, placeholder: 'Enter email', validation: 'required|email', width: '250px' }
|
||||
];
|
||||
defaultProps.defaultData = Array.isArray(defaultProps.defaultData) ? defaultProps.defaultData : [];
|
||||
break;
|
||||
|
||||
case 'info-display':
|
||||
// Ensure all required info display properties
|
||||
defaultProps.title = defaultProps.title || 'Information';
|
||||
|
Loading…
x
Reference in New Issue
Block a user