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:
Afiq 2025-05-31 10:45:56 +08:00
parent 33dc901107
commit cf3a2c1a58
9 changed files with 1696 additions and 43 deletions

View File

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

View File

@ -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',

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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);
}
};

View File

@ -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,

View File

@ -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,

View File

@ -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';