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.
This commit is contained in:
parent
8f84b00a9e
commit
c43d9b6849
@ -7,7 +7,7 @@
|
|||||||
<!-- Left section - Logo and navigation -->
|
<!-- Left section - Logo and navigation -->
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Icon
|
<Icon
|
||||||
@click="navigateTo('/', { external: true })"
|
@click="navigateTo('/form-builder/manage')"
|
||||||
name="ph:arrow-circle-left-duotone"
|
name="ph:arrow-circle-left-duotone"
|
||||||
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
class="cursor-pointer w-6 h-6 hover:text-gray-600 text-gray-500"
|
||||||
/>
|
/>
|
||||||
@ -91,17 +91,13 @@
|
|||||||
<Icon name="material-symbols:code" class="mr-2 w-4 h-4" />
|
<Icon name="material-symbols:code" class="mr-2 w-4 h-4" />
|
||||||
<span>Form Settings</span>
|
<span>Form Settings</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="navigateToManage(); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
|
||||||
<Icon name="material-symbols:settings" class="mr-2 w-4 h-4" />
|
|
||||||
<span>Manage Forms</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div v-if="hasCurrentForm" class="flex-1 flex overflow-hidden">
|
||||||
<!-- Left Panel - Components Sidebar -->
|
<!-- Left Panel - Components Sidebar -->
|
||||||
<div
|
<div
|
||||||
v-if="!isPreview"
|
v-if="!isPreview"
|
||||||
@ -553,7 +549,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state - No form selected -->
|
||||||
|
<div v-else class="flex-1 flex items-center justify-center bg-gray-50">
|
||||||
|
<div class="text-center p-8 max-w-md">
|
||||||
|
<Icon name="material-symbols:description" class="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
||||||
|
<h2 class="text-xl font-semibold text-gray-800 mb-2">Create a New Form</h2>
|
||||||
|
<p class="text-gray-600 mb-6">Get started by creating a new form or navigate back to manage your existing forms.</p>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="mb-4">
|
||||||
|
<FormKit
|
||||||
|
v-model="newFormName"
|
||||||
|
type="text"
|
||||||
|
label="Form Name"
|
||||||
|
placeholder="Enter a name for your new form"
|
||||||
|
validation="required"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
v-model="newFormDescription"
|
||||||
|
type="textarea"
|
||||||
|
label="Description (Optional)"
|
||||||
|
placeholder="Enter a description"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
v-model="newFormCategory"
|
||||||
|
type="select"
|
||||||
|
label="Category (Optional)"
|
||||||
|
placeholder="Select a category"
|
||||||
|
:options="categoryOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
v-model="newFormGroup"
|
||||||
|
type="text"
|
||||||
|
label="Group (Optional)"
|
||||||
|
placeholder="Enter a group name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-4">
|
||||||
|
<RsButton @click="createNewForm" variant="primary" :disabled="!newFormName.trim()">
|
||||||
|
<Icon name="material-symbols:add" class="mr-1" />
|
||||||
|
Create Form
|
||||||
|
</RsButton>
|
||||||
|
|
||||||
|
<RsButton @click="navigateToManage" variant="tertiary">
|
||||||
|
<Icon name="material-symbols:arrow-back" class="mr-1" />
|
||||||
|
Back to Forms
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Form Settings Modal -->
|
<!-- Form Settings Modal -->
|
||||||
<RsModal v-model="showFormSettings" title="Form Settings & Scripts" size="xl" position="center">
|
<RsModal v-model="showFormSettings" title="Form Settings & Scripts" size="xl" position="center">
|
||||||
@ -577,6 +629,23 @@
|
|||||||
help="Brief description of what this form is for"
|
help="Brief description of what this form is for"
|
||||||
rows="3"
|
rows="3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Category"
|
||||||
|
v-model="formStore.formCategory"
|
||||||
|
:options="categoryOptions"
|
||||||
|
help="Categorize your form for better organization"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Group"
|
||||||
|
v-model="formStore.formGroup"
|
||||||
|
help="Group related forms together"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -1067,6 +1136,13 @@ const router = useRouter();
|
|||||||
const formStore = useFormBuilderStore();
|
const formStore = useFormBuilderStore();
|
||||||
let toast;
|
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 to use the toast composable if available
|
||||||
try {
|
try {
|
||||||
toast = useToast();
|
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
|
// Form JSON representation for developer view
|
||||||
const formJson = computed(() => {
|
const formJson = computed(() => {
|
||||||
return {
|
return {
|
||||||
@ -1705,6 +1805,9 @@ onMounted(async () => {
|
|||||||
console.error('Error loading form from ID:', error);
|
console.error('Error loading form from ID:', error);
|
||||||
toast.error(`Failed to load form: ${error.message || 'Unknown 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) {
|
} catch (error) {
|
||||||
console.error('Error initializing form builder:', error);
|
console.error('Error initializing form builder:', error);
|
||||||
@ -2199,6 +2302,54 @@ const navigateToManage = () => {
|
|||||||
navigationTarget.value = "/form-builder/manage";
|
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 = () => {
|
const handleOptimizeLayout = () => {
|
||||||
formStore.optimizeGridLayout();
|
formStore.optimizeGridLayout();
|
||||||
};
|
};
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
<FormKit
|
<FormKit
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search forms..."
|
placeholder="Search forms by name or description..."
|
||||||
:classes="{
|
:classes="{
|
||||||
outer: 'mb-0',
|
outer: 'mb-0',
|
||||||
wrapper: 'relative',
|
wrapper: 'relative',
|
||||||
@ -67,17 +67,45 @@
|
|||||||
</FormKit>
|
</FormKit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<RsButton @click="clearFilters" variant="secondary" size="sm" :disabled="loading" v-if="searchQuery">
|
<!-- Category Filter -->
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
<div class="relative">
|
||||||
Clear Filters
|
<FormKit
|
||||||
</RsButton>
|
v-model="selectedCategory"
|
||||||
|
type="select"
|
||||||
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
placeholder="All Categories"
|
||||||
<Icon name="material-symbols:refresh" class="mr-1" />
|
:options="categoryOptions"
|
||||||
Refresh
|
:classes="{
|
||||||
</RsButton>
|
outer: 'mb-0',
|
||||||
</div>
|
input: 'min-w-[140px] text-sm'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group Filter -->
|
||||||
|
<div class="relative">
|
||||||
|
<FormKit
|
||||||
|
v-model="selectedGroup"
|
||||||
|
type="select"
|
||||||
|
placeholder="All Groups"
|
||||||
|
:options="groupOptions"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'min-w-[140px] text-sm'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RsButton @click="clearFilters" variant="secondary" size="sm" :disabled="loading" v-if="hasActiveFilters">
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
|
||||||
|
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
||||||
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
||||||
|
Refresh
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@ -92,18 +120,19 @@
|
|||||||
<div v-else class="bg-white shadow-md rounded-lg overflow-hidden">
|
<div v-else class="bg-white shadow-md rounded-lg overflow-hidden">
|
||||||
<table class="w-full table-auto">
|
<table class="w-full table-auto">
|
||||||
<thead class="bg-gray-50 border-b">
|
<thead class="bg-gray-50 border-b">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Components</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Group</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Updated</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Updated</th>
|
||||||
</tr>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200">
|
<tbody class="divide-y divide-gray-200">
|
||||||
<tr v-if="filteredForms.length === 0">
|
<tr v-if="filteredForms.length === 0">
|
||||||
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
|
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<Icon name="material-symbols:description-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
<Icon name="material-symbols:description-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
||||||
<p class="text-lg font-medium mb-1">
|
<p class="text-lg font-medium mb-1">
|
||||||
@ -118,16 +147,16 @@
|
|||||||
<div>Search: "{{ searchQuery }}"</div>
|
<div>Search: "{{ searchQuery }}"</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<RsButton v-if="searchQuery" @click="clearFilters" variant="secondary" size="sm">
|
<RsButton v-if="hasActiveFilters" @click="clearFilters" variant="secondary" size="sm">
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
Clear Filters
|
Clear Filters
|
||||||
</RsButton>
|
</RsButton>
|
||||||
<RsButton v-if="!searchQuery" @click="createNewForm" variant="primary" size="sm">
|
<RsButton v-if="!hasActiveFilters" @click="createNewForm" variant="primary" size="sm">
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
<Icon name="material-symbols:add" class="mr-1" />
|
||||||
Create New Form
|
Create New Form
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -137,18 +166,38 @@
|
|||||||
<div class="text-sm text-gray-500">ID: {{ form.id }}</div>
|
<div class="text-sm text-gray-500">ID: {{ form.id }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="text-sm text-gray-700 max-w-xs truncate">
|
<div class="text-sm text-gray-700 max-w-xs">
|
||||||
{{ form.description || 'No description' }}
|
<div
|
||||||
</div>
|
v-if="form.description && form.description.trim()"
|
||||||
</td>
|
class="line-clamp-2 cursor-help"
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
:title="form.description.length > 100 ? form.description : ''"
|
||||||
<div class="text-sm text-gray-700">
|
>
|
||||||
{{ form.components ? form.components.length : 0 }} components
|
{{ form.description }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div v-else class="text-gray-400 italic">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
No description
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(form.createdAt) }}</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div v-if="form.category" class="text-sm">
|
||||||
|
<RsBadge :variant="getCategoryColor(form.category)" size="sm">
|
||||||
|
{{ form.category }}
|
||||||
|
</RsBadge>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-gray-400 italic">No category</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div v-if="form.group" class="text-sm">
|
||||||
|
<RsBadge variant="secondary" size="sm">
|
||||||
|
{{ form.group }}
|
||||||
|
</RsBadge>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-gray-400 italic">No group</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="text-sm text-gray-500">{{ formatDate(form.createdAt) }}</div>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(form.updatedAt) }}</div>
|
<div class="text-sm text-gray-500">{{ formatDate(form.updatedAt) }}</div>
|
||||||
</td>
|
</td>
|
||||||
@ -385,6 +434,8 @@ try {
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
|
const selectedCategory = ref('');
|
||||||
|
const selectedGroup = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showDeleteConfirm = ref(false);
|
const showDeleteConfirm = ref(false);
|
||||||
const formToDelete = ref(null);
|
const formToDelete = ref(null);
|
||||||
@ -398,6 +449,27 @@ const jsonContent = ref('');
|
|||||||
const jsonValidationMessage = ref('');
|
const jsonValidationMessage = ref('');
|
||||||
const jsonIsValid = ref(false);
|
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
|
// Filtered forms
|
||||||
const filteredForms = computed(() => {
|
const filteredForms = computed(() => {
|
||||||
let filtered = formStore.savedForms;
|
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;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -552,8 +634,8 @@ onMounted(async () => {
|
|||||||
await loadForms();
|
await loadForms();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for changes in search and reload forms
|
// Watch for changes in search and filters
|
||||||
watch([searchQuery], () => {
|
watch([searchQuery, selectedCategory, selectedGroup], () => {
|
||||||
// Debounce the search to avoid too many API calls
|
// Debounce the search to avoid too many API calls
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
@ -566,9 +648,24 @@ let searchTimeout = null;
|
|||||||
// Clear all filters
|
// Clear all filters
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
|
selectedCategory.value = '';
|
||||||
|
selectedGroup.value = '';
|
||||||
// loadForms will be called automatically by the watcher
|
// 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
|
// Clean up the search timeout on component unmount
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
@ -835,4 +932,13 @@ button:disabled {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -3003,10 +3003,6 @@ const sendToBack = () => {
|
|||||||
<Icon name="material-symbols:tune" class="mr-2 w-4 h-4" />
|
<Icon name="material-symbols:tune" class="mr-2 w-4 h-4" />
|
||||||
<span>Process Settings</span>
|
<span>Process Settings</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="confirmNavigation('/process-builder/manage'); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
|
||||||
<Icon name="material-symbols:settings" class="mr-2 w-4 h-4" />
|
|
||||||
<span>Manage Processes</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -208,6 +208,28 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"formCategory": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formTags": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formGroup": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"creator": {
|
"creator": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
9
prisma/migrations/20241201_add_form_grouping.sql
Normal file
9
prisma/migrations/20241201_add_form_grouping.sql
Normal file
@ -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`);
|
@ -65,11 +65,17 @@ model form {
|
|||||||
formEvents Json?
|
formEvents Json?
|
||||||
scriptMode String? @default("safe") @db.VarChar(20)
|
scriptMode String? @default("safe") @db.VarChar(20)
|
||||||
submitButton Json?
|
submitButton Json?
|
||||||
|
formCategory String? @db.VarChar(100)
|
||||||
|
formTags Json?
|
||||||
|
formGroup String? @db.VarChar(100)
|
||||||
creator user? @relation(fields: [formCreatedBy], references: [userID])
|
creator user? @relation(fields: [formCreatedBy], references: [userID])
|
||||||
formHistory formHistory[]
|
formHistory formHistory[]
|
||||||
task task[]
|
task task[]
|
||||||
|
|
||||||
@@index([formCreatedBy], map: "FK_form_creator")
|
@@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 {
|
model formHistory {
|
||||||
|
@ -61,7 +61,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
submitButton: currentForm.submitButton,
|
submitButton: currentForm.submitButton,
|
||||||
versionNumber: nextVersionNumber,
|
versionNumber: nextVersionNumber,
|
||||||
changeDescription: body.changeDescription || null,
|
changeDescription: body.changeDescription || null,
|
||||||
savedBy: body.savedBy || currentForm.formCreatedBy,
|
savedBy: event.context.user?.userID || currentForm.formCreatedBy,
|
||||||
savedDate: currentForm.formModifiedDate || currentForm.formCreatedDate
|
savedDate: currentForm.formModifiedDate || currentForm.formCreatedDate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -109,6 +109,18 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (body.submitButton !== undefined) {
|
if (body.submitButton !== undefined) {
|
||||||
updateData.submitButton = body.submitButton;
|
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
|
// Try to update by UUID first
|
||||||
let form;
|
let form;
|
||||||
|
@ -25,12 +25,15 @@ export default defineEventHandler(async (event) => {
|
|||||||
formDescription: body.formDescription || null,
|
formDescription: body.formDescription || null,
|
||||||
formComponents: body.components || [],
|
formComponents: body.components || [],
|
||||||
formStatus: body.status || 'active',
|
formStatus: body.status || 'active',
|
||||||
formCreatedBy: body.createdBy || null, // In a real app, this would come from the authenticated user
|
|
||||||
customScript: body.customScript || null,
|
customScript: body.customScript || null,
|
||||||
customCSS: body.customCSS || null,
|
customCSS: body.customCSS || null,
|
||||||
formEvents: body.formEvents || null,
|
formEvents: body.formEvents || null,
|
||||||
scriptMode: body.scriptMode || 'safe',
|
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,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,6 +21,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
formStatus: true,
|
formStatus: true,
|
||||||
formCreatedDate: true,
|
formCreatedDate: true,
|
||||||
formModifiedDate: true,
|
formModifiedDate: true,
|
||||||
|
formCategory: true,
|
||||||
|
formGroup: true,
|
||||||
|
formTags: true,
|
||||||
// Don't include the full components data to keep response size small
|
// Don't include the full components data to keep response size small
|
||||||
creator: {
|
creator: {
|
||||||
select: {
|
select: {
|
||||||
|
@ -37,8 +37,13 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
// Form preview data
|
// Form preview data
|
||||||
previewFormData: {},
|
previewFormData: {},
|
||||||
|
|
||||||
// Form history tracking
|
// Form history tracking
|
||||||
lastChangeDescription: null,
|
lastChangeDescription: null,
|
||||||
|
|
||||||
|
// Form grouping
|
||||||
|
formCategory: '',
|
||||||
|
formTags: [],
|
||||||
|
formGroup: '',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -646,15 +651,18 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
// Fetch forms from the API
|
// Fetch forms from the API
|
||||||
const forms = await this.getForms();
|
const forms = await this.getForms();
|
||||||
|
|
||||||
// Transform to the format expected by the UI
|
// Transform to the format expected by the UI
|
||||||
this.savedForms = forms.map(form => ({
|
this.savedForms = forms.map(form => ({
|
||||||
id: form.formUUID,
|
id: form.formUUID,
|
||||||
name: form.formName,
|
name: form.formName,
|
||||||
description: form.formDescription || '',
|
description: form.formDescription || '',
|
||||||
components: form.formComponents || [],
|
components: form.formComponents || [],
|
||||||
createdAt: form.formCreatedDate,
|
category: form.formCategory || '',
|
||||||
updatedAt: form.formModifiedDate
|
group: form.formGroup || '',
|
||||||
}));
|
tags: form.formTags || [],
|
||||||
|
createdAt: form.formCreatedDate,
|
||||||
|
updatedAt: form.formModifiedDate
|
||||||
|
}));
|
||||||
|
|
||||||
return this.savedForms;
|
return this.savedForms;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -676,12 +684,15 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
})),
|
})),
|
||||||
customScript: this.formCustomScript,
|
customScript: this.formCustomScript,
|
||||||
customCSS: this.formCustomCSS,
|
customCSS: this.formCustomCSS,
|
||||||
formEvents: this.formEvents,
|
formEvents: this.formEvents,
|
||||||
scriptMode: this.scriptMode,
|
scriptMode: this.scriptMode,
|
||||||
submitButton: this.submitButton,
|
submitButton: this.submitButton,
|
||||||
// Add user info and change description for history tracking
|
formCategory: this.formCategory,
|
||||||
savedBy: 1, // TODO: Get from authenticated user
|
formTags: this.formTags,
|
||||||
changeDescription: this.lastChangeDescription || null
|
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
|
// Determine if this is a new form or an update
|
||||||
@ -755,10 +766,15 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
this.formComponents = [];
|
this.formComponents = [];
|
||||||
this.selectedComponentId = null;
|
this.selectedComponentId = null;
|
||||||
|
|
||||||
// Set form data
|
// Set form data
|
||||||
this.formName = result.form.formName;
|
this.formName = result.form.formName;
|
||||||
this.formDescription = result.form.formDescription || '';
|
this.formDescription = result.form.formDescription || '';
|
||||||
this.currentFormId = result.form.formUUID;
|
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
|
// Load custom scripts and settings
|
||||||
this.formCustomScript = result.form.customScript || '';
|
this.formCustomScript = result.form.customScript || '';
|
||||||
@ -835,10 +851,15 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
// Clear form data
|
// Clear form data
|
||||||
this.formComponents = [];
|
this.formComponents = [];
|
||||||
this.selectedComponentId = null;
|
this.selectedComponentId = null;
|
||||||
this.formName = 'New Form';
|
this.formName = 'New Form';
|
||||||
this.formDescription = '';
|
this.formDescription = '';
|
||||||
this.currentFormId = null;
|
this.currentFormId = null;
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
|
|
||||||
|
// Reset grouping fields
|
||||||
|
this.formCategory = '';
|
||||||
|
this.formTags = [];
|
||||||
|
this.formGroup = '';
|
||||||
|
|
||||||
// Reset custom scripts and settings
|
// Reset custom scripts and settings
|
||||||
this.formCustomScript = '';
|
this.formCustomScript = '';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user