From c43d9b6849849a9a7c0fd76e8ed4b7b0b23b7503 Mon Sep 17 00:00:00 2001 From: Md Afiq Iskandar Date: Tue, 5 Aug 2025 17:14:04 +0800 Subject: [PATCH] Enhance Form Builder and Management Features - Updated the form builder to allow users to create new forms with additional fields for category and group, improving organization and usability. - Introduced an empty state in the form builder to guide users in creating new forms. - Enhanced the management page with new filters for category and group, allowing for better form organization and retrieval. - Updated the database schema to include new fields for form category, tags, and group, along with corresponding API adjustments for form creation and updates. - Improved the user interface with better handling of form descriptions and added visual indicators for categories and groups in the forms table. --- pages/form-builder/index.vue | 165 ++++++++++++++- pages/form-builder/manage.vue | 198 ++++++++++++++---- pages/process-builder/index.vue | 4 - prisma/json/json-schema.json | 22 ++ .../migrations/20241201_add_form_grouping.sql | 9 + prisma/schema.prisma | 6 + server/api/forms/[id].put.js | 14 +- server/api/forms/create.post.js | 7 +- server/api/forms/index.get.js | 3 + stores/formBuilder.js | 71 ++++--- 10 files changed, 414 insertions(+), 85 deletions(-) create mode 100644 prisma/migrations/20241201_add_form_grouping.sql diff --git a/pages/form-builder/index.vue b/pages/form-builder/index.vue index 6565a6e..cba8f0b 100644 --- a/pages/form-builder/index.vue +++ b/pages/form-builder/index.vue @@ -7,7 +7,7 @@
@@ -91,17 +91,13 @@ Form Settings -
-
+
- + +
+
+ +

Create a New Form

+

Get started by creating a new form or navigate back to manage your existing forms.

+ +
+
+ + + + +
+ + + +
+
+ +
+ + + Create Form + + + + + Back to Forms + +
+
+
+
@@ -577,6 +629,23 @@ help="Brief description of what this form is for" rows="3" /> + +
+ + + +
@@ -1067,6 +1136,13 @@ const router = useRouter(); const formStore = useFormBuilderStore(); let toast; +// Track if we're creating a new form +const isCreatingForm = ref(false); +const newFormName = ref(''); +const newFormDescription = ref(''); +const newFormCategory = ref(''); +const newFormGroup = ref(''); + // Try to use the toast composable if available try { toast = useToast(); @@ -1166,6 +1242,30 @@ const formName = computed({ } }); +// Category options for form creation +const categoryOptions = [ + { label: 'Select Category', value: '' }, + { label: 'Forms', value: 'forms' }, + { label: 'Surveys', value: 'surveys' }, + { label: 'Applications', value: 'applications' }, + { label: 'Feedback', value: 'feedback' }, + { label: 'Registration', value: 'registration' }, + { label: 'Other', value: 'other' } +]; + +// Computed to check if we have a current form loaded +const hasCurrentForm = computed(() => { + // Consider a form loaded if any of these conditions are true: + // 1. Form has components (user has added fields) + // 2. Form has an ID (saved form) + // 3. Form name has been changed from default (form creation completed) + return formStore.formComponents.length > 0 || + formStore.currentFormId || + (formStore.formName && + formStore.formName.trim() !== '' && + formStore.formName !== 'New Form'); +}); + // Form JSON representation for developer view const formJson = computed(() => { return { @@ -1705,6 +1805,9 @@ onMounted(async () => { console.error('Error loading form from ID:', error); toast.error(`Failed to load form: ${error.message || 'Unknown error'}`); } + } else { + // No form ID provided, ensure we start with a clean slate + formStore.clearForm(); } } catch (error) { console.error('Error initializing form builder:', error); @@ -2199,6 +2302,54 @@ const navigateToManage = () => { navigationTarget.value = "/form-builder/manage"; }; +// Handle creating a new form +const createNewForm = async () => { + if (!newFormName.value.trim()) { + toast.error('Please enter a form name'); + return; + } + + try { + // Set the form name and description in the store + formStore.setFormName(newFormName.value.trim()); + formStore.formDescription = newFormDescription.value.trim(); + formStore.formCategory = newFormCategory.value.trim() || null; + formStore.formGroup = newFormGroup.value.trim() || null; + + // Clear existing form components to start fresh + formStore.formComponents = []; + formStore.currentFormId = null; // This ensures we create a new form instead of updating existing + + // Save the form to the database immediately + const savedForm = await formStore.saveForm(); + + if (savedForm) { + // Update URL to include the form ID + const newPath = `/form-builder?id=${savedForm.formUUID}`; + window.history.replaceState({}, '', newPath); + + // Reset navigation confirmation + navigationConfirmed.value = false; + + // Reset form variables + isCreatingForm.value = false; + newFormName.value = ''; + newFormDescription.value = ''; + newFormCategory.value = ''; + newFormGroup.value = ''; + + // Show success message + toast.success(`Form "${formStore.formName}" created and saved successfully`); + } else { + toast.error('Failed to save form to database. Please try again.'); + } + + } catch (error) { + console.error('Error creating form:', error); + toast.error(`Failed to create form: ${error.message || 'Unknown error'}`); + } +}; + const handleOptimizeLayout = () => { formStore.optimizeGridLayout(); }; diff --git a/pages/form-builder/manage.vue b/pages/form-builder/manage.vue index e28460a..0bf7148 100644 --- a/pages/form-builder/manage.vue +++ b/pages/form-builder/manage.vue @@ -52,7 +52,7 @@ - - - Clear Filters - - - - - Refresh - -
+
+ +
+ +
+ + +
+ +
+ + + + Clear Filters + + + + + Refresh + +
@@ -92,18 +120,19 @@
- - - - - - - - + + + + + + + + + - - + @@ -137,18 +166,38 @@
ID: {{ form.id }}
- - +
+
+ {{ form.description }} +
+
+ No description +
+
+ + + + @@ -385,6 +434,8 @@ try { // State const searchQuery = ref(''); +const selectedCategory = ref(''); +const selectedGroup = ref(''); const loading = ref(false); const showDeleteConfirm = ref(false); const formToDelete = ref(null); @@ -398,6 +449,27 @@ const jsonContent = ref(''); const jsonValidationMessage = ref(''); const jsonIsValid = ref(false); +// Computed properties for grouping +const categoryOptions = computed(() => { + const categories = [...new Set(formStore.savedForms.map(form => form.category).filter(Boolean))]; + return [ + { label: 'All Categories', value: '' }, + ...categories.map(cat => ({ label: cat, value: cat })) + ]; +}); + +const groupOptions = computed(() => { + const groups = [...new Set(formStore.savedForms.map(form => form.group).filter(Boolean))]; + return [ + { label: 'All Groups', value: '' }, + ...groups.map(group => ({ label: group, value: group })) + ]; +}); + +const hasActiveFilters = computed(() => { + return searchQuery.value || selectedCategory.value || selectedGroup.value; +}); + // Filtered forms const filteredForms = computed(() => { let filtered = formStore.savedForms; @@ -412,6 +484,16 @@ const filteredForms = computed(() => { ); } + // Filter by category + if (selectedCategory.value) { + filtered = filtered.filter(form => form.category === selectedCategory.value); + } + + // Filter by group + if (selectedGroup.value) { + filtered = filtered.filter(form => form.group === selectedGroup.value); + } + return filtered; }); @@ -552,8 +634,8 @@ onMounted(async () => { await loadForms(); }); -// Watch for changes in search and reload forms -watch([searchQuery], () => { +// Watch for changes in search and filters +watch([searchQuery, selectedCategory, selectedGroup], () => { // Debounce the search to avoid too many API calls clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { @@ -566,9 +648,24 @@ let searchTimeout = null; // Clear all filters const clearFilters = () => { searchQuery.value = ''; + selectedCategory.value = ''; + selectedGroup.value = ''; // loadForms will be called automatically by the watcher }; +// Helper function to get category color +const getCategoryColor = (category) => { + const colors = { + 'forms': 'primary', + 'surveys': 'success', + 'applications': 'info', + 'feedback': 'warning', + 'registration': 'danger', + 'default': 'secondary' + }; + return colors[category?.toLowerCase()] || colors.default; +}; + // Clean up the search timeout on component unmount onUnmounted(() => { clearTimeout(searchTimeout); @@ -835,4 +932,13 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } + +/* Line clamp utility for description text */ +.line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 9f11333..ff65be0 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -3003,10 +3003,6 @@ const sendToBack = () => { Process Settings - diff --git a/prisma/json/json-schema.json b/prisma/json/json-schema.json index 65e9f23..421aa37 100644 --- a/prisma/json/json-schema.json +++ b/prisma/json/json-schema.json @@ -208,6 +208,28 @@ "null" ] }, + "formCategory": { + "type": [ + "string", + "null" + ] + }, + "formTags": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "formGroup": { + "type": [ + "string", + "null" + ] + }, "creator": { "anyOf": [ { diff --git a/prisma/migrations/20241201_add_form_grouping.sql b/prisma/migrations/20241201_add_form_grouping.sql new file mode 100644 index 0000000..be61e96 --- /dev/null +++ b/prisma/migrations/20241201_add_form_grouping.sql @@ -0,0 +1,9 @@ +-- Add form grouping fields +ALTER TABLE `form` ADD COLUMN `formCategory` VARCHAR(100) NULL; +ALTER TABLE `form` ADD COLUMN `formTags` JSON NULL; +ALTER TABLE `form` ADD COLUMN `formGroup` VARCHAR(100) NULL; + +-- Add indexes for better performance +CREATE INDEX `IDX_form_category` ON `form`(`formCategory`); +CREATE INDEX `IDX_form_group` ON `form`(`formGroup`); +CREATE INDEX `IDX_form_status` ON `form`(`formStatus`); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b793d7c..d80ec3b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -65,11 +65,17 @@ model form { formEvents Json? scriptMode String? @default("safe") @db.VarChar(20) submitButton Json? + formCategory String? @db.VarChar(100) + formTags Json? + formGroup String? @db.VarChar(100) creator user? @relation(fields: [formCreatedBy], references: [userID]) formHistory formHistory[] task task[] @@index([formCreatedBy], map: "FK_form_creator") + @@index([formCategory], map: "IDX_form_category") + @@index([formGroup], map: "IDX_form_group") + @@index([formStatus], map: "IDX_form_status") } model formHistory { diff --git a/server/api/forms/[id].put.js b/server/api/forms/[id].put.js index a66cf59..f12e462 100644 --- a/server/api/forms/[id].put.js +++ b/server/api/forms/[id].put.js @@ -61,7 +61,7 @@ export default defineEventHandler(async (event) => { submitButton: currentForm.submitButton, versionNumber: nextVersionNumber, changeDescription: body.changeDescription || null, - savedBy: body.savedBy || currentForm.formCreatedBy, + savedBy: event.context.user?.userID || currentForm.formCreatedBy, savedDate: currentForm.formModifiedDate || currentForm.formCreatedDate } }); @@ -109,6 +109,18 @@ export default defineEventHandler(async (event) => { if (body.submitButton !== undefined) { updateData.submitButton = body.submitButton; } + + if (body.formCategory !== undefined) { + updateData.formCategory = body.formCategory && body.formCategory.trim() ? body.formCategory.trim() : null; + } + + if (body.formTags !== undefined) { + updateData.formTags = body.formTags; + } + + if (body.formGroup !== undefined) { + updateData.formGroup = body.formGroup && body.formGroup.trim() ? body.formGroup.trim() : null; + } // Try to update by UUID first let form; diff --git a/server/api/forms/create.post.js b/server/api/forms/create.post.js index 5bdeaf0..7c5d31d 100644 --- a/server/api/forms/create.post.js +++ b/server/api/forms/create.post.js @@ -25,12 +25,15 @@ 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 customScript: body.customScript || null, customCSS: body.customCSS || null, formEvents: body.formEvents || null, scriptMode: body.scriptMode || 'safe', - submitButton: body.submitButton || null + submitButton: body.submitButton || null, + formCategory: body.formCategory && body.formCategory.trim() ? body.formCategory.trim() : null, + formTags: body.formTags || null, + formGroup: body.formGroup && body.formGroup.trim() ? body.formGroup.trim() : null, + formCreatedBy: event.context.user?.userID || undefined, } }); diff --git a/server/api/forms/index.get.js b/server/api/forms/index.get.js index 86a5d69..9a5d716 100644 --- a/server/api/forms/index.get.js +++ b/server/api/forms/index.get.js @@ -21,6 +21,9 @@ export default defineEventHandler(async (event) => { formStatus: true, formCreatedDate: true, formModifiedDate: true, + formCategory: true, + formGroup: true, + formTags: true, // Don't include the full components data to keep response size small creator: { select: { diff --git a/stores/formBuilder.js b/stores/formBuilder.js index d3d47a9..b04fd81 100644 --- a/stores/formBuilder.js +++ b/stores/formBuilder.js @@ -37,8 +37,13 @@ export const useFormBuilderStore = defineStore('formBuilder', { // Form preview data previewFormData: {}, - // Form history tracking - lastChangeDescription: null, + // Form history tracking + lastChangeDescription: null, + + // Form grouping + formCategory: '', + formTags: [], + formGroup: '', }), getters: { @@ -646,15 +651,18 @@ export const useFormBuilderStore = defineStore('formBuilder', { // Fetch forms from the API const forms = await this.getForms(); - // Transform to the format expected by the UI - this.savedForms = forms.map(form => ({ - id: form.formUUID, - name: form.formName, - description: form.formDescription || '', - components: form.formComponents || [], - createdAt: form.formCreatedDate, - updatedAt: form.formModifiedDate - })); + // Transform to the format expected by the UI + this.savedForms = forms.map(form => ({ + id: form.formUUID, + name: form.formName, + description: form.formDescription || '', + components: form.formComponents || [], + category: form.formCategory || '', + group: form.formGroup || '', + tags: form.formTags || [], + createdAt: form.formCreatedDate, + updatedAt: form.formModifiedDate + })); return this.savedForms; } catch (error) { @@ -676,12 +684,15 @@ export const useFormBuilderStore = defineStore('formBuilder', { })), customScript: this.formCustomScript, customCSS: this.formCustomCSS, - formEvents: this.formEvents, - scriptMode: this.scriptMode, - submitButton: this.submitButton, - // Add user info and change description for history tracking - savedBy: 1, // TODO: Get from authenticated user - changeDescription: this.lastChangeDescription || null + formEvents: this.formEvents, + scriptMode: this.scriptMode, + submitButton: this.submitButton, + formCategory: this.formCategory, + formTags: this.formTags, + formGroup: this.formGroup, + // Add change description for history tracking + changeDescription: this.lastChangeDescription || null, + // Note: savedBy will be handled by the server using the authenticated user from auth middleware }; // Determine if this is a new form or an update @@ -755,10 +766,15 @@ export const useFormBuilderStore = defineStore('formBuilder', { this.formComponents = []; this.selectedComponentId = null; - // Set form data - this.formName = result.form.formName; - this.formDescription = result.form.formDescription || ''; - this.currentFormId = result.form.formUUID; + // Set form data + this.formName = result.form.formName; + this.formDescription = result.form.formDescription || ''; + this.currentFormId = result.form.formUUID; + + // Load grouping data + this.formCategory = result.form.formCategory || ''; + this.formTags = result.form.formTags || []; + this.formGroup = result.form.formGroup || ''; // Load custom scripts and settings this.formCustomScript = result.form.customScript || ''; @@ -835,10 +851,15 @@ export const useFormBuilderStore = defineStore('formBuilder', { // Clear form data this.formComponents = []; this.selectedComponentId = null; - this.formName = 'New Form'; - this.formDescription = ''; - this.currentFormId = null; - this.hasUnsavedChanges = false; + this.formName = 'New Form'; + this.formDescription = ''; + this.currentFormId = null; + this.hasUnsavedChanges = false; + + // Reset grouping fields + this.formCategory = ''; + this.formTags = []; + this.formGroup = ''; // Reset custom scripts and settings this.formCustomScript = '';
NameDescriptionComponentsCreatedLast UpdatedActions
NameDescriptionCategoryGroupCreatedLast UpdatedActions
+

@@ -118,16 +147,16 @@

Search: "{{ searchQuery }}"
-
- - - Clear Filters - - - - Create New Form - -
+
+ + + Clear Filters + + + + Create New Form + +
-
- {{ form.description || 'No description' }} -
-
-
- {{ form.components ? form.components.length : 0 }} components -
-
-
{{ formatDate(form.createdAt) }}
-
+
+ + {{ form.category }} + +
+
No category
+
+
+ + {{ form.group }} + +
+
No group
+
+
{{ formatDate(form.createdAt) }}
+
{{ formatDate(form.updatedAt) }}