Merge branch 'demo' into demo-pagewrapper
This commit is contained in:
commit
f217b90a98
@ -27,14 +27,21 @@
|
|||||||
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
||||||
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
||||||
:readonly="component.props.readonly || isPreview"
|
:readonly="component.props.readonly || isPreview"
|
||||||
:disabled="isPreview" :options="component.props.options || undefined" :value="component.props.value || undefined"
|
:disabled="isPreview || (component.props.readonly && ['select', 'checkbox', 'radio'].includes(component.type))"
|
||||||
|
:options="component.props.options || undefined" :value="component.props.value || undefined"
|
||||||
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
||||||
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
||||||
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
||||||
:maxFiles="component.props.maxFiles || undefined" :classes="component.type === 'checkbox' ? {
|
:maxFiles="component.props.maxFiles || undefined" :rows="component.type === 'textarea' ? (component.props.rows || 3) : undefined"
|
||||||
|
:classes="component.type === 'checkbox' ? {
|
||||||
wrapper: 'mb-1',
|
wrapper: 'mb-1',
|
||||||
options: 'space-y-0.5'
|
options: 'space-y-0.5'
|
||||||
} : {}" :class="{ 'canvas-component': isPreview }" />
|
} : {}" :class="{
|
||||||
|
'canvas-component': isPreview,
|
||||||
|
'readonly-select': component.props.readonly && component.type === 'select',
|
||||||
|
'readonly-checkbox': component.props.readonly && component.type === 'checkbox',
|
||||||
|
'readonly-radio': component.props.readonly && component.type === 'radio'
|
||||||
|
}" />
|
||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<div v-else-if="component.type === 'heading'" class="py-2">
|
<div v-else-if="component.type === 'heading'" class="py-2">
|
||||||
@ -325,15 +332,70 @@
|
|||||||
|
|
||||||
<!-- Button Component -->
|
<!-- Button Component -->
|
||||||
<div v-else-if="component.type === 'button'" class="py-2">
|
<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">
|
<label v-if="component.props.label && component.props.showLabel !== false" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{{ component.props.label }}
|
{{ component.props.label }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<RsButton :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
|
<!-- Custom Button with Custom Colors -->
|
||||||
:size="component.props.size || 'md'" :disabled="component.props.disabled || false" @click="handleButtonClick"
|
<div v-if="component.props.variant === 'custom'" class="inline-block">
|
||||||
class="button-component">
|
<!-- Link Button with Custom Colors -->
|
||||||
{{ component.props.label || 'Button' }}
|
<a v-if="component.props.linkType && component.props.linkType !== 'none' && getButtonLink()"
|
||||||
</RsButton>
|
:href="getButtonLink()"
|
||||||
|
:target="component.props.linkTarget || '_self'"
|
||||||
|
class="inline-block">
|
||||||
|
<button
|
||||||
|
:type="component.props.buttonType || 'button'"
|
||||||
|
:disabled="component.props.disabled || false"
|
||||||
|
:class="getButtonSizeClass(component.props.size)"
|
||||||
|
:style="getCustomButtonStyles(component.props)"
|
||||||
|
:data-hover-effect="component.props.customHoverEffect || 'none'"
|
||||||
|
class="button-component custom-button"
|
||||||
|
@click="handleButtonClick"
|
||||||
|
>
|
||||||
|
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
|
||||||
|
<Icon v-if="component.props.icon" :name="component.props.icon" class="w-4 h-4" :class="{'ml-2': component.props.showButtonText !== false}" />
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Regular Button with Custom Colors (no link) -->
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
:type="component.props.buttonType || 'button'"
|
||||||
|
:disabled="component.props.disabled || false"
|
||||||
|
:class="getButtonSizeClass(component.props.size)"
|
||||||
|
:style="getCustomButtonStyles(component.props)"
|
||||||
|
:data-hover-effect="component.props.customHoverEffect || 'none'"
|
||||||
|
class="button-component custom-button"
|
||||||
|
@click="handleButtonClick"
|
||||||
|
>
|
||||||
|
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
|
||||||
|
<Icon v-if="component.props.icon" :name="component.props.icon" class="w-4 h-4" :class="{'ml-2': component.props.showButtonText !== false}" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Standard Button (non-custom colors) -->
|
||||||
|
<div v-else class="inline-block">
|
||||||
|
<!-- Link Button -->
|
||||||
|
<a v-if="component.props.linkType && component.props.linkType !== 'none' && getButtonLink()"
|
||||||
|
:href="getButtonLink()"
|
||||||
|
:target="component.props.linkTarget || '_self'"
|
||||||
|
class="inline-block">
|
||||||
|
<RsButton :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
|
||||||
|
:size="component.props.size || 'md'" :disabled="component.props.disabled || false"
|
||||||
|
class="button-component">
|
||||||
|
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
|
||||||
|
<Icon v-if="component.props.icon" :name="component.props.icon" class="w-4 h-4" :class="{'ml-2': component.props.showButtonText !== false}" />
|
||||||
|
</RsButton>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Regular Button (no link) -->
|
||||||
|
<RsButton v-else :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
|
||||||
|
:size="component.props.size || 'md'" :disabled="component.props.disabled || false" @click="handleButtonClick"
|
||||||
|
class="button-component">
|
||||||
|
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
|
||||||
|
<Icon v-if="component.props.icon" :name="component.props.icon" class="w-4 h-4" :class="{'ml-2': component.props.showButtonText !== false}" />
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
<div v-if="component.props.help" class="mt-1 text-xs text-gray-500">
|
||||||
{{ component.props.help }}
|
{{ component.props.help }}
|
||||||
@ -394,7 +456,7 @@
|
|||||||
'no-header': !component.props.showHeader
|
'no-header': !component.props.showHeader
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- In preview mode, show the nested components or placeholder -->
|
<!-- In preview mode, show only the nested components (no placeholder) -->
|
||||||
<div v-if="isPreview" class="section-fields">
|
<div v-if="isPreview" class="section-fields">
|
||||||
<!-- Render nested components if they exist -->
|
<!-- Render nested components if they exist -->
|
||||||
<div v-if="component.props.children && component.props.children.length > 0" class="space-y-3">
|
<div v-if="component.props.children && component.props.children.length > 0" class="space-y-3">
|
||||||
@ -402,17 +464,7 @@
|
|||||||
<component-preview :component="childComponent" :is-preview="true" />
|
<component-preview :component="childComponent" :is-preview="true" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- No placeholder in preview mode when empty -->
|
||||||
<!-- Show placeholder if no nested components -->
|
|
||||||
<div v-else class="section-placeholder">
|
|
||||||
<div class="placeholder-content">
|
|
||||||
<Icon name="material-symbols:dashboard-customize-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
|
||||||
<p class="text-sm text-gray-500 text-center mb-2">Form Section Container</p>
|
|
||||||
<p class="text-xs text-gray-400 text-center">
|
|
||||||
This section will contain the form fields grouped here
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- In form builder mode, show drop zone -->
|
<!-- In form builder mode, show drop zone -->
|
||||||
@ -477,14 +529,15 @@
|
|||||||
class="section-drop-placeholder"
|
class="section-drop-placeholder"
|
||||||
:class="{
|
:class="{
|
||||||
'drop-active': sectionDropStates[component.id]?.isDraggingOver,
|
'drop-active': sectionDropStates[component.id]?.isDraggingOver,
|
||||||
'empty': !component.props.children || component.props.children.length === 0
|
'empty': !component.props.children || component.props.children.length === 0,
|
||||||
|
'hidden-placeholder': component.props.showPlaceholder === false
|
||||||
}"
|
}"
|
||||||
@dragover.prevent="handleSectionDragOver($event, component.id)"
|
@dragover.prevent="handleSectionDragOver($event, component.id)"
|
||||||
@dragleave="handleSectionDragLeave($event, component.id)"
|
@dragleave="handleSectionDragLeave($event, component.id)"
|
||||||
@drop="handleSectionDrop($event, component.id)"
|
@drop="handleSectionDrop($event, component.id)"
|
||||||
@dragenter.prevent="handleSectionDragEnter($event, component.id)"
|
@dragenter.prevent="handleSectionDragEnter($event, component.id)"
|
||||||
>
|
>
|
||||||
<div class="placeholder-content">
|
<div v-if="component.props.showPlaceholder !== false" class="placeholder-content">
|
||||||
<Icon name="material-symbols:add-box-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
<Icon name="material-symbols:add-box-outline" class="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
||||||
<p class="text-sm text-gray-500 text-center mb-1">Drop Components Here</p>
|
<p class="text-sm text-gray-500 text-center mb-1">Drop Components Here</p>
|
||||||
<p class="text-xs text-gray-400 text-center">
|
<p class="text-xs text-gray-400 text-center">
|
||||||
@ -534,7 +587,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['select-nested-component']);
|
const emit = defineEmits(['select-nested-component', 'form-data-updated']);
|
||||||
|
|
||||||
// Get access to the form builder store
|
// Get access to the form builder store
|
||||||
const formStore = useFormBuilderStore();
|
const formStore = useFormBuilderStore();
|
||||||
@ -593,6 +646,37 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize repeating groups
|
||||||
|
if (props.component.type === 'repeating-group') {
|
||||||
|
const groupName = props.component.props.name;
|
||||||
|
if (groupName) {
|
||||||
|
// Get current groups or initialize empty array
|
||||||
|
const currentGroups = safeGetField(groupName, formStore.previewFormData);
|
||||||
|
|
||||||
|
// If no groups exist and minItems is specified, create initial groups
|
||||||
|
if ((!currentGroups || currentGroups.length === 0) && props.component.props.minItems > 0) {
|
||||||
|
const initialGroups = [];
|
||||||
|
const minItems = props.component.props.minItems || 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < minItems; i++) {
|
||||||
|
const newGroup = {};
|
||||||
|
|
||||||
|
// Add fields from configuration
|
||||||
|
if (props.component.props.fields) {
|
||||||
|
props.component.props.fields.forEach(field => {
|
||||||
|
newGroup[field.name] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialGroups.push(newGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedData = { ...formStore.previewFormData, [groupName]: initialGroups };
|
||||||
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for changes to component props, especially defaultItems
|
// Watch for changes to component props, especially defaultItems
|
||||||
@ -609,6 +693,39 @@ watch(() => props.component.props.defaultItems, (newDefaultItems, oldDefaultItem
|
|||||||
}
|
}
|
||||||
}, { deep: true, immediate: true });
|
}, { deep: true, immediate: true });
|
||||||
|
|
||||||
|
// Watch for changes to repeating group props
|
||||||
|
watch(() => props.component.props.minItems, (newMinItems, oldMinItems) => {
|
||||||
|
if (props.component.type === 'repeating-group') {
|
||||||
|
const groupName = props.component.props.name;
|
||||||
|
if (!groupName) return;
|
||||||
|
|
||||||
|
const currentGroups = safeGetField(groupName, formStore.previewFormData);
|
||||||
|
const minItems = newMinItems || 1;
|
||||||
|
|
||||||
|
// If current groups are fewer than minItems, add missing groups
|
||||||
|
if ((!currentGroups || currentGroups.length < minItems) && minItems > 0) {
|
||||||
|
const groupsToAdd = minItems - (currentGroups?.length || 0);
|
||||||
|
const updatedGroups = [...(currentGroups || [])];
|
||||||
|
|
||||||
|
for (let i = 0; i < groupsToAdd; i++) {
|
||||||
|
const newGroup = {};
|
||||||
|
|
||||||
|
// Add fields from configuration
|
||||||
|
if (props.component.props.fields) {
|
||||||
|
props.component.props.fields.forEach(field => {
|
||||||
|
newGroup[field.name] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedGroups.push(newGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedData = { ...formStore.previewFormData, [groupName]: updatedGroups };
|
||||||
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { deep: true, immediate: true });
|
||||||
|
|
||||||
// Repeating group and dynamic list functionality
|
// Repeating group and dynamic list functionality
|
||||||
const addGroupItem = () => {
|
const addGroupItem = () => {
|
||||||
if (props.isPreview) return;
|
if (props.isPreview) return;
|
||||||
@ -635,6 +752,9 @@ const addGroupItem = () => {
|
|||||||
// Update the form data
|
// Update the form data
|
||||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||||
formStore.updatePreviewFormData(updatedData);
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
|
||||||
|
// Also emit an event to notify parent components about the data change
|
||||||
|
emit('form-data-updated', updatedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeGroupItem = (index) => {
|
const removeGroupItem = (index) => {
|
||||||
@ -646,12 +766,22 @@ const removeGroupItem = (index) => {
|
|||||||
// Get current groups
|
// Get current groups
|
||||||
const currentGroups = [...(safeGetField(groupName, formStore.previewFormData) || [])];
|
const currentGroups = [...(safeGetField(groupName, formStore.previewFormData) || [])];
|
||||||
|
|
||||||
|
// Check if we can remove this item (respect minimum items)
|
||||||
|
const minItems = props.component.props.minItems || 1;
|
||||||
|
if (currentGroups.length <= minItems) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the group at the specified index
|
// Remove the group at the specified index
|
||||||
currentGroups.splice(index, 1);
|
currentGroups.splice(index, 1);
|
||||||
|
|
||||||
// Update the form data
|
// Update the form data
|
||||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||||
formStore.updatePreviewFormData(updatedData);
|
formStore.updatePreviewFormData(updatedData);
|
||||||
|
|
||||||
|
// Also emit an event to notify parent components about the data change
|
||||||
|
// This is important for FormKit integration
|
||||||
|
emit('form-data-updated', updatedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addListItem = () => {
|
const addListItem = () => {
|
||||||
@ -1247,6 +1377,106 @@ const saveNestedComponentSettings = (updatedComponent) => {
|
|||||||
// Close the modal
|
// Close the modal
|
||||||
closeNestedSettingsModal();
|
closeNestedSettingsModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Button link functionality
|
||||||
|
const getButtonLink = () => {
|
||||||
|
if (!props.component || props.component.type !== 'button') return null;
|
||||||
|
|
||||||
|
const { linkType, linkUrl, linkProcessId, linkTarget, iframeDebug, iframeHideComplete, iframeTheme, iframeCustomParams } = props.component.props;
|
||||||
|
|
||||||
|
if (linkType === 'url' && linkUrl) {
|
||||||
|
return linkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkType === 'process' && linkProcessId) {
|
||||||
|
// Generate the process workflow URL with iframe parameters
|
||||||
|
const baseUrl = `${window.location.origin}/workflow/${linkProcessId}`;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
// Add debug parameter (false = iframe mode, true = debug mode)
|
||||||
|
if (iframeDebug !== undefined) {
|
||||||
|
params.append('debug', iframeDebug ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hideComplete parameter
|
||||||
|
if (iframeHideComplete) {
|
||||||
|
params.append('hideComplete', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add theme parameter
|
||||||
|
if (iframeTheme) {
|
||||||
|
params.append('theme', iframeTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom parameters
|
||||||
|
if (iframeCustomParams) {
|
||||||
|
const customParams = new URLSearchParams(iframeCustomParams);
|
||||||
|
customParams.forEach((value, key) => {
|
||||||
|
params.append(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom button styling functions
|
||||||
|
const getCustomButtonStyles = (props) => {
|
||||||
|
if (!props || props.variant !== 'custom') return {};
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
backgroundColor: props.customBackgroundColor || '#3b82f6',
|
||||||
|
color: props.customTextColor || '#ffffff',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease-in-out'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add border if specified
|
||||||
|
if (props.customBorderColor && props.customBorderWidth) {
|
||||||
|
styles.border = `${props.customBorderWidth}px solid ${props.customBorderColor}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add border radius
|
||||||
|
if (props.customBorderRadius) {
|
||||||
|
styles.borderRadius = `${props.customBorderRadius}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hover effects
|
||||||
|
const hoverEffect = props.customHoverEffect;
|
||||||
|
if (hoverEffect && hoverEffect !== 'none') {
|
||||||
|
switch (hoverEffect) {
|
||||||
|
case 'darken':
|
||||||
|
styles[':hover'] = { filter: 'brightness(0.9)' };
|
||||||
|
break;
|
||||||
|
case 'lighten':
|
||||||
|
styles[':hover'] = { filter: 'brightness(1.1)' };
|
||||||
|
break;
|
||||||
|
case 'scale':
|
||||||
|
styles[':hover'] = { transform: 'scale(1.05)' };
|
||||||
|
break;
|
||||||
|
case 'glow':
|
||||||
|
styles[':hover'] = {
|
||||||
|
boxShadow: `0 0 10px ${props.customBackgroundColor || '#3b82f6'}`
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getButtonSizeClass = (size) => {
|
||||||
|
const sizeClasses = {
|
||||||
|
'sm': 'px-3 py-1.5 text-sm',
|
||||||
|
'md': 'px-4 py-2 text-sm',
|
||||||
|
'lg': 'px-6 py-3 text-base'
|
||||||
|
};
|
||||||
|
return sizeClasses[size] || sizeClasses['md'];
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -1271,6 +1501,32 @@ const saveNestedComponentSettings = (updatedComponent) => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Readonly styles for select, checkbox, and radio components */
|
||||||
|
:deep(.readonly-select),
|
||||||
|
:deep(.readonly-checkbox),
|
||||||
|
:deep(.readonly-radio) {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.readonly-select select),
|
||||||
|
:deep(.readonly-select .formkit-inner),
|
||||||
|
:deep(.readonly-checkbox input[type="checkbox"]),
|
||||||
|
:deep(.readonly-radio input[type="radio"]) {
|
||||||
|
pointer-events: none !important;
|
||||||
|
opacity: 0.8 !important;
|
||||||
|
background-color: #f3f4f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.readonly-checkbox) .formkit-options,
|
||||||
|
:deep(.readonly-radio) .formkit-options {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.readonly-checkbox) .formkit-wrapper,
|
||||||
|
:deep(.readonly-radio) .formkit-wrapper {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Image Preview Component */
|
/* Image Preview Component */
|
||||||
.image-preview-container {
|
.image-preview-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1485,10 +1741,55 @@ const saveNestedComponentSettings = (updatedComponent) => {
|
|||||||
background-color: #f1f5f9;
|
background-color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-drop-placeholder.hidden-placeholder {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ghost styles for dragging */
|
/* Ghost styles for dragging */
|
||||||
.ghost {
|
.ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
background-color: #dbeafe !important;
|
background-color: #dbeafe !important;
|
||||||
border-color: #3b82f6 !important;
|
border-color: #3b82f6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom button styles */
|
||||||
|
.custom-button {
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom button hover effects */
|
||||||
|
.custom-button[data-hover-effect="darken"]:hover {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[data-hover-effect="lighten"]:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[data-hover-effect="scale"]:hover {
|
||||||
|
transform: scale(1.05) translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[data-hover-effect="glow"]:hover {
|
||||||
|
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -598,6 +598,10 @@ const availableComponents = [
|
|||||||
variant: 'primary', // primary, secondary, success, warning, danger
|
variant: 'primary', // primary, secondary, success, warning, danger
|
||||||
size: 'md', // sm, md, lg
|
size: 'md', // sm, md, lg
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
showLabel: true, // Whether to show the label above the button
|
||||||
|
showButtonText: true, // Whether to show text on the button
|
||||||
|
buttonText: '', // Text to display on button (falls back to label if empty)
|
||||||
|
icon: '', // Optional icon to display on button
|
||||||
onClick: '' // Custom JavaScript code to execute
|
onClick: '' // Custom JavaScript code to execute
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -623,6 +627,7 @@ const availableComponents = [
|
|||||||
spacing: 'normal', // compact, normal, relaxed
|
spacing: 'normal', // compact, normal, relaxed
|
||||||
width: '100%',
|
width: '100%',
|
||||||
gridColumn: 'span 12',
|
gridColumn: 'span 12',
|
||||||
|
showPlaceholder: true, // Whether to show the placeholder in builder mode
|
||||||
children: [], // Array to hold nested components
|
children: [], // Array to hold nested components
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
|
@ -218,6 +218,19 @@
|
|||||||
rows="2"
|
rows="2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
v-if="showField('rows')"
|
||||||
|
type="number"
|
||||||
|
label="Number of Rows"
|
||||||
|
name="rows"
|
||||||
|
v-model="configModel.rows"
|
||||||
|
help="Number of visible text lines in the textarea"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="3"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
v-if="showField('readonly')"
|
v-if="showField('readonly')"
|
||||||
type="switch"
|
type="switch"
|
||||||
@ -490,26 +503,347 @@
|
|||||||
{ label: 'Secondary (Gray)', value: 'secondary' },
|
{ label: 'Secondary (Gray)', value: 'secondary' },
|
||||||
{ label: 'Success (Green)', value: 'success' },
|
{ label: 'Success (Green)', value: 'success' },
|
||||||
{ label: 'Danger (Red)', value: 'danger' },
|
{ label: 'Danger (Red)', value: 'danger' },
|
||||||
{ label: 'Warning (Orange)', value: 'warning' }
|
{ label: 'Warning (Orange)', value: 'warning' },
|
||||||
|
{ label: 'Custom Color', value: 'custom' }
|
||||||
]"
|
]"
|
||||||
help="Visual appearance of the button"
|
help="Visual appearance of the button"
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Color Picker -->
|
||||||
|
<div v-if="configModel.variant === 'custom'" class="space-y-3">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Custom Color Settings</h5>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
label="Background Color"
|
||||||
|
name="customBackgroundColor"
|
||||||
|
v-model="configModel.customBackgroundColor"
|
||||||
|
help="Choose the button background color"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
label="Text Color"
|
||||||
|
name="customTextColor"
|
||||||
|
v-model="configModel.customTextColor"
|
||||||
|
help="Choose the button text color"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="color"
|
||||||
|
label="Border Color"
|
||||||
|
name="customBorderColor"
|
||||||
|
v-model="configModel.customBorderColor"
|
||||||
|
help="Choose the button border color"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="number"
|
||||||
|
label="Border Width (px)"
|
||||||
|
name="customBorderWidth"
|
||||||
|
v-model="configModel.customBorderWidth"
|
||||||
|
help="Set the border width in pixels"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
placeholder="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="number"
|
||||||
|
label="Border Radius (px)"
|
||||||
|
name="customBorderRadius"
|
||||||
|
v-model="configModel.customBorderRadius"
|
||||||
|
help="Set the corner roundness in pixels"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
:min="0"
|
||||||
|
:max="50"
|
||||||
|
placeholder="6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Hover Effect"
|
||||||
|
name="customHoverEffect"
|
||||||
|
v-model="configModel.customHoverEffect"
|
||||||
|
:options="[
|
||||||
|
{ label: 'None', value: 'none' },
|
||||||
|
{ label: 'Darken', value: 'darken' },
|
||||||
|
{ label: 'Lighten', value: 'lighten' },
|
||||||
|
{ label: 'Scale', value: 'scale' },
|
||||||
|
{ label: 'Glow', value: 'glow' }
|
||||||
|
]"
|
||||||
|
help="Choose hover animation effect"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Color Preview -->
|
||||||
|
<div class="mt-4 p-3 bg-gray-50 border border-gray-200 rounded-lg">
|
||||||
|
<h6 class="text-sm font-medium text-gray-700 mb-2">Color Preview</h6>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 rounded font-medium transition-all duration-200"
|
||||||
|
:style="getCustomButtonStyles()"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{{ configModel.buttonText || configModel.label || 'Button Preview' }}
|
||||||
|
</button>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
<div>Background: {{ configModel.customBackgroundColor || '#3b82f6' }}</div>
|
||||||
|
<div>Text: {{ configModel.customTextColor || '#ffffff' }}</div>
|
||||||
|
<div>Border: {{ configModel.customBorderColor || 'transparent' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Button Size"
|
||||||
|
name="size"
|
||||||
|
v-model="configModel.size"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Small', value: 'sm' },
|
||||||
|
{ label: 'Medium', value: 'md' },
|
||||||
|
{ label: 'Large', value: 'lg' }
|
||||||
|
]"
|
||||||
|
help="Size of the button"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Icon (Optional)"
|
||||||
|
name="icon"
|
||||||
|
v-model="configModel.icon"
|
||||||
|
help="Icon name (e.g., 'material-symbols:add')"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="material-symbols:add"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Show Label Above Button"
|
||||||
|
name="showLabel"
|
||||||
|
v-model="configModel.showLabel"
|
||||||
|
help="Display the label text above the button"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Show Text on Button"
|
||||||
|
name="showButtonText"
|
||||||
|
v-model="configModel.showButtonText"
|
||||||
|
help="Display text on the button (icon-only if disabled)"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
type="select"
|
v-if="configModel.showButtonText !== false"
|
||||||
label="Button Size"
|
type="text"
|
||||||
name="size"
|
label="Button Text"
|
||||||
v-model="configModel.size"
|
name="buttonText"
|
||||||
:options="[
|
v-model="configModel.buttonText"
|
||||||
{ label: 'Small', value: 'sm' },
|
help="Text displayed on the button (uses label if empty)"
|
||||||
{ label: 'Medium', value: 'md' },
|
|
||||||
{ label: 'Large', value: 'lg' }
|
|
||||||
]"
|
|
||||||
help="Size of the button"
|
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="e.g., Submit, Save, Continue"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Button Link Settings -->
|
||||||
|
<div class="mt-6 space-y-3">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Button Link Settings</h5>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Link Type"
|
||||||
|
name="linkType"
|
||||||
|
v-model="configModel.linkType"
|
||||||
|
:options="[
|
||||||
|
{ label: 'No Link', value: 'none' },
|
||||||
|
{ label: 'Custom URL', value: 'url' },
|
||||||
|
{ label: 'Process Link', value: 'process' }
|
||||||
|
]"
|
||||||
|
help="Choose how the button should behave when clicked"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Custom URL Link -->
|
||||||
|
<div v-if="configModel.linkType === 'url'" class="space-y-3">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="URL"
|
||||||
|
name="linkUrl"
|
||||||
|
v-model="configModel.linkUrl"
|
||||||
|
help="Enter the URL to navigate to when button is clicked"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
validation="url"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Open In"
|
||||||
|
name="linkTarget"
|
||||||
|
v-model="configModel.linkTarget"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Same Window', value: '_self' },
|
||||||
|
{ label: 'New Window/Tab', value: '_blank' }
|
||||||
|
]"
|
||||||
|
help="Choose how the link should open"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Process Link -->
|
||||||
|
<div v-if="configModel.linkType === 'process'" class="space-y-3">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Select Process"
|
||||||
|
name="linkProcessId"
|
||||||
|
v-model="configModel.linkProcessId"
|
||||||
|
:options="publishedProcesses"
|
||||||
|
help="Choose a published process to link to"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="Select a process..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Open In"
|
||||||
|
name="linkTarget"
|
||||||
|
v-model="configModel.linkTarget"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Same Window', value: '_self' },
|
||||||
|
{ label: 'New Window/Tab', value: '_blank' }
|
||||||
|
]"
|
||||||
|
help="Choose how the link should open"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Iframe Parameters -->
|
||||||
|
<div class="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<h6 class="text-sm font-medium text-blue-800 mb-3">Iframe Integration Parameters</h6>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Debug Mode"
|
||||||
|
name="iframeDebug"
|
||||||
|
v-model="configModel.iframeDebug"
|
||||||
|
help="Enable debug mode (show UI chrome) - OFF for iframe mode"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Hide Completion"
|
||||||
|
name="iframeHideComplete"
|
||||||
|
v-model="configModel.iframeHideComplete"
|
||||||
|
help="Hide completion message (auto-advance)"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Theme"
|
||||||
|
name="iframeTheme"
|
||||||
|
v-model="configModel.iframeTheme"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Default', value: '' },
|
||||||
|
{ label: 'Dark', value: 'dark' },
|
||||||
|
{ label: 'Light', value: 'light' }
|
||||||
|
]"
|
||||||
|
help="Apply custom theme to the workflow"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Custom Parameters"
|
||||||
|
name="iframeCustomParams"
|
||||||
|
v-model="configModel.iframeCustomParams"
|
||||||
|
help="Additional URL parameters (e.g., 'param1=value1¶m2=value2')"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="param1=value1¶m2=value2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 p-2 bg-gray-100 rounded text-xs text-gray-600">
|
||||||
|
<strong>Generated URL Preview:</strong><br>
|
||||||
|
<code class="text-blue-600">{{ getIframeUrlPreview() }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Button Action Script -->
|
||||||
|
<div class="mt-6 space-y-3">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Button Click Action</h5>
|
||||||
|
<p class="text-xs text-gray-600">
|
||||||
|
Write JavaScript code to execute when this button is clicked. You can access form data and perform actions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||||
|
<div class="mb-2 text-xs text-gray-600">
|
||||||
|
<strong>Available functions:</strong> getField(), setField(), showField(), hideField(), enableField(), disableField(), showSuccess(), showError(), showInfo()
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="configModel.onClick"
|
||||||
|
class="w-full h-32 font-mono text-sm p-2 border border-gray-300 rounded"
|
||||||
|
placeholder="// Example:
|
||||||
|
// Get values from other fields
|
||||||
|
const name = getField('name');
|
||||||
|
const email = getField('email');
|
||||||
|
|
||||||
|
// Show a message
|
||||||
|
if (name && email) {
|
||||||
|
showSuccess('Form data is valid!');
|
||||||
|
} else {
|
||||||
|
showError('Please fill in all required fields');
|
||||||
|
}"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Templates -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
@click="insertButtonScriptTemplate('validation')"
|
||||||
|
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
Insert Validation Example
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="insertButtonScriptTemplate('calculation')"
|
||||||
|
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
Insert Calculation Example
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="insertButtonScriptTemplate('toggle')"
|
||||||
|
class="text-xs px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
Insert Toggle Fields Example
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Dynamic List Configuration -->
|
<!-- Dynamic List Configuration -->
|
||||||
@ -703,6 +1037,255 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Data Table Configuration -->
|
||||||
|
<template v-if="component.type === 'repeating-table'">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Table 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>
|
||||||
|
|
||||||
|
<!-- Button Text 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 button"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="Edit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Delete Button Text"
|
||||||
|
name="deleteText"
|
||||||
|
v-model="configModel.deleteText"
|
||||||
|
help="Text for the delete button"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
placeholder="Delete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Behavior Settings -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Table Behavior</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="Allow Edit"
|
||||||
|
name="allowEdit"
|
||||||
|
v-model="configModel.allowEdit"
|
||||||
|
help="Allow editing existing records"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Allow Delete"
|
||||||
|
name="allowDelete"
|
||||||
|
v-model="configModel.allowDelete"
|
||||||
|
help="Allow deleting 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' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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="Enable Export"
|
||||||
|
name="enableExport"
|
||||||
|
v-model="configModel.enableExport"
|
||||||
|
help="Allow exporting table data"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Columns Management -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h5 class="text-sm font-medium text-gray-700">Table Columns</h5>
|
||||||
|
<RsButton @click="addTableColumn" variant="primary" size="sm">
|
||||||
|
<Icon name="heroicons:plus" class="w-4 h-4 mr-1" />
|
||||||
|
Add Column
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border rounded-md p-3 bg-gray-50 space-y-3">
|
||||||
|
<div v-for="(column, index) in (configModel.columns || [])" :key="index" class="border p-3 rounded bg-white">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h4 class="font-medium text-sm text-gray-800">Column {{ index + 1 }}</h4>
|
||||||
|
<button
|
||||||
|
@click="removeTableColumn(index)"
|
||||||
|
class="text-red-500 hover:text-red-700"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Column Name"
|
||||||
|
v-model="column.name"
|
||||||
|
placeholder="e.g., name, email, phone"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Column Label"
|
||||||
|
v-model="column.label"
|
||||||
|
placeholder="e.g., Name, Email, Phone"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Column 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: 'Time', value: 'time' },
|
||||||
|
{ label: 'URL', value: 'url' },
|
||||||
|
{ label: 'Select', value: 'select' },
|
||||||
|
{ label: 'Checkbox', value: 'checkbox' },
|
||||||
|
{ label: 'Text Area', value: 'textarea' }
|
||||||
|
]"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Placeholder"
|
||||||
|
v-model="column.placeholder"
|
||||||
|
placeholder="Enter placeholder text"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Validation Rules"
|
||||||
|
v-model="column.validation"
|
||||||
|
placeholder="e.g., required|email"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="column.required" class="mr-2" />
|
||||||
|
<span class="text-sm text-gray-700">Required Field</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Options for select type -->
|
||||||
|
<div v-if="column.type === 'select'" class="mt-4">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h6 class="text-sm font-medium text-gray-700">Select Options</h6>
|
||||||
|
<button
|
||||||
|
@click="addColumnOption(index)"
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:plus" class="w-3 h-3 mr-1" />
|
||||||
|
Add Option
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div 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-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Repeating Group Configuration -->
|
<!-- Repeating Group Configuration -->
|
||||||
<template v-if="component.type === 'repeating-group'">
|
<template v-if="component.type === 'repeating-group'">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@ -902,6 +1485,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
|
v-if="configModel.showHeader"
|
||||||
type="select"
|
type="select"
|
||||||
label="Header Size"
|
label="Header Size"
|
||||||
name="headerSize"
|
name="headerSize"
|
||||||
@ -916,6 +1500,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
|
v-if="configModel.showHeader"
|
||||||
type="switch"
|
type="switch"
|
||||||
label="Collapsible"
|
label="Collapsible"
|
||||||
name="collapsible"
|
name="collapsible"
|
||||||
@ -925,13 +1510,22 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
|
v-if="configModel.showHeader && configModel.collapsible"
|
||||||
type="switch"
|
type="switch"
|
||||||
label="Start Collapsed"
|
label="Start Collapsed"
|
||||||
name="collapsed"
|
name="collapsed"
|
||||||
v-model="configModel.collapsed"
|
v-model="configModel.collapsed"
|
||||||
help="Start with this section collapsed"
|
help="Start with this section collapsed"
|
||||||
:classes="{ outer: 'field-wrapper' }"
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
:disabled="!configModel.collapsible"
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
type="switch"
|
||||||
|
label="Show Placeholder"
|
||||||
|
name="showPlaceholder"
|
||||||
|
v-model="configModel.showPlaceholder"
|
||||||
|
help="Show placeholder when empty in builder mode"
|
||||||
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1744,6 +2338,7 @@ const showField = (fieldName) => {
|
|||||||
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||||
value: ['heading', 'paragraph', 'hidden'],
|
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', 'form-section', 'info-display', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
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', 'form-section', 'info-display', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||||
|
rows: ['textarea'],
|
||||||
options: ['select', 'checkbox', 'radio'],
|
options: ['select', 'checkbox', 'radio'],
|
||||||
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||||
readonly: ['text', 'number', 'email', 'textarea', 'mask', 'url', 'tel']
|
readonly: ['text', 'number', 'email', 'textarea', 'mask', 'url', 'tel']
|
||||||
@ -1787,6 +2382,113 @@ const isTextBasedField = computed(() => {
|
|||||||
return ['text', 'textarea', 'email', 'password', 'url', 'tel'].includes(props.component?.type)
|
return ['text', 'textarea', 'email', 'password', 'url', 'tel'].includes(props.component?.type)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Published processes for button linking
|
||||||
|
const publishedProcesses = ref([])
|
||||||
|
|
||||||
|
// Fetch published processes when component is a button
|
||||||
|
const fetchPublishedProcesses = async () => {
|
||||||
|
if (props.component?.type === 'button') {
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/process?status=published&limit=1000')
|
||||||
|
if (response.success) {
|
||||||
|
publishedProcesses.value = response.data.processes.map(process => ({
|
||||||
|
label: process.processName,
|
||||||
|
value: process.processUUID
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching published processes:', error)
|
||||||
|
publishedProcesses.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for component changes to fetch processes when needed
|
||||||
|
watch(() => props.component, (newComponent) => {
|
||||||
|
if (newComponent?.type === 'button') {
|
||||||
|
fetchPublishedProcesses()
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Generate iframe URL preview
|
||||||
|
const getIframeUrlPreview = () => {
|
||||||
|
if (!configModel.value.linkProcessId) {
|
||||||
|
return 'Select a process first...'
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = `${window.location.origin}/workflow/${configModel.value.linkProcessId}`
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
// Add debug parameter (false = iframe mode, true = debug mode)
|
||||||
|
if (configModel.value.iframeDebug !== undefined) {
|
||||||
|
params.append('debug', configModel.value.iframeDebug ? 'true' : 'false')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hideComplete parameter
|
||||||
|
if (configModel.value.iframeHideComplete) {
|
||||||
|
params.append('hideComplete', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add theme parameter
|
||||||
|
if (configModel.value.iframeTheme) {
|
||||||
|
params.append('theme', configModel.value.iframeTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom parameters
|
||||||
|
if (configModel.value.iframeCustomParams) {
|
||||||
|
const customParams = new URLSearchParams(configModel.value.iframeCustomParams)
|
||||||
|
customParams.forEach((value, key) => {
|
||||||
|
params.append(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString()
|
||||||
|
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate custom button styles for preview
|
||||||
|
const getCustomButtonStyles = () => {
|
||||||
|
const styles = {
|
||||||
|
backgroundColor: configModel.value.customBackgroundColor || '#3b82f6',
|
||||||
|
color: configModel.value.customTextColor || '#ffffff',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add border if specified
|
||||||
|
if (configModel.value.customBorderColor && configModel.value.customBorderWidth) {
|
||||||
|
styles.border = `${configModel.value.customBorderWidth}px solid ${configModel.value.customBorderColor}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add border radius
|
||||||
|
if (configModel.value.customBorderRadius) {
|
||||||
|
styles.borderRadius = `${configModel.value.customBorderRadius}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add hover effects
|
||||||
|
const hoverEffect = configModel.value.customHoverEffect
|
||||||
|
if (hoverEffect && hoverEffect !== 'none') {
|
||||||
|
switch (hoverEffect) {
|
||||||
|
case 'darken':
|
||||||
|
styles[':hover'] = { filter: 'brightness(0.9)' }
|
||||||
|
break
|
||||||
|
case 'lighten':
|
||||||
|
styles[':hover'] = { filter: 'brightness(1.1)' }
|
||||||
|
break
|
||||||
|
case 'scale':
|
||||||
|
styles[':hover'] = { transform: 'scale(1.05)' }
|
||||||
|
break
|
||||||
|
case 'glow':
|
||||||
|
styles[':hover'] = {
|
||||||
|
boxShadow: `0 0 10px ${configModel.value.customBackgroundColor || '#3b82f6'}`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles
|
||||||
|
}
|
||||||
|
|
||||||
// Type changing functionality
|
// Type changing functionality
|
||||||
const compatibilityGroups = {
|
const compatibilityGroups = {
|
||||||
// Text-based inputs (can switch between each other)
|
// Text-based inputs (can switch between each other)
|
||||||
@ -2162,6 +2864,80 @@ const handleReset = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button script template helper
|
||||||
|
const insertButtonScriptTemplate = (templateType) => {
|
||||||
|
let template = '';
|
||||||
|
|
||||||
|
switch (templateType) {
|
||||||
|
case 'validation':
|
||||||
|
template = `// Form validation example
|
||||||
|
const name = getField('name');
|
||||||
|
const email = getField('email');
|
||||||
|
const phone = getField('phone');
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!name || !email) {
|
||||||
|
showError('Please fill in all required fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email format
|
||||||
|
if (email && !email.includes('@')) {
|
||||||
|
showError('Please enter a valid email address');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success message
|
||||||
|
showSuccess('Form validation passed!');
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'calculation':
|
||||||
|
template = `// Calculation example
|
||||||
|
const quantity = Number(getField('quantity') || 0);
|
||||||
|
const price = Number(getField('price') || 0);
|
||||||
|
const taxRate = 0.08; // 8% tax
|
||||||
|
|
||||||
|
// Calculate subtotal
|
||||||
|
const subtotal = quantity * price;
|
||||||
|
setField('subtotal', subtotal.toFixed(2));
|
||||||
|
|
||||||
|
// Calculate tax
|
||||||
|
const tax = subtotal * taxRate;
|
||||||
|
setField('tax', tax.toFixed(2));
|
||||||
|
|
||||||
|
// Calculate total
|
||||||
|
const total = subtotal + tax;
|
||||||
|
setField('total', total.toFixed(2));
|
||||||
|
|
||||||
|
showInfo('Calculation completed');
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'toggle':
|
||||||
|
template = `// Toggle fields visibility example
|
||||||
|
const selectedOption = getField('options');
|
||||||
|
|
||||||
|
// Hide all detail fields first
|
||||||
|
hideField('details_option_1');
|
||||||
|
hideField('details_option_2');
|
||||||
|
hideField('details_option_3');
|
||||||
|
|
||||||
|
// Show only the relevant detail field
|
||||||
|
if (selectedOption === 'option_1') {
|
||||||
|
showField('details_option_1');
|
||||||
|
} else if (selectedOption === 'option_2') {
|
||||||
|
showField('details_option_2');
|
||||||
|
} else if (selectedOption === 'option_3') {
|
||||||
|
showField('details_option_3');
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
configModel.value.onClick = template;
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
emit('close')
|
emit('close')
|
||||||
@ -2431,6 +3207,24 @@ const getDefaultPropsForType = (type) => {
|
|||||||
},
|
},
|
||||||
paragraph: {
|
paragraph: {
|
||||||
value: 'Enter some descriptive text here.'
|
value: 'Enter some descriptive text here.'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
type: 'button',
|
||||||
|
buttonType: 'button',
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
showLabel: false,
|
||||||
|
showButtonText: true,
|
||||||
|
buttonText: '',
|
||||||
|
icon: '',
|
||||||
|
linkType: 'none', // 'none', 'url', 'process'
|
||||||
|
linkUrl: '',
|
||||||
|
linkProcessId: '',
|
||||||
|
linkTarget: '_self', // '_self', '_blank'
|
||||||
|
iframeDebug: false, // Show/hide debug UI
|
||||||
|
iframeHideComplete: false, // Hide completion message
|
||||||
|
iframeTheme: '', // Custom theme
|
||||||
|
iframeCustomParams: '' // Additional URL parameters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +256,8 @@ const categories = [
|
|||||||
{ id: 'onboarding', name: 'Onboarding' },
|
{ id: 'onboarding', name: 'Onboarding' },
|
||||||
{ id: 'support', name: 'Support & Service' },
|
{ id: 'support', name: 'Support & Service' },
|
||||||
{ id: 'business', name: 'Business Operations' },
|
{ id: 'business', name: 'Business Operations' },
|
||||||
{ id: 'automation', name: 'Automation' }
|
{ id: 'automation', name: 'Automation' },
|
||||||
|
{ id: 'data-driven', name: 'Data-Driven Workflows' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Active category
|
// Active category
|
||||||
@ -1207,6 +1208,832 @@ const templates = [
|
|||||||
{ id: 'e40', source: 'form-10', target: 'notification-11', sourceHandle: 'form-10-bottom', targetHandle: 'notification-11-top', type: 'smoothstep' },
|
{ id: 'e40', source: 'form-10', target: 'notification-11', sourceHandle: 'form-10-bottom', targetHandle: 'notification-11-top', type: 'smoothstep' },
|
||||||
{ id: 'e41', source: 'notification-11', target: 'end-6', sourceHandle: 'notification-11-bottom', targetHandle: 'end-6-top', type: 'smoothstep' }
|
{ id: 'e41', source: 'notification-11', target: 'end-6', sourceHandle: 'notification-11-bottom', targetHandle: 'end-6-top', type: 'smoothstep' }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'url-data-capture',
|
||||||
|
name: 'URL Data Capture & Form Pre-fill',
|
||||||
|
description: 'Capture user ID from URL parameters, fetch user data via API, and pre-fill a form for editing.',
|
||||||
|
category: 'data-driven',
|
||||||
|
complexity: 'Simple',
|
||||||
|
nodeCount: 4,
|
||||||
|
edgeCount: 3,
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
name: 'userId',
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'User ID captured from URL parameter',
|
||||||
|
defaultValue: '',
|
||||||
|
captureFromUrl: true,
|
||||||
|
urlParameterName: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userData',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'User data retrieved from API',
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'formSubmissionResult',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Result of form submission',
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'start-url-1',
|
||||||
|
type: 'start',
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
data: {
|
||||||
|
label: 'Start',
|
||||||
|
description: 'Begin URL data capture process',
|
||||||
|
backgroundColor: '#dcfce7',
|
||||||
|
borderColor: '#10b981',
|
||||||
|
textColor: '#065f46',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-fetch-user',
|
||||||
|
type: 'api',
|
||||||
|
position: { x: 300, y: 100 },
|
||||||
|
data: {
|
||||||
|
label: 'Fetch User Data',
|
||||||
|
description: 'Retrieve user information from API',
|
||||||
|
apiMethod: 'GET',
|
||||||
|
apiUrl: '/api/users/{userId}',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Content-Type', value: 'application/json' },
|
||||||
|
{ key: 'Authorization', value: 'Bearer YOUR_API_TOKEN' }
|
||||||
|
],
|
||||||
|
outputVariable: 'userData',
|
||||||
|
errorVariable: 'apiError',
|
||||||
|
continueOnError: false,
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
textColor: '#1e40af',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'form-edit-user',
|
||||||
|
type: 'form',
|
||||||
|
position: { x: 500, y: 100 },
|
||||||
|
data: {
|
||||||
|
label: 'Edit User Information',
|
||||||
|
description: 'Form pre-filled with user data for editing',
|
||||||
|
formId: null,
|
||||||
|
formName: 'User Information Form',
|
||||||
|
inputMappings: [
|
||||||
|
{ processVariable: 'userData.name', formField: 'fullName' },
|
||||||
|
{ processVariable: 'userData.email', formField: 'email' },
|
||||||
|
{ processVariable: 'userData.phone', formField: 'phoneNumber' }
|
||||||
|
],
|
||||||
|
outputMappings: [
|
||||||
|
{ formField: 'fullName', processVariable: 'updatedName' },
|
||||||
|
{ formField: 'email', processVariable: 'updatedEmail' },
|
||||||
|
{ formField: 'phoneNumber', processVariable: 'updatedPhone' }
|
||||||
|
],
|
||||||
|
backgroundColor: '#faf5ff',
|
||||||
|
borderColor: '#9333ea',
|
||||||
|
textColor: '#6b21a8',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'end-url-1',
|
||||||
|
type: 'end',
|
||||||
|
position: { x: 700, y: 100 },
|
||||||
|
data: {
|
||||||
|
label: 'End',
|
||||||
|
description: 'Process completed',
|
||||||
|
backgroundColor: '#fee2e2',
|
||||||
|
borderColor: '#dc2626',
|
||||||
|
textColor: '#991b1b',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ id: 'e-url-1', source: 'start-url-1', target: 'api-fetch-user', sourceHandle: 'start-url-1-bottom', targetHandle: 'api-fetch-user-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-url-2', source: 'api-fetch-user', target: 'form-edit-user', sourceHandle: 'api-fetch-user-bottom', targetHandle: 'form-edit-user-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-url-3', source: 'form-edit-user', target: 'end-url-1', sourceHandle: 'form-edit-user-bottom', targetHandle: 'end-url-1-top', type: 'smoothstep' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dynamic-survey',
|
||||||
|
name: 'Dynamic Survey with Conditional Logic',
|
||||||
|
description: 'Multi-step survey with conditional questions based on URL parameters and previous answers.',
|
||||||
|
category: 'data-driven',
|
||||||
|
complexity: 'Medium',
|
||||||
|
nodeCount: 6,
|
||||||
|
edgeCount: 7,
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
name: 'surveyType',
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Type of survey from URL parameter',
|
||||||
|
defaultValue: 'general',
|
||||||
|
captureFromUrl: true,
|
||||||
|
urlParameterName: 'type'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userRole',
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'User role from URL parameter',
|
||||||
|
defaultValue: 'user',
|
||||||
|
captureFromUrl: true,
|
||||||
|
urlParameterName: 'role'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'basicInfo',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Basic information from first form',
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isManager',
|
||||||
|
type: 'boolean',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Whether user is in management role',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'surveyResults',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Complete survey results',
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'start-survey',
|
||||||
|
type: 'start',
|
||||||
|
position: { x: 100, y: 200 },
|
||||||
|
data: {
|
||||||
|
label: 'Start Survey',
|
||||||
|
description: 'Begin dynamic survey process',
|
||||||
|
backgroundColor: '#dcfce7',
|
||||||
|
borderColor: '#10b981',
|
||||||
|
textColor: '#065f46',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'form-basic-info',
|
||||||
|
type: 'form',
|
||||||
|
position: { x: 300, y: 200 },
|
||||||
|
data: {
|
||||||
|
label: 'Basic Information',
|
||||||
|
description: 'Collect basic user information',
|
||||||
|
formId: null,
|
||||||
|
formName: 'Basic Information Form',
|
||||||
|
outputMappings: [
|
||||||
|
{ formField: 'name', processVariable: 'basicInfo.name' },
|
||||||
|
{ formField: 'department', processVariable: 'basicInfo.department' },
|
||||||
|
{ formField: 'experience', processVariable: 'basicInfo.experience' }
|
||||||
|
],
|
||||||
|
backgroundColor: '#faf5ff',
|
||||||
|
borderColor: '#9333ea',
|
||||||
|
textColor: '#6b21a8',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'business-rule-role-check',
|
||||||
|
type: 'business-rule',
|
||||||
|
position: { x: 500, y: 200 },
|
||||||
|
data: {
|
||||||
|
label: 'Check User Role',
|
||||||
|
description: 'Determine if user is manager based on role parameter',
|
||||||
|
ruleGroups: [
|
||||||
|
{
|
||||||
|
name: 'Manager Check',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
variable: 'userRole',
|
||||||
|
operator: 'eq',
|
||||||
|
value: 'manager',
|
||||||
|
minValue: null,
|
||||||
|
maxValue: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'set_variable',
|
||||||
|
variable: 'isManager',
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
backgroundColor: '#fdf4ff',
|
||||||
|
borderColor: '#a855f7',
|
||||||
|
textColor: '#7c3aed',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gateway-manager-check',
|
||||||
|
type: 'gateway',
|
||||||
|
position: { x: 700, y: 200 },
|
||||||
|
data: {
|
||||||
|
label: 'Is Manager?',
|
||||||
|
description: 'Route based on manager status',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
variable: 'isManager',
|
||||||
|
operator: 'eq',
|
||||||
|
value: 'true',
|
||||||
|
output: 'Manager Path'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultPath: 'Employee Path',
|
||||||
|
backgroundColor: '#fff7ed',
|
||||||
|
borderColor: '#f97316',
|
||||||
|
textColor: '#c2410c',
|
||||||
|
shape: 'diamond'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'form-manager-questions',
|
||||||
|
type: 'form',
|
||||||
|
position: { x: 900, y: 150 },
|
||||||
|
data: {
|
||||||
|
label: 'Manager Questions',
|
||||||
|
description: 'Additional questions for managers',
|
||||||
|
formId: null,
|
||||||
|
formName: 'Manager Survey Form',
|
||||||
|
outputMappings: [
|
||||||
|
{ formField: 'teamSize', processVariable: 'surveyResults.teamSize' },
|
||||||
|
{ formField: 'managementStyle', processVariable: 'surveyResults.managementStyle' },
|
||||||
|
{ formField: 'budgetResponsibility', processVariable: 'surveyResults.budgetResponsibility' }
|
||||||
|
],
|
||||||
|
backgroundColor: '#faf5ff',
|
||||||
|
borderColor: '#9333ea',
|
||||||
|
textColor: '#6b21a8',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'form-employee-questions',
|
||||||
|
type: 'form',
|
||||||
|
position: { x: 900, y: 250 },
|
||||||
|
data: {
|
||||||
|
label: 'Employee Questions',
|
||||||
|
description: 'Questions for regular employees',
|
||||||
|
formId: null,
|
||||||
|
formName: 'Employee Survey Form',
|
||||||
|
outputMappings: [
|
||||||
|
{ formField: 'jobSatisfaction', processVariable: 'surveyResults.satisfaction' },
|
||||||
|
{ formField: 'workloadRating', processVariable: 'surveyResults.workload' },
|
||||||
|
{ formField: 'careerGoals', processVariable: 'surveyResults.goals' }
|
||||||
|
],
|
||||||
|
backgroundColor: '#faf5ff',
|
||||||
|
borderColor: '#9333ea',
|
||||||
|
textColor: '#6b21a8',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'end-survey',
|
||||||
|
type: 'end',
|
||||||
|
position: { x: 1100, y: 200 },
|
||||||
|
data: {
|
||||||
|
label: 'Survey Complete',
|
||||||
|
description: 'Survey completed successfully',
|
||||||
|
backgroundColor: '#fee2e2',
|
||||||
|
borderColor: '#dc2626',
|
||||||
|
textColor: '#991b1b',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ id: 'e-survey-1', source: 'start-survey', target: 'form-basic-info', sourceHandle: 'start-survey-bottom', targetHandle: 'form-basic-info-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-survey-2', source: 'form-basic-info', target: 'business-rule-role-check', sourceHandle: 'form-basic-info-bottom', targetHandle: 'business-rule-role-check-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-survey-3', source: 'business-rule-role-check', target: 'gateway-manager-check', sourceHandle: 'business-rule-role-check-bottom', targetHandle: 'gateway-manager-check-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-survey-4', source: 'gateway-manager-check', target: 'form-manager-questions', sourceHandle: 'gateway-manager-check-right', targetHandle: 'form-manager-questions-left', type: 'smoothstep', label: 'Manager Path' },
|
||||||
|
{ id: 'e-survey-5', source: 'gateway-manager-check', target: 'form-employee-questions', sourceHandle: 'gateway-manager-check-bottom', targetHandle: 'form-employee-questions-left', type: 'smoothstep', label: 'Employee Path' },
|
||||||
|
{ id: 'e-survey-6', source: 'form-manager-questions', target: 'end-survey', sourceHandle: 'form-manager-questions-bottom', targetHandle: 'end-survey-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-survey-7', source: 'form-employee-questions', target: 'end-survey', sourceHandle: 'form-employee-questions-bottom', targetHandle: 'end-survey-left', type: 'smoothstep' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'order-processing',
|
||||||
|
name: 'Order Processing with Status Updates',
|
||||||
|
description: 'Process orders with automatic status updates and notifications based on order ID from URL.',
|
||||||
|
category: 'business',
|
||||||
|
complexity: 'Medium',
|
||||||
|
nodeCount: 7,
|
||||||
|
edgeCount: 8,
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
name: 'orderId',
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Order ID from URL parameter',
|
||||||
|
defaultValue: '',
|
||||||
|
captureFromUrl: true,
|
||||||
|
urlParameterName: 'orderId'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'priority',
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Order priority from URL parameter',
|
||||||
|
defaultValue: 'normal',
|
||||||
|
captureFromUrl: true,
|
||||||
|
urlParameterName: 'priority'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'orderData',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Order details from API',
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isHighPriority',
|
||||||
|
type: 'boolean',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Whether order is high priority',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'processingResult',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Result of order processing',
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'start-order',
|
||||||
|
type: 'start',
|
||||||
|
position: { x: 100, y: 300 },
|
||||||
|
data: {
|
||||||
|
label: 'Start Processing',
|
||||||
|
description: 'Begin order processing',
|
||||||
|
backgroundColor: '#dcfce7',
|
||||||
|
borderColor: '#10b981',
|
||||||
|
textColor: '#065f46',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-get-order',
|
||||||
|
type: 'api',
|
||||||
|
position: { x: 300, y: 300 },
|
||||||
|
data: {
|
||||||
|
label: 'Fetch Order',
|
||||||
|
description: 'Get order details from system',
|
||||||
|
apiMethod: 'GET',
|
||||||
|
apiUrl: '/api/orders/{orderId}',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Content-Type', value: 'application/json' },
|
||||||
|
{ key: 'Authorization', value: 'Bearer YOUR_API_TOKEN' }
|
||||||
|
],
|
||||||
|
outputVariable: 'orderData',
|
||||||
|
errorVariable: 'orderError',
|
||||||
|
continueOnError: false,
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
textColor: '#1e40af',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'business-rule-priority',
|
||||||
|
type: 'business-rule',
|
||||||
|
position: { x: 500, y: 300 },
|
||||||
|
data: {
|
||||||
|
label: 'Check Priority',
|
||||||
|
description: 'Determine processing priority',
|
||||||
|
ruleGroups: [
|
||||||
|
{
|
||||||
|
name: 'High Priority Check',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
variable: 'priority',
|
||||||
|
operator: 'eq',
|
||||||
|
value: 'high',
|
||||||
|
minValue: null,
|
||||||
|
maxValue: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'set_variable',
|
||||||
|
variable: 'isHighPriority',
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
backgroundColor: '#fdf4ff',
|
||||||
|
borderColor: '#a855f7',
|
||||||
|
textColor: '#7c3aed',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gateway-priority-check',
|
||||||
|
type: 'gateway',
|
||||||
|
position: { x: 700, y: 300 },
|
||||||
|
data: {
|
||||||
|
label: 'High Priority?',
|
||||||
|
description: 'Route based on priority',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
variable: 'isHighPriority',
|
||||||
|
operator: 'eq',
|
||||||
|
value: 'true',
|
||||||
|
output: 'Express Processing'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultPath: 'Standard Processing',
|
||||||
|
backgroundColor: '#fff7ed',
|
||||||
|
borderColor: '#f97316',
|
||||||
|
textColor: '#c2410c',
|
||||||
|
shape: 'diamond'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-express-process',
|
||||||
|
type: 'api',
|
||||||
|
position: { x: 900, y: 250 },
|
||||||
|
data: {
|
||||||
|
label: 'Express Processing',
|
||||||
|
description: 'Process high priority order',
|
||||||
|
apiMethod: 'POST',
|
||||||
|
apiUrl: '/api/orders/{orderId}/process-express',
|
||||||
|
requestBody: '{"orderId": "{orderId}", "priority": "high"}',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Content-Type', value: 'application/json' }
|
||||||
|
],
|
||||||
|
outputVariable: 'processingResult',
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
textColor: '#1e40af',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-standard-process',
|
||||||
|
type: 'api',
|
||||||
|
position: { x: 900, y: 350 },
|
||||||
|
data: {
|
||||||
|
label: 'Standard Processing',
|
||||||
|
description: 'Process regular order',
|
||||||
|
apiMethod: 'POST',
|
||||||
|
apiUrl: '/api/orders/{orderId}/process-standard',
|
||||||
|
requestBody: '{"orderId": "{orderId}", "priority": "normal"}',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Content-Type', value: 'application/json' }
|
||||||
|
],
|
||||||
|
outputVariable: 'processingResult',
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
textColor: '#1e40af',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-complete',
|
||||||
|
type: 'notification',
|
||||||
|
position: { x: 1100, y: 300 },
|
||||||
|
data: {
|
||||||
|
label: 'Notify Completion',
|
||||||
|
description: 'Send processing complete notification',
|
||||||
|
notificationType: 'success',
|
||||||
|
recipientType: 'email',
|
||||||
|
recipientEmail: '{orderData.customerEmail}',
|
||||||
|
subject: 'Order Processing Complete',
|
||||||
|
message: 'Your order {orderId} has been processed successfully.',
|
||||||
|
priority: 'medium',
|
||||||
|
backgroundColor: '#f0f9ff',
|
||||||
|
borderColor: '#0ea5e9',
|
||||||
|
textColor: '#0284c7',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'end-order',
|
||||||
|
type: 'end',
|
||||||
|
position: { x: 1300, y: 300 },
|
||||||
|
data: {
|
||||||
|
label: 'Complete',
|
||||||
|
description: 'Order processing complete',
|
||||||
|
backgroundColor: '#fee2e2',
|
||||||
|
borderColor: '#dc2626',
|
||||||
|
textColor: '#991b1b',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ id: 'e-order-1', source: 'start-order', target: 'api-get-order', sourceHandle: 'start-order-bottom', targetHandle: 'api-get-order-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-order-2', source: 'api-get-order', target: 'business-rule-priority', sourceHandle: 'api-get-order-bottom', targetHandle: 'business-rule-priority-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-order-3', source: 'business-rule-priority', target: 'gateway-priority-check', sourceHandle: 'business-rule-priority-bottom', targetHandle: 'gateway-priority-check-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-order-4', source: 'gateway-priority-check', target: 'api-express-process', sourceHandle: 'gateway-priority-check-right', targetHandle: 'api-express-process-left', type: 'smoothstep', label: 'Express Processing' },
|
||||||
|
{ id: 'e-order-5', source: 'gateway-priority-check', target: 'api-standard-process', sourceHandle: 'gateway-priority-check-bottom', targetHandle: 'api-standard-process-left', type: 'smoothstep', label: 'Standard Processing' },
|
||||||
|
{ id: 'e-order-6', source: 'api-express-process', target: 'notification-complete', sourceHandle: 'api-express-process-bottom', targetHandle: 'notification-complete-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-order-7', source: 'api-standard-process', target: 'notification-complete', sourceHandle: 'api-standard-process-bottom', targetHandle: 'notification-complete-left', type: 'smoothstep' },
|
||||||
|
{ id: 'e-order-8', source: 'notification-complete', target: 'end-order', sourceHandle: 'notification-complete-bottom', targetHandle: 'end-order-top', type: 'smoothstep' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'customer-feedback-analysis',
|
||||||
|
name: 'Customer Feedback Analysis',
|
||||||
|
description: 'Automated feedback collection and analysis with sentiment detection and response routing.',
|
||||||
|
category: 'automation',
|
||||||
|
complexity: 'Complex',
|
||||||
|
nodeCount: 8,
|
||||||
|
edgeCount: 10,
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
name: 'customerId',
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Customer ID from URL parameter',
|
||||||
|
defaultValue: '',
|
||||||
|
captureFromUrl: true,
|
||||||
|
urlParameterName: 'customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feedbackType',
|
||||||
|
type: 'string',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Type of feedback from URL parameter',
|
||||||
|
defaultValue: 'general',
|
||||||
|
captureFromUrl: true,
|
||||||
|
urlParameterName: 'type'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customerData',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Customer information from API',
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feedbackData',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Feedback form submission data',
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sentimentAnalysis',
|
||||||
|
type: 'object',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'AI sentiment analysis results',
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isNegativeFeedback',
|
||||||
|
type: 'boolean',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Whether feedback is negative',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'escalationRequired',
|
||||||
|
type: 'boolean',
|
||||||
|
scope: 'global',
|
||||||
|
description: 'Whether escalation is needed',
|
||||||
|
defaultValue: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'start-feedback',
|
||||||
|
type: 'start',
|
||||||
|
position: { x: 100, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: 'Start Feedback',
|
||||||
|
description: 'Begin feedback collection process',
|
||||||
|
backgroundColor: '#dcfce7',
|
||||||
|
borderColor: '#10b981',
|
||||||
|
textColor: '#065f46',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-get-customer',
|
||||||
|
type: 'api',
|
||||||
|
position: { x: 300, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: 'Get Customer Info',
|
||||||
|
description: 'Fetch customer details',
|
||||||
|
apiMethod: 'GET',
|
||||||
|
apiUrl: '/api/customers/{customerId}',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Content-Type', value: 'application/json' },
|
||||||
|
{ key: 'Authorization', value: 'Bearer YOUR_API_TOKEN' }
|
||||||
|
],
|
||||||
|
outputVariable: 'customerData',
|
||||||
|
errorVariable: 'customerError',
|
||||||
|
continueOnError: false,
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
textColor: '#1e40af',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'form-feedback',
|
||||||
|
type: 'form',
|
||||||
|
position: { x: 500, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: 'Feedback Form',
|
||||||
|
description: 'Collect customer feedback',
|
||||||
|
formId: null,
|
||||||
|
formName: 'Customer Feedback Form',
|
||||||
|
inputMappings: [
|
||||||
|
{ processVariable: 'customerData.name', formField: 'customerName' },
|
||||||
|
{ processVariable: 'customerData.email', formField: 'customerEmail' },
|
||||||
|
{ processVariable: 'feedbackType', formField: 'feedbackCategory' }
|
||||||
|
],
|
||||||
|
outputMappings: [
|
||||||
|
{ formField: 'rating', processVariable: 'feedbackData.rating' },
|
||||||
|
{ formField: 'comments', processVariable: 'feedbackData.comments' },
|
||||||
|
{ formField: 'improvementSuggestions', processVariable: 'feedbackData.suggestions' }
|
||||||
|
],
|
||||||
|
backgroundColor: '#faf5ff',
|
||||||
|
borderColor: '#9333ea',
|
||||||
|
textColor: '#6b21a8',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-sentiment-analysis',
|
||||||
|
type: 'api',
|
||||||
|
position: { x: 700, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: 'Analyze Sentiment',
|
||||||
|
description: 'AI-powered sentiment analysis',
|
||||||
|
apiMethod: 'POST',
|
||||||
|
apiUrl: '/api/ai/sentiment-analysis',
|
||||||
|
requestBody: '{"text": "{feedbackData.comments}", "context": "customer_feedback"}',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Content-Type', value: 'application/json' },
|
||||||
|
{ key: 'X-API-Key', value: 'YOUR_AI_API_KEY' }
|
||||||
|
],
|
||||||
|
outputVariable: 'sentimentAnalysis',
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
textColor: '#1e40af',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'business-rule-sentiment',
|
||||||
|
type: 'business-rule',
|
||||||
|
position: { x: 900, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: 'Evaluate Sentiment',
|
||||||
|
description: 'Determine if feedback requires escalation',
|
||||||
|
ruleGroups: [
|
||||||
|
{
|
||||||
|
name: 'Negative Sentiment',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
variable: 'sentimentAnalysis.sentiment',
|
||||||
|
operator: 'eq',
|
||||||
|
value: 'negative',
|
||||||
|
minValue: null,
|
||||||
|
maxValue: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'set_variable',
|
||||||
|
variable: 'isNegativeFeedback',
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Low Rating',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
variable: 'feedbackData.rating',
|
||||||
|
operator: 'lte',
|
||||||
|
value: '2',
|
||||||
|
minValue: null,
|
||||||
|
maxValue: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'set_variable',
|
||||||
|
variable: 'escalationRequired',
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
backgroundColor: '#fdf4ff',
|
||||||
|
borderColor: '#a855f7',
|
||||||
|
textColor: '#7c3aed',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gateway-escalation',
|
||||||
|
type: 'gateway',
|
||||||
|
position: { x: 1100, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: 'Needs Escalation?',
|
||||||
|
description: 'Check if escalation is required',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
variable: 'escalationRequired',
|
||||||
|
operator: 'eq',
|
||||||
|
value: 'true',
|
||||||
|
output: 'Escalate'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultPath: 'Standard Response',
|
||||||
|
backgroundColor: '#fff7ed',
|
||||||
|
borderColor: '#f97316',
|
||||||
|
textColor: '#c2410c',
|
||||||
|
shape: 'diamond'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-escalate',
|
||||||
|
type: 'notification',
|
||||||
|
position: { x: 1300, y: 350 },
|
||||||
|
data: {
|
||||||
|
label: 'Escalate to Manager',
|
||||||
|
description: 'Notify manager of negative feedback',
|
||||||
|
notificationType: 'warning',
|
||||||
|
recipientType: 'role',
|
||||||
|
recipientRole: 'customer_success_manager',
|
||||||
|
subject: 'Urgent: Negative Customer Feedback',
|
||||||
|
message: 'Customer {customerData.name} has provided negative feedback requiring immediate attention.',
|
||||||
|
priority: 'high',
|
||||||
|
backgroundColor: '#f0f9ff',
|
||||||
|
borderColor: '#0ea5e9',
|
||||||
|
textColor: '#0284c7',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-standard',
|
||||||
|
type: 'notification',
|
||||||
|
position: { x: 1300, y: 450 },
|
||||||
|
data: {
|
||||||
|
label: 'Thank Customer',
|
||||||
|
description: 'Send standard thank you message',
|
||||||
|
notificationType: 'success',
|
||||||
|
recipientType: 'variable',
|
||||||
|
recipientVariable: 'customerData.email',
|
||||||
|
subject: 'Thank you for your feedback',
|
||||||
|
message: 'Thank you for taking the time to provide feedback. We appreciate your input.',
|
||||||
|
priority: 'medium',
|
||||||
|
backgroundColor: '#f0f9ff',
|
||||||
|
borderColor: '#0ea5e9',
|
||||||
|
textColor: '#0284c7',
|
||||||
|
shape: 'rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'end-feedback',
|
||||||
|
type: 'end',
|
||||||
|
position: { x: 1500, y: 400 },
|
||||||
|
data: {
|
||||||
|
label: 'Complete',
|
||||||
|
description: 'Feedback processing complete',
|
||||||
|
backgroundColor: '#fee2e2',
|
||||||
|
borderColor: '#dc2626',
|
||||||
|
textColor: '#991b1b',
|
||||||
|
shape: 'circle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ id: 'e-feedback-1', source: 'start-feedback', target: 'api-get-customer', sourceHandle: 'start-feedback-bottom', targetHandle: 'api-get-customer-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-feedback-2', source: 'api-get-customer', target: 'form-feedback', sourceHandle: 'api-get-customer-bottom', targetHandle: 'form-feedback-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-feedback-3', source: 'form-feedback', target: 'api-sentiment-analysis', sourceHandle: 'form-feedback-bottom', targetHandle: 'api-sentiment-analysis-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-feedback-4', source: 'api-sentiment-analysis', target: 'business-rule-sentiment', sourceHandle: 'api-sentiment-analysis-bottom', targetHandle: 'business-rule-sentiment-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-feedback-5', source: 'business-rule-sentiment', target: 'gateway-escalation', sourceHandle: 'business-rule-sentiment-bottom', targetHandle: 'gateway-escalation-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-feedback-6', source: 'gateway-escalation', target: 'notification-escalate', sourceHandle: 'gateway-escalation-right', targetHandle: 'notification-escalate-left', type: 'smoothstep', label: 'Escalate' },
|
||||||
|
{ id: 'e-feedback-7', source: 'gateway-escalation', target: 'notification-standard', sourceHandle: 'gateway-escalation-bottom', targetHandle: 'notification-standard-left', type: 'smoothstep', label: 'Standard Response' },
|
||||||
|
{ id: 'e-feedback-8', source: 'notification-escalate', target: 'end-feedback', sourceHandle: 'notification-escalate-bottom', targetHandle: 'end-feedback-top', type: 'smoothstep' },
|
||||||
|
{ id: 'e-feedback-9', source: 'notification-standard', target: 'end-feedback', sourceHandle: 'notification-standard-bottom', targetHandle: 'end-feedback-left', type: 'smoothstep' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -1246,7 +2073,8 @@ const getTemplateIcon = (template) => {
|
|||||||
'onboarding': 'material-symbols:person-add-outline',
|
'onboarding': 'material-symbols:person-add-outline',
|
||||||
'support': 'material-symbols:support-agent',
|
'support': 'material-symbols:support-agent',
|
||||||
'business': 'material-symbols:business-center-outline',
|
'business': 'material-symbols:business-center-outline',
|
||||||
'automation': 'material-symbols:precision-manufacturing-outline'
|
'automation': 'material-symbols:precision-manufacturing-outline',
|
||||||
|
'data-driven': 'material-symbols:data-usage'
|
||||||
};
|
};
|
||||||
|
|
||||||
return icons[template.category] || 'material-symbols:account-tree';
|
return icons[template.category] || 'material-symbols:account-tree';
|
||||||
|
@ -55,16 +55,31 @@
|
|||||||
|
|
||||||
<!-- Data Table -->
|
<!-- Data Table -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
|
<!-- Performance warning for too many columns -->
|
||||||
|
<div v-if="config.columns.length > 20" class="performance-warning">
|
||||||
|
<div class="warning-content">
|
||||||
|
<Icon name="heroicons:information-circle" class="warning-icon" />
|
||||||
|
<div class="warning-text">
|
||||||
|
<p class="warning-title">Performance Notice</p>
|
||||||
|
<p class="warning-message">
|
||||||
|
This table has {{ config.columns.length }} columns. For optimal performance, only the first 20 columns are displayed.
|
||||||
|
Consider reducing the number of columns for better user experience.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="paginatedData.length > 0" class="table-wrapper">
|
<div v-if="paginatedData.length > 0" class="table-wrapper">
|
||||||
<div class="table-content">
|
<div class="table-content" ref="tableContainer">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead class="table-header-row">
|
<thead class="table-header-row">
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="config.showRowNumbers" class="row-number-header">#</th>
|
<th v-if="config.showRowNumbers" class="row-number-header">#</th>
|
||||||
<th
|
<th
|
||||||
v-for="column in config.columns"
|
v-for="column in visibleColumns"
|
||||||
:key="column.name"
|
:key="column.name"
|
||||||
class="column-header"
|
class="column-header"
|
||||||
|
:style="{ width: getColumnWidth(column) }"
|
||||||
>
|
>
|
||||||
<div class="column-header-content">
|
<div class="column-header-content">
|
||||||
<span class="header-text">{{ column.label }}</span>
|
<span class="header-text">{{ column.label }}</span>
|
||||||
@ -77,7 +92,7 @@
|
|||||||
<tbody class="table-body">
|
<tbody class="table-body">
|
||||||
<tr
|
<tr
|
||||||
v-for="(record, index) in paginatedData"
|
v-for="(record, index) in paginatedData"
|
||||||
:key="index"
|
:key="getRecordKey(record, index)"
|
||||||
class="data-row"
|
class="data-row"
|
||||||
:class="{ 'row-hover': !isPreview }"
|
:class="{ 'row-hover': !isPreview }"
|
||||||
>
|
>
|
||||||
@ -85,12 +100,17 @@
|
|||||||
<div class="row-number">{{ getRowNumber(index) }}</div>
|
<div class="row-number">{{ getRowNumber(index) }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
v-for="column in config.columns"
|
v-for="column in visibleColumns"
|
||||||
:key="column.name"
|
:key="column.name"
|
||||||
class="data-cell"
|
class="data-cell"
|
||||||
|
:style="{ width: getColumnWidth(column) }"
|
||||||
>
|
>
|
||||||
<div class="cell-content">
|
<div class="cell-content">
|
||||||
<span class="cell-value">{{ formatCellValue(record[column.name], column) }}</span>
|
<LazyCellValue
|
||||||
|
:value="record[column.name]"
|
||||||
|
:column="column"
|
||||||
|
:record="record"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showActions" class="actions-cell">
|
<td v-if="showActions" class="actions-cell">
|
||||||
@ -124,20 +144,16 @@
|
|||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty-state">
|
||||||
<div class="empty-content">
|
<div class="empty-content">
|
||||||
<div class="empty-icon-container">
|
<Icon name="heroicons:table-cells" class="empty-icon" />
|
||||||
<Icon name="heroicons:table-cells" class="empty-icon" />
|
<h3 class="empty-title">No records yet</h3>
|
||||||
</div>
|
<p class="empty-message">
|
||||||
<div class="empty-text">
|
{{ config.help || 'Add your first record to get started.' }}
|
||||||
<h4 class="empty-title">No records yet</h4>
|
</p>
|
||||||
<p class="empty-description">
|
|
||||||
{{ searchQuery ? 'No records match your search criteria.' : 'Click "' + (config.buttonText || 'Add Record') + '" to add your first record.' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<RsButton
|
<RsButton
|
||||||
v-if="!searchQuery && !isAddDisabled"
|
v-if="!isAddDisabled"
|
||||||
@click="openAddModal"
|
@click="openAddModal"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="md"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Icon name="heroicons:plus" class="w-4 h-4 mr-2" />
|
<Icon name="heroicons:plus" class="w-4 h-4 mr-2" />
|
||||||
{{ config.buttonText || 'Add Record' }}
|
{{ config.buttonText || 'Add Record' }}
|
||||||
@ -147,92 +163,56 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div v-if="filteredData.length > 0 && totalPages > 1" class="pagination-container">
|
<div v-if="totalPages > 1" class="pagination-container">
|
||||||
<div class="pagination-info">
|
<div class="pagination-info">
|
||||||
<span class="pagination-text">
|
Showing {{ startRecord }} to {{ endRecord }} of {{ filteredData.length }} records
|
||||||
Showing {{ startRecord }} to {{ endRecord }} of {{ filteredData.length }} records
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination-controls">
|
<div class="pagination-controls">
|
||||||
<div class="page-size-selector">
|
<RsButton
|
||||||
<label class="page-size-label">Show:</label>
|
@click="goToPage(currentPage - 1)"
|
||||||
<select v-model="pageSize" @change="currentPage = 1" class="page-size-select">
|
:disabled="currentPage === 1"
|
||||||
<option value="5">5</option>
|
variant="secondary-outline"
|
||||||
<option value="10">10</option>
|
size="sm"
|
||||||
<option value="25">25</option>
|
>
|
||||||
<option value="50">50</option>
|
<Icon name="heroicons:chevron-left" class="w-4 h-4" />
|
||||||
</select>
|
Previous
|
||||||
<span class="page-size-suffix">per page</span>
|
</RsButton>
|
||||||
|
|
||||||
|
<div class="page-numbers">
|
||||||
|
<RsButton
|
||||||
|
v-for="page in visiblePages"
|
||||||
|
:key="page"
|
||||||
|
@click="page === '...' ? null : goToPage(page)"
|
||||||
|
:variant="page === currentPage ? 'primary' : 'secondary-outline'"
|
||||||
|
size="sm"
|
||||||
|
:disabled="page === '...'"
|
||||||
|
class="page-button"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-navigation">
|
<RsButton
|
||||||
<RsButton
|
@click="goToPage(currentPage + 1)"
|
||||||
@click="goToPage(1)"
|
:disabled="currentPage === totalPages"
|
||||||
:disabled="currentPage === 1"
|
variant="secondary-outline"
|
||||||
variant="secondary-outline"
|
size="sm"
|
||||||
size="sm"
|
>
|
||||||
title="First page"
|
Next
|
||||||
>
|
<Icon name="heroicons:chevron-right" class="w-4 h-4" />
|
||||||
<Icon name="heroicons:chevron-double-left" class="w-4 h-4" />
|
</RsButton>
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<RsButton
|
|
||||||
@click="goToPage(currentPage - 1)"
|
|
||||||
:disabled="currentPage === 1"
|
|
||||||
variant="secondary-outline"
|
|
||||||
size="sm"
|
|
||||||
title="Previous page"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:chevron-left" class="w-4 h-4" />
|
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<div class="page-numbers">
|
|
||||||
<RsButton
|
|
||||||
v-for="page in visiblePages"
|
|
||||||
:key="page"
|
|
||||||
@click="goToPage(page)"
|
|
||||||
:variant="page === currentPage ? 'primary' : 'secondary-outline'"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{{ page }}
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RsButton
|
|
||||||
@click="goToPage(currentPage + 1)"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
variant="secondary-outline"
|
|
||||||
size="sm"
|
|
||||||
title="Next page"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:chevron-right" class="w-4 h-4" />
|
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<RsButton
|
|
||||||
@click="goToPage(totalPages)"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
variant="secondary-outline"
|
|
||||||
size="sm"
|
|
||||||
title="Last page"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:chevron-double-right" class="w-4 h-4" />
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add/Edit Modal -->
|
<!-- Add/Edit Modal -->
|
||||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
<div v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||||
<div class="modal-container">
|
<div class="modal-container" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title-section">
|
<h3 class="modal-title">
|
||||||
<h3 class="modal-title">
|
{{ editingIndex !== null ? 'Edit Record' : 'Add Record' }}
|
||||||
{{ editingIndex !== null ? 'Edit Record' : 'Add New Record' }}
|
</h3>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<RsButton @click="closeModal" variant="secondary-text" size="sm">
|
<RsButton @click="closeModal" variant="secondary-text" size="sm">
|
||||||
<Icon name="heroicons:x-mark" class="w-6 h-6" />
|
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -280,23 +260,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div v-if="showDeleteConfirm" class="modal-overlay" @click.self="cancelDelete">
|
<div v-if="showDeleteConfirm" class="modal-overlay" @click="cancelDelete">
|
||||||
<div class="modal-container modal-small">
|
<div class="modal-container delete-modal" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title-section">
|
<h3 class="modal-title">Confirm Delete</h3>
|
||||||
<h3 class="modal-title">Confirm Delete</h3>
|
|
||||||
</div>
|
|
||||||
<RsButton @click="cancelDelete" variant="secondary-text" size="sm">
|
<RsButton @click="cancelDelete" variant="secondary-text" size="sm">
|
||||||
<Icon name="heroicons:x-mark" class="w-6 h-6" />
|
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="delete-warning">
|
<div class="warning-content">
|
||||||
<div class="warning-icon-container">
|
<Icon name="heroicons:exclamation-triangle" class="warning-icon" />
|
||||||
<Icon name="heroicons:exclamation-triangle" class="warning-icon" />
|
<div class="warning-text">
|
||||||
</div>
|
|
||||||
<div class="warning-content">
|
|
||||||
<p class="warning-title">Are you sure you want to delete this record?</p>
|
<p class="warning-title">Are you sure you want to delete this record?</p>
|
||||||
<p class="warning-message">This action cannot be undone.</p>
|
<p class="warning-message">This action cannot be undone.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -320,6 +296,91 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
|
||||||
|
// Lazy cell component for better performance
|
||||||
|
const LazyCellValue = defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String, Number, Boolean, Object],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const cellRef = ref(null)
|
||||||
|
|
||||||
|
const observer = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (cellRef.value) {
|
||||||
|
observer.value = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
isVisible.value = true
|
||||||
|
observer.value?.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
)
|
||||||
|
observer.value.observe(cellRef.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
observer.value?.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatValue = computed(() => {
|
||||||
|
if (!isVisible.value) return ''
|
||||||
|
|
||||||
|
const value = props.value
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'date') {
|
||||||
|
return new Date(value).toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'time') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'checkbox') {
|
||||||
|
return value ? 'Yes' : 'No'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'select' && props.column.options) {
|
||||||
|
const option = props.column.options.find(opt => opt.value === value)
|
||||||
|
return option ? option.label : value
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVisible,
|
||||||
|
cellRef,
|
||||||
|
formatValue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<span ref="cellRef" class="cell-value">
|
||||||
|
{{ formatValue }}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
config: {
|
config: {
|
||||||
@ -347,12 +408,28 @@ const editingIndex = ref(null)
|
|||||||
const deleteIndex = ref(null)
|
const deleteIndex = ref(null)
|
||||||
const formData = ref({})
|
const formData = ref({})
|
||||||
const formId = ref(`table-form-${uuidv4()}`)
|
const formId = ref(`table-form-${uuidv4()}`)
|
||||||
|
const tableContainer = ref(null)
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
|
||||||
// Computed properties
|
// Performance optimizations
|
||||||
|
const columnCache = ref(new Map())
|
||||||
|
const recordKeys = ref(new Map())
|
||||||
|
|
||||||
|
// Debounced search for better performance
|
||||||
|
const debouncedSearch = useDebounceFn((query) => {
|
||||||
|
// Clear cache when search changes
|
||||||
|
columnCache.value.clear()
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
// Watch search query with debouncing
|
||||||
|
watch(searchQuery, (newQuery) => {
|
||||||
|
debouncedSearch(newQuery)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed properties with memoization
|
||||||
const filteredData = computed(() => {
|
const filteredData = computed(() => {
|
||||||
if (!searchQuery.value) return data.value
|
if (!searchQuery.value) return data.value
|
||||||
|
|
||||||
@ -365,6 +442,46 @@ const filteredData = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Column virtualization for large datasets
|
||||||
|
const visibleColumns = computed(() => {
|
||||||
|
const columns = props.config.columns
|
||||||
|
const maxColumns = 20 // Limit visible columns for performance
|
||||||
|
|
||||||
|
if (columns.length <= maxColumns) {
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return first 20 columns for performance
|
||||||
|
return columns.slice(0, maxColumns)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Column width calculation
|
||||||
|
const getColumnWidth = (column) => {
|
||||||
|
const baseWidth = 150
|
||||||
|
const typeWidths = {
|
||||||
|
text: 150,
|
||||||
|
number: 100,
|
||||||
|
email: 200,
|
||||||
|
tel: 120,
|
||||||
|
date: 120,
|
||||||
|
time: 100,
|
||||||
|
url: 200,
|
||||||
|
select: 150,
|
||||||
|
checkbox: 80,
|
||||||
|
textarea: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${typeWidths[column.type] || baseWidth}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record key generation for better Vue rendering
|
||||||
|
const getRecordKey = (record, index) => {
|
||||||
|
if (!recordKeys.value.has(record)) {
|
||||||
|
recordKeys.value.set(record, `record-${index}-${Date.now()}`)
|
||||||
|
}
|
||||||
|
return recordKeys.value.get(record)
|
||||||
|
}
|
||||||
|
|
||||||
const isAddDisabled = computed(() => {
|
const isAddDisabled = computed(() => {
|
||||||
return props.config.maxRecords && data.value.length >= props.config.maxRecords
|
return props.config.maxRecords && data.value.length >= props.config.maxRecords
|
||||||
})
|
})
|
||||||
@ -429,6 +546,9 @@ const visiblePages = computed(() => {
|
|||||||
// Watch for external data changes
|
// Watch for external data changes
|
||||||
watch(() => props.modelValue, (newValue) => {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
data.value = [...newValue]
|
data.value = [...newValue]
|
||||||
|
// Clear caches when data changes
|
||||||
|
columnCache.value.clear()
|
||||||
|
recordKeys.value.clear()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// Watch for internal data changes and emit
|
// Watch for internal data changes and emit
|
||||||
@ -512,29 +632,8 @@ const getOriginalIndex = (record) => {
|
|||||||
return data.value.findIndex(item => item === record)
|
return data.value.findIndex(item => item === record)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatCellValue = (value, column) => {
|
const getRowNumber = (index) => {
|
||||||
if (value === null || value === undefined || value === '') {
|
return startRecord.value + index
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'date') {
|
|
||||||
return new Date(value).toLocaleDateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'time') {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'checkbox') {
|
|
||||||
return value ? 'Yes' : 'No'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'select' && column.options) {
|
|
||||||
const option = column.options.find(opt => opt.value === value)
|
|
||||||
return option ? option.label : value
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFormKitType = (columnType) => {
|
const getFormKitType = (columnType) => {
|
||||||
@ -586,17 +685,115 @@ const goToPage = (page) => {
|
|||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRowNumber = (index) => {
|
// Cleanup on unmount
|
||||||
return (currentPage.value - 1) * pageSize.value + index + 1
|
onUnmounted(() => {
|
||||||
}
|
columnCache.value.clear()
|
||||||
|
recordKeys.value.clear()
|
||||||
// Watch for search changes to reset pagination
|
|
||||||
watch(searchQuery, () => {
|
|
||||||
currentPage.value = 1
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Performance optimizations */
|
||||||
|
.table-content {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header,
|
||||||
|
.data-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-content {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-value {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Virtual scrolling optimizations */
|
||||||
|
.data-row {
|
||||||
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lazy loading optimizations */
|
||||||
|
.cell-value:empty {
|
||||||
|
min-height: 1.2em;
|
||||||
|
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance warning styles */
|
||||||
|
.performance-warning {
|
||||||
|
@apply bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-content {
|
||||||
|
@apply flex items-start space-x-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
@apply w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-title {
|
||||||
|
@apply text-sm font-medium text-yellow-800 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-message {
|
||||||
|
@apply text-xs text-yellow-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive optimizations */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.table-content {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header,
|
||||||
|
.data-cell {
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.table-content {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header,
|
||||||
|
.data-cell {
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.repeating-table-container {
|
.repeating-table-container {
|
||||||
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
|
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -172,6 +172,7 @@
|
|||||||
v-model="localNodeData.outputVariable"
|
v-model="localNodeData.outputVariable"
|
||||||
:availableVariables="availableVariables"
|
:availableVariables="availableVariables"
|
||||||
:allowCreate="true"
|
:allowCreate="true"
|
||||||
|
:allowObjectPath="false"
|
||||||
@change="saveChanges"
|
@change="saveChanges"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -188,6 +189,7 @@
|
|||||||
v-model="localNodeData.errorVariable"
|
v-model="localNodeData.errorVariable"
|
||||||
:availableVariables="availableVariables"
|
:availableVariables="availableVariables"
|
||||||
:allowCreate="true"
|
:allowCreate="true"
|
||||||
|
:allowObjectPath="false"
|
||||||
@change="saveChanges"
|
@change="saveChanges"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
:value="modelValue"
|
:value="baseVariableName"
|
||||||
@change="handleVariableSelect"
|
@change="handleVariableSelect"
|
||||||
:class="[
|
:class="[
|
||||||
'form-select w-full',
|
'form-select w-full',
|
||||||
@ -47,6 +47,63 @@
|
|||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Object Property Path Input (for object/array variables) -->
|
||||||
|
<div v-if="selectedVariable && isObjectType(selectedVariable.type) && allowObjectPath" class="mt-2">
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">
|
||||||
|
Object Property Path (optional)
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div class="flex-1 relative">
|
||||||
|
<div class="flex items-center border rounded-md focus-within:ring-2 focus-within:ring-purple-500 focus-within:border-purple-500" :class="propertyPathError ? 'border-red-500' : 'border-gray-300'">
|
||||||
|
<span class="px-3 py-2 text-sm text-gray-600 bg-gray-50 border-r border-gray-300 rounded-l-md">
|
||||||
|
{{ baseVariableName }}.
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="propertyPath"
|
||||||
|
type="text"
|
||||||
|
placeholder="data.user.name or [0].title"
|
||||||
|
class="flex-1 px-3 py-2 text-sm border-0 rounded-r-md focus:outline-none focus:ring-0"
|
||||||
|
@input="handlePropertyPathChange"
|
||||||
|
@blur="validatePropertyPath"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="propertyPath"
|
||||||
|
@click="clearPropertyPath"
|
||||||
|
class="p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
title="Clear property path"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:close" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Property Path Examples -->
|
||||||
|
<div class="mt-1 text-xs text-gray-500">
|
||||||
|
<details class="cursor-pointer">
|
||||||
|
<summary class="hover:text-gray-700">Examples</summary>
|
||||||
|
<div class="mt-1 space-y-1 pl-2 border-l-2 border-gray-200">
|
||||||
|
<div><code class="bg-gray-100 px-1">data.user.name</code> - Access nested object property</div>
|
||||||
|
<div><code class="bg-gray-100 px-1">items[0].title</code> - Access first array item property</div>
|
||||||
|
<div><code class="bg-gray-100 px-1">results.users[2].email</code> - Complex nested access</div>
|
||||||
|
<div><code class="bg-gray-100 px-1">response.data.attributes.value</code> - Deep nesting</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Property Path Error -->
|
||||||
|
<div v-if="propertyPathError" class="mt-1 text-red-600 text-xs flex items-center">
|
||||||
|
<Icon name="material-symbols:error" class="w-3 h-3 mr-1" />
|
||||||
|
{{ propertyPathError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Final Variable Path Preview -->
|
||||||
|
<div v-if="finalVariablePath && finalVariablePath !== baseVariableName" class="mt-2 p-2 bg-blue-50 border border-blue-200 rounded-md">
|
||||||
|
<div class="text-xs font-medium text-blue-800 mb-1">Full Variable Path:</div>
|
||||||
|
<code class="text-sm text-blue-700 bg-blue-100 px-2 py-1 rounded">{{ finalVariablePath }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Variable Info Display -->
|
<!-- Variable Info Display -->
|
||||||
<div v-if="selectedVariable" class="mt-2 p-2 bg-gray-50 rounded-md border text-sm">
|
<div v-if="selectedVariable" class="mt-2 p-2 bg-gray-50 rounded-md border text-sm">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@ -224,6 +281,10 @@ const props = defineProps({
|
|||||||
required: {
|
required: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
allowObjectPath: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true // Enable object property path input
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -239,14 +300,32 @@ const newVariableDefaultValue = ref('');
|
|||||||
const newVariableDescription = ref('');
|
const newVariableDescription = ref('');
|
||||||
const nameValidationError = ref('');
|
const nameValidationError = ref('');
|
||||||
|
|
||||||
|
// Object property path state
|
||||||
|
const propertyPath = ref('');
|
||||||
|
const propertyPathError = ref('');
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
|
const baseVariableName = computed(() => {
|
||||||
|
// Extract base variable name from modelValue (strip property path)
|
||||||
|
const value = props.modelValue || '';
|
||||||
|
const dotIndex = value.indexOf('.');
|
||||||
|
return dotIndex > -1 ? value.substring(0, dotIndex) : value;
|
||||||
|
});
|
||||||
|
|
||||||
const selectedVariable = computed(() => {
|
const selectedVariable = computed(() => {
|
||||||
return props.availableVariables.find(v => v.name === props.modelValue);
|
return props.availableVariables.find(v => v.name === baseVariableName.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalVariablePath = computed(() => {
|
||||||
|
if (!baseVariableName.value) return '';
|
||||||
|
if (!propertyPath.value) return baseVariableName.value;
|
||||||
|
return `${baseVariableName.value}.${propertyPath.value}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasError = computed(() => {
|
const hasError = computed(() => {
|
||||||
if (!props.required && !props.modelValue) return false;
|
if (!props.required && !props.modelValue) return false;
|
||||||
if (props.modelValue && !selectedVariable.value) return true;
|
if (props.modelValue && !selectedVariable.value) return true;
|
||||||
|
if (propertyPathError.value) return true;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -255,7 +334,10 @@ const errorMessage = computed(() => {
|
|||||||
return 'Variable selection is required';
|
return 'Variable selection is required';
|
||||||
}
|
}
|
||||||
if (props.modelValue && !selectedVariable.value) {
|
if (props.modelValue && !selectedVariable.value) {
|
||||||
return `Variable "${props.modelValue}" not found`;
|
return `Variable "${baseVariableName.value}" not found`;
|
||||||
|
}
|
||||||
|
if (propertyPathError.value) {
|
||||||
|
return propertyPathError.value;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
@ -305,9 +387,74 @@ const canCreateVariable = computed(() => {
|
|||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleVariableSelect = (event) => {
|
const handleVariableSelect = (event) => {
|
||||||
emit('update:modelValue', event.target.value);
|
const selectedVar = event.target.value;
|
||||||
|
propertyPath.value = ''; // Reset property path when changing variable
|
||||||
|
propertyPathError.value = '';
|
||||||
|
emit('update:modelValue', selectedVar);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePropertyPathChange = () => {
|
||||||
|
propertyPathError.value = ''; // Clear error on input
|
||||||
|
emit('update:modelValue', finalVariablePath.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePropertyPath = () => {
|
||||||
|
if (!propertyPath.value) {
|
||||||
|
propertyPathError.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation for property path syntax
|
||||||
|
const path = propertyPath.value.trim();
|
||||||
|
|
||||||
|
// Check for invalid characters or patterns
|
||||||
|
if (path.includes('..') || path.startsWith('.') || path.endsWith('.')) {
|
||||||
|
propertyPathError.value = 'Invalid property path format';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for balanced brackets
|
||||||
|
const openBrackets = (path.match(/\[/g) || []).length;
|
||||||
|
const closeBrackets = (path.match(/\]/g) || []).length;
|
||||||
|
if (openBrackets !== closeBrackets) {
|
||||||
|
propertyPathError.value = 'Unmatched brackets in property path';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyPathError.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearPropertyPath = () => {
|
||||||
|
propertyPath.value = '';
|
||||||
|
propertyPathError.value = '';
|
||||||
|
emit('update:modelValue', baseVariableName.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isObjectType = (type) => {
|
||||||
|
return ['object', 'array', 'map', 'set'].includes(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for external changes to modelValue to sync property path
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
propertyPath.value = '';
|
||||||
|
propertyPathError.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotIndex = newValue.indexOf('.');
|
||||||
|
if (dotIndex > -1) {
|
||||||
|
// modelValue contains a property path
|
||||||
|
const baseName = newValue.substring(0, dotIndex);
|
||||||
|
const path = newValue.substring(dotIndex + 1);
|
||||||
|
propertyPath.value = path;
|
||||||
|
} else {
|
||||||
|
// No property path in modelValue
|
||||||
|
propertyPath.value = '';
|
||||||
|
}
|
||||||
|
propertyPathError.value = '';
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const openCreateVariable = () => {
|
const openCreateVariable = () => {
|
||||||
showCreateVariable.value = true;
|
showCreateVariable.value = true;
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
|
@ -479,6 +479,28 @@
|
|||||||
help="A clear description helps others understand the purpose of this variable"
|
help="A clear description helps others understand the purpose of this variable"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
name="captureFromUrl"
|
||||||
|
v-model="variableForm.captureFromUrl"
|
||||||
|
type="checkbox"
|
||||||
|
label="Capture from URL Parameters"
|
||||||
|
help="Enable this to automatically capture the variable value from URL query parameters during workflow execution"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
v-if="variableForm.captureFromUrl"
|
||||||
|
name="urlParameterName"
|
||||||
|
v-model="variableForm.urlParameterName"
|
||||||
|
type="text"
|
||||||
|
label="URL Parameter Name"
|
||||||
|
placeholder="Enter the URL parameter name (e.g. 'userId' for ?userId=123)"
|
||||||
|
validation="required"
|
||||||
|
:validation-messages="{
|
||||||
|
required: 'URL parameter name is required when capture from URL is enabled',
|
||||||
|
}"
|
||||||
|
help="The name of the URL query parameter to capture (without the ? or &)"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200">
|
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200">
|
||||||
<RsButton type="button" @click="closeModal" variant="tertiary">
|
<RsButton type="button" @click="closeModal" variant="tertiary">
|
||||||
Cancel
|
Cancel
|
||||||
@ -512,7 +534,9 @@ const variableForm = ref({
|
|||||||
type: "string",
|
type: "string",
|
||||||
scope: "global",
|
scope: "global",
|
||||||
description: "",
|
description: "",
|
||||||
defaultValue: ""
|
defaultValue: "",
|
||||||
|
captureFromUrl: false,
|
||||||
|
urlParameterName: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
// Variable type options with descriptions
|
// Variable type options with descriptions
|
||||||
@ -1498,7 +1522,9 @@ const resetForm = () => {
|
|||||||
type: "string",
|
type: "string",
|
||||||
scope: "global",
|
scope: "global",
|
||||||
description: "",
|
description: "",
|
||||||
defaultValue: ""
|
defaultValue: "",
|
||||||
|
captureFromUrl: false,
|
||||||
|
urlParameterName: ""
|
||||||
};
|
};
|
||||||
editingVariable.value = null;
|
editingVariable.value = null;
|
||||||
};
|
};
|
||||||
@ -1532,7 +1558,9 @@ const saveVariable = async (formData) => {
|
|||||||
type: formData.type,
|
type: formData.type,
|
||||||
scope: "global",
|
scope: "global",
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
value: processedDefaultValue
|
value: processedDefaultValue,
|
||||||
|
captureFromUrl: formData.captureFromUrl || false,
|
||||||
|
urlParameterName: formData.urlParameterName || ""
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingVariable.value) {
|
if (editingVariable.value) {
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
// Utility to safely get a field value from a form data object
|
// Utility to safely get a field value from a form data object
|
||||||
export function safeGetField(field, formData) {
|
export function safeGetField(field, formData) {
|
||||||
if (formData && Object.prototype.hasOwnProperty.call(formData, field)) {
|
if (formData && Object.prototype.hasOwnProperty.call(formData, field)) {
|
||||||
return formData[field] !== undefined && formData[field] !== null ? formData[field] : '';
|
const value = formData[field];
|
||||||
|
// If the value is undefined or null, return empty string for backward compatibility
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
// Only warn in development
|
// Only warn in development
|
||||||
|
@ -398,65 +398,6 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</rs-card>
|
</rs-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Development Roadmap -->
|
|
||||||
<rs-card>
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Icon name="material-symbols:road" class="w-5 h-5 mr-2 text-purple-500" />
|
|
||||||
Development Roadmap & System Guide
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
||||||
<h3 class="font-semibold text-blue-900 mb-2">🚀 Getting Started with Corrad BPM</h3>
|
|
||||||
<p class="text-blue-800 text-sm mb-3">
|
|
||||||
Follow these steps to start developing business processes in the system:
|
|
||||||
</p>
|
|
||||||
<ol class="list-decimal list-inside space-y-1 text-sm text-blue-800">
|
|
||||||
<li><strong>Design Forms:</strong> Use Form Builder to create data collection forms</li>
|
|
||||||
<li><strong>Create Processes:</strong> Use Process Builder to design workflow logic</li>
|
|
||||||
<li><strong>Configure Integrations:</strong> Set up API calls and business rules</li>
|
|
||||||
<li><strong>Test & Deploy:</strong> Test process execution and publish to production</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div
|
|
||||||
v-for="phase in developmentRoadmap"
|
|
||||||
:key="phase.phase"
|
|
||||||
class="border border-gray-200 rounded-lg p-4"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<h4 class="font-semibold text-gray-900">{{ phase.phase }}</h4>
|
|
||||||
<RsBadge :class="getStatusColor(phase.status)" size="sm">
|
|
||||||
{{ phase.status.replace('-', ' ') }}
|
|
||||||
</RsBadge>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-2">
|
|
||||||
<li
|
|
||||||
v-for="item in phase.items"
|
|
||||||
:key="item"
|
|
||||||
class="flex items-start text-sm text-gray-600"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
:name="phase.status === 'completed' ? 'material-symbols:check-circle' :
|
|
||||||
phase.status === 'in-progress' ? 'material-symbols:radio-button-partial' :
|
|
||||||
'material-symbols:radio-button-unchecked'"
|
|
||||||
:class="phase.status === 'completed' ? 'text-green-500' :
|
|
||||||
phase.status === 'in-progress' ? 'text-blue-500' :
|
|
||||||
'text-gray-400'"
|
|
||||||
class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0"
|
|
||||||
/>
|
|
||||||
{{ item }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -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,9 +91,14 @@
|
|||||||
<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" />
|
<!-- Help -->
|
||||||
<span>Manage Forms</span>
|
<div class="px-3 py-1 text-xs font-medium text-gray-500 uppercase tracking-wide mt-2">
|
||||||
|
Help
|
||||||
|
</div>
|
||||||
|
<button @click="showHelpModal = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
||||||
|
<Icon name="material-symbols:help-outline" class="mr-2 w-4 h-4" />
|
||||||
|
<span>How to Use</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,7 +106,7 @@
|
|||||||
</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"
|
||||||
@ -295,16 +300,20 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Only show submit button if no submit button components exist in the form -->
|
<!-- Only show submit button if enabled and no submit button components exist in the form -->
|
||||||
<FormKit
|
<FormKit
|
||||||
v-if="!formStore.formComponents.some(comp => comp.type === 'button' && comp.props.buttonType === 'submit')"
|
v-if="formStore.submitButton.enabled && !formStore.formComponents.some(comp => comp.type === 'button' && comp.props.buttonType === 'submit')"
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Submit"
|
:label="formStore.submitButton.label || 'Submit'"
|
||||||
class="form-submit mt-6"
|
class="form-submit mt-6"
|
||||||
:class="{
|
:class="{
|
||||||
'mx-4 mb-4': selectedDevice !== 'Desktop',
|
'mx-4 mb-4': selectedDevice !== 'Desktop',
|
||||||
'mx-0 mb-0': selectedDevice === 'Desktop'
|
'mx-0 mb-0': selectedDevice === 'Desktop'
|
||||||
}"
|
}"
|
||||||
|
:classes="{
|
||||||
|
input: getSubmitButtonStyles(formStore.submitButton.category, formStore.submitButton.color).classes
|
||||||
|
}"
|
||||||
|
:style="getSubmitButtonStyles(formStore.submitButton.category, formStore.submitButton.color).style"
|
||||||
/>
|
/>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
</div>
|
</div>
|
||||||
@ -504,6 +513,116 @@
|
|||||||
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Rows (for textarea) -->
|
||||||
|
<div v-if="showQuickField('rows')" class="setting-item">
|
||||||
|
<label class="setting-label">Number of Rows</label>
|
||||||
|
<FormKit
|
||||||
|
type="number"
|
||||||
|
v-model="quickSettings.rows"
|
||||||
|
@input="updateQuickSetting('rows', $event)"
|
||||||
|
:placeholder="getPlaceholder('rows')"
|
||||||
|
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link Type (for button) -->
|
||||||
|
<div v-if="showQuickField('linkType')" class="setting-item">
|
||||||
|
<label class="setting-label">Link Type</label>
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
v-model="quickSettings.linkType"
|
||||||
|
@input="updateQuickSetting('linkType', $event)"
|
||||||
|
:options="[
|
||||||
|
{ label: 'No Link', value: 'none' },
|
||||||
|
{ label: 'Custom URL', value: 'url' },
|
||||||
|
{ label: 'Process Link', value: 'process' }
|
||||||
|
]"
|
||||||
|
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link URL (for button) -->
|
||||||
|
<div v-if="showQuickField('linkUrl') && quickSettings.linkType === 'url'" class="setting-item">
|
||||||
|
<label class="setting-label">URL</label>
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
v-model="quickSettings.linkUrl"
|
||||||
|
@input="updateQuickSetting('linkUrl', $event)"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link Process (for button) -->
|
||||||
|
<div v-if="showQuickField('linkProcessId') && quickSettings.linkType === 'process'" class="setting-item">
|
||||||
|
<label class="setting-label">Process</label>
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
v-model="quickSettings.linkProcessId"
|
||||||
|
@input="updateQuickSetting('linkProcessId', $event)"
|
||||||
|
:options="publishedProcesses"
|
||||||
|
placeholder="Select a process..."
|
||||||
|
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Iframe Parameters (for button with process link) -->
|
||||||
|
<div v-if="showQuickField('iframeDebug') && quickSettings.linkType === 'process'" class="space-y-2">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="quickSettings.iframeDebug"
|
||||||
|
@change="updateQuickSetting('iframeDebug', $event.target.checked)"
|
||||||
|
class="toggle-input"
|
||||||
|
/>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-label">Debug Mode (OFF = iframe)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="quickSettings.iframeHideComplete"
|
||||||
|
@change="updateQuickSetting('iframeHideComplete', $event.target.checked)"
|
||||||
|
class="toggle-input"
|
||||||
|
/>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-label">Hide Completion</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-label">Theme</label>
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
v-model="quickSettings.iframeTheme"
|
||||||
|
@input="updateQuickSetting('iframeTheme', $event)"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Default', value: '' },
|
||||||
|
{ label: 'Dark', value: 'dark' },
|
||||||
|
{ label: 'Light', value: 'light' }
|
||||||
|
]"
|
||||||
|
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-label">Custom Params</label>
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
v-model="quickSettings.iframeCustomParams"
|
||||||
|
@input="updateQuickSetting('iframeCustomParams', $event)"
|
||||||
|
placeholder="param1=value1¶m2=value2"
|
||||||
|
:classes="{ outer: 'mb-0', input: 'text-sm' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -549,7 +668,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">
|
||||||
@ -573,9 +748,122 @@
|
|||||||
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>
|
||||||
|
|
||||||
|
<!-- Submit Button Settings Tab -->
|
||||||
|
<template #submit>
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<div class="bg-blue-50 border border-blue-200 text-blue-800 p-3 rounded mb-4">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<Icon name="material-symbols:info" class="w-5 h-5 mr-2 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium text-sm">Submit Button Configuration</h4>
|
||||||
|
<p class="text-xs mt-1">
|
||||||
|
Configure the default submit button that appears at the bottom of your form.
|
||||||
|
Note: If you add a custom submit button component to your form, the default button will be hidden automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enable/Disable Submit Button -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="formStore.submitButton.enabled"
|
||||||
|
class="form-checkbox h-4 w-4 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span class="ml-2 text-sm font-medium text-gray-700">Show default submit button</span>
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-gray-500 ml-6">
|
||||||
|
When enabled, a submit button will appear at the bottom of your form unless you add a custom submit button component.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button Label -->
|
||||||
|
<div v-if="formStore.submitButton.enabled">
|
||||||
|
<FormKit
|
||||||
|
type="text"
|
||||||
|
label="Submit Button Text"
|
||||||
|
v-model="formStore.submitButton.label"
|
||||||
|
help="The text that appears on the submit button"
|
||||||
|
validation="required"
|
||||||
|
placeholder="Submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button Category -->
|
||||||
|
<div v-if="formStore.submitButton.enabled">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Button Style Category"
|
||||||
|
v-model="formStore.submitButton.category"
|
||||||
|
help="Primary buttons are more prominent, secondary are more subtle"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Primary', value: 'primary' },
|
||||||
|
{ label: 'Secondary', value: 'secondary' }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button Color -->
|
||||||
|
<div v-if="formStore.submitButton.enabled">
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Button Color"
|
||||||
|
v-model="formStore.submitButton.color"
|
||||||
|
help="Choose the color theme for the submit button"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Primary', value: 'primary' },
|
||||||
|
{ label: 'Secondary', value: 'secondary' },
|
||||||
|
{ label: 'Success', value: 'success' },
|
||||||
|
{ label: 'Info', value: 'info' },
|
||||||
|
{ label: 'Warning', value: 'warning' },
|
||||||
|
{ label: 'Danger', value: 'danger' }
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Section -->
|
||||||
|
<div v-if="formStore.submitButton.enabled" class="border-t pt-4">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Preview</h4>
|
||||||
|
<div class="bg-gray-50 p-4 rounded border">
|
||||||
|
<FormKit
|
||||||
|
type="submit"
|
||||||
|
:label="formStore.submitButton.label || 'Submit'"
|
||||||
|
:classes="{
|
||||||
|
input: getSubmitButtonStyles(formStore.submitButton.category, formStore.submitButton.color).classes
|
||||||
|
}"
|
||||||
|
:style="getSubmitButtonStyles(formStore.submitButton.category, formStore.submitButton.color).style"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
This is how your submit button will appear in the form.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Custom JavaScript Tab -->
|
<!-- Custom JavaScript Tab -->
|
||||||
<template #javascript>
|
<template #javascript>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
@ -942,6 +1230,206 @@
|
|||||||
@restored="handleFormRestored"
|
@restored="handleFormRestored"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Help Modal -->
|
||||||
|
<RsModal v-model="showHelpModal" title="How to Use Form Builder" size="xl" position="center">
|
||||||
|
<div class="p-6 space-y-6">
|
||||||
|
<!-- Getting Started -->
|
||||||
|
<div class="help-section">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<Icon name="material-symbols:rocket-launch" class="w-5 h-5 mr-2 text-blue-600" />
|
||||||
|
Getting Started
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 text-sm text-gray-700">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-xs font-bold mt-0.5">1</div>
|
||||||
|
<div>
|
||||||
|
<strong>Name Your Form:</strong> Enter a descriptive name for your form at the top of the page.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-xs font-bold mt-0.5">2</div>
|
||||||
|
<div>
|
||||||
|
<strong>Add Components:</strong> Drag and drop components from the left sidebar to the canvas area.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<div class="w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-xs font-bold mt-0.5">3</div>
|
||||||
|
<div>
|
||||||
|
<strong>Configure Fields:</strong> Click on any field to configure its settings in the right panel.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Component Categories -->
|
||||||
|
<div class="help-section">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<Icon name="material-symbols:category" class="w-5 h-5 mr-2 text-green-600" />
|
||||||
|
Component Categories
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium text-gray-800">Basic Inputs</h4>
|
||||||
|
<ul class="text-gray-600 space-y-1">
|
||||||
|
<li>• Text, Number, Email, Password</li>
|
||||||
|
<li>• Text Area, URL, Phone</li>
|
||||||
|
<li>• Date, Time, Date & Time</li>
|
||||||
|
<li>• Color Picker, Range Slider</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium text-gray-800">Selection Inputs</h4>
|
||||||
|
<ul class="text-gray-600 space-y-1">
|
||||||
|
<li>• Dropdown, Radio Buttons</li>
|
||||||
|
<li>• Checkboxes, Switch Toggle</li>
|
||||||
|
<li>• File Upload, Drop Zone</li>
|
||||||
|
<li>• OTP Verification</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium text-gray-800">Layout Elements</h4>
|
||||||
|
<ul class="text-gray-600 space-y-1">
|
||||||
|
<li>• Headings, Paragraphs</li>
|
||||||
|
<li>• Dividers, Form Sections</li>
|
||||||
|
<li>• Information Display</li>
|
||||||
|
<li>• Action Buttons</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h4 class="font-medium text-gray-800">Advanced Features</h4>
|
||||||
|
<ul class="text-gray-600 space-y-1">
|
||||||
|
<li>• Dynamic Lists</li>
|
||||||
|
<li>• Repeating Groups</li>
|
||||||
|
<li>• Conditional Logic</li>
|
||||||
|
<li>• Custom Scripts</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Field Configuration -->
|
||||||
|
<div class="help-section">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<Icon name="material-symbols:settings" class="w-5 h-5 mr-2 text-purple-600" />
|
||||||
|
Field Configuration
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 text-sm text-gray-700">
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg">
|
||||||
|
<h4 class="font-medium text-gray-800 mb-2">Quick Settings (Right Panel)</h4>
|
||||||
|
<ul class="space-y-1 text-gray-600">
|
||||||
|
<li>• <strong>Label:</strong> Field name displayed to users</li>
|
||||||
|
<li>• <strong>Name:</strong> Internal field identifier</li>
|
||||||
|
<li>• <strong>Placeholder:</strong> Hint text in empty fields</li>
|
||||||
|
<li>• <strong>Required:</strong> Make field mandatory</li>
|
||||||
|
<li>• <strong>Width:</strong> Control field size (S, M, L, XL)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg">
|
||||||
|
<h4 class="font-medium text-gray-800 mb-2">Advanced Settings (Settings Icon)</h4>
|
||||||
|
<ul class="space-y-1 text-gray-600">
|
||||||
|
<li>• <strong>Validation:</strong> Add custom validation rules</li>
|
||||||
|
<li>• <strong>Conditional Logic:</strong> Show/hide based on other fields</li>
|
||||||
|
<li>• <strong>Custom Scripts:</strong> Add JavaScript functionality</li>
|
||||||
|
<li>• <strong>Button Links:</strong> Configure process links and iframe parameters</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Button Features -->
|
||||||
|
<div class="help-section">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<Icon name="material-symbols:smart-button" class="w-5 h-5 mr-2 text-orange-600" />
|
||||||
|
Button Features
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 text-sm text-gray-700">
|
||||||
|
<div class="bg-blue-50 p-3 rounded-lg border border-blue-200">
|
||||||
|
<h4 class="font-medium text-blue-800 mb-2">Link Configuration</h4>
|
||||||
|
<ul class="space-y-1 text-blue-700">
|
||||||
|
<li>• <strong>No Link:</strong> Regular button behavior</li>
|
||||||
|
<li>• <strong>Custom URL:</strong> Link to any website</li>
|
||||||
|
<li>• <strong>Process Link:</strong> Link to published workflows</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-50 p-3 rounded-lg border border-green-200">
|
||||||
|
<h4 class="font-medium text-green-800 mb-2">Iframe Integration</h4>
|
||||||
|
<ul class="space-y-1 text-green-700">
|
||||||
|
<li>• <strong>Debug Mode:</strong> Control UI visibility (OFF = iframe mode)</li>
|
||||||
|
<li>• <strong>Hide Completion:</strong> Auto-advance to next step</li>
|
||||||
|
<li>• <strong>Theme:</strong> Apply custom themes (Dark/Light)</li>
|
||||||
|
<li>• <strong>Custom Parameters:</strong> Add additional URL parameters</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tips & Tricks -->
|
||||||
|
<div class="help-section">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<Icon name="material-symbols:lightbulb" class="w-5 h-5 mr-2 text-yellow-600" />
|
||||||
|
Tips & Tricks
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="bg-yellow-50 p-3 rounded-lg border border-yellow-200">
|
||||||
|
<h4 class="font-medium text-yellow-800 mb-1">Keyboard Shortcuts</h4>
|
||||||
|
<ul class="text-yellow-700 space-y-1">
|
||||||
|
<li>• <kbd class="bg-yellow-200 px-1 rounded">Ctrl+S</kbd> Save form</li>
|
||||||
|
<li>• <kbd class="bg-yellow-200 px-1 rounded">Ctrl+Z</kbd> Undo changes</li>
|
||||||
|
<li>• <kbd class="bg-yellow-200 px-1 rounded">Delete</kbd> Remove selected field</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="bg-purple-50 p-3 rounded-lg border border-purple-200">
|
||||||
|
<h4 class="font-medium text-purple-800 mb-1">Best Practices</h4>
|
||||||
|
<ul class="text-purple-700 space-y-1">
|
||||||
|
<li>• Use descriptive field names</li>
|
||||||
|
<li>• Group related fields together</li>
|
||||||
|
<li>• Test your form in preview mode</li>
|
||||||
|
<li>• Save your work frequently</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Mode -->
|
||||||
|
<div class="help-section">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-3 flex items-center">
|
||||||
|
<Icon name="material-symbols:preview" class="w-5 h-5 mr-2 text-indigo-600" />
|
||||||
|
Preview Mode
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 text-sm text-gray-700">
|
||||||
|
<p>Click the <strong>Preview</strong> button to see how your form will look to users. In preview mode:</p>
|
||||||
|
<ul class="space-y-1 text-gray-600">
|
||||||
|
<li>• Test form functionality and validation</li>
|
||||||
|
<li>• Check responsive design on different devices</li>
|
||||||
|
<li>• Verify all field configurations work correctly</li>
|
||||||
|
<li>• Test button links and iframe integrations</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
Need more help? Check our documentation or contact support.
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<RsButton @click="showHelpModal = false" variant="tertiary">
|
||||||
|
Close
|
||||||
|
</RsButton>
|
||||||
|
<RsButton @click="openDocumentation" variant="primary">
|
||||||
|
<Icon name="material-symbols:description" class="mr-1" />
|
||||||
|
View Documentation
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RsModal>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -967,6 +1455,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();
|
||||||
@ -985,6 +1480,7 @@ const isPreview = ref(false);
|
|||||||
const showFormSettings = ref(false);
|
const showFormSettings = ref(false);
|
||||||
const showTemplatesModal = ref(false);
|
const showTemplatesModal = ref(false);
|
||||||
const showFormHistoryModal = ref(false);
|
const showFormHistoryModal = ref(false);
|
||||||
|
const showHelpModal = ref(false);
|
||||||
const showDropdown = ref(false);
|
const showDropdown = ref(false);
|
||||||
const leftSidebarTab = ref('components');
|
const leftSidebarTab = ref('components');
|
||||||
const showFieldSettingsPanel = ref(true);
|
const showFieldSettingsPanel = ref(true);
|
||||||
@ -995,6 +1491,48 @@ const pendingNavigation = ref(null);
|
|||||||
const navigationTarget = ref(null);
|
const navigationTarget = ref(null);
|
||||||
const activeSettingsTab = ref('info');
|
const activeSettingsTab = ref('info');
|
||||||
|
|
||||||
|
// Published processes for button linking
|
||||||
|
const publishedProcesses = ref([]);
|
||||||
|
|
||||||
|
// Fetch published processes for button linking
|
||||||
|
const fetchPublishedProcesses = async () => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch('/api/process?status=published&limit=1000')
|
||||||
|
if (response.success) {
|
||||||
|
publishedProcesses.value = response.data.processes.map(process => ({
|
||||||
|
label: process.processName,
|
||||||
|
value: process.processUUID
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching published processes:', error)
|
||||||
|
publishedProcesses.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open documentation
|
||||||
|
const openDocumentation = () => {
|
||||||
|
// Open documentation in new tab
|
||||||
|
window.open('/documentation', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get submit button CSS classes and styles based on category and color
|
||||||
|
const getSubmitButtonStyles = (category, color) => {
|
||||||
|
const baseClasses = 'px-4 py-2 rounded font-medium transition-all duration-200 text-white border-0';
|
||||||
|
|
||||||
|
// Use CSS custom properties from theme.css with opacity based on category
|
||||||
|
const opacity = category === 'primary' ? '1' : '0.8';
|
||||||
|
const hoverOpacity = category === 'primary' ? '0.9' : '0.7';
|
||||||
|
|
||||||
|
return {
|
||||||
|
classes: baseClasses,
|
||||||
|
style: {
|
||||||
|
backgroundColor: `rgba(var(--color-${color}), ${opacity})`,
|
||||||
|
'--hover-bg': `rgba(var(--color-${color}), ${hoverOpacity})`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Responsive device preview state
|
// Responsive device preview state
|
||||||
const selectedDevice = ref('Desktop');
|
const selectedDevice = ref('Desktop');
|
||||||
const deviceSizes = ref([
|
const deviceSizes = ref([
|
||||||
@ -1032,6 +1570,7 @@ const conditionalLogicEngine = ref(null);
|
|||||||
// Settings tabs configuration
|
// Settings tabs configuration
|
||||||
const settingsTabs = [
|
const settingsTabs = [
|
||||||
{ key: 'info', label: 'Form Info', icon: 'material-symbols:info-outline' },
|
{ key: 'info', label: 'Form Info', icon: 'material-symbols:info-outline' },
|
||||||
|
{ key: 'submit', label: 'Submit Button', icon: 'material-symbols:play-arrow' },
|
||||||
{ key: 'javascript', label: 'JavaScript', icon: 'material-symbols:code' },
|
{ key: 'javascript', label: 'JavaScript', icon: 'material-symbols:code' },
|
||||||
{ key: 'css', label: 'CSS', icon: 'material-symbols:palette-outline' },
|
{ key: 'css', label: 'CSS', icon: 'material-symbols:palette-outline' },
|
||||||
{ key: 'events', label: 'Events', icon: 'material-symbols:event-outline' },
|
{ key: 'events', label: 'Events', icon: 'material-symbols:event-outline' },
|
||||||
@ -1048,6 +1587,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 {
|
||||||
@ -1062,7 +1625,8 @@ const formJson = computed(() => {
|
|||||||
customScript: formStore.formCustomScript,
|
customScript: formStore.formCustomScript,
|
||||||
customCSS: formStore.formCustomCSS,
|
customCSS: formStore.formCustomCSS,
|
||||||
formEvents: formStore.formEvents,
|
formEvents: formStore.formEvents,
|
||||||
scriptMode: formStore.scriptMode
|
scriptMode: formStore.scriptMode,
|
||||||
|
submitButton: formStore.submitButton
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1269,6 +1833,30 @@ const applyJsonChanges = () => {
|
|||||||
formStore.scriptMode = importedJson.scriptMode;
|
formStore.scriptMode = importedJson.scriptMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import submit button settings if available
|
||||||
|
if (importedJson.submitButton) {
|
||||||
|
formStore.submitButton = {
|
||||||
|
enabled: importedJson.submitButton.enabled !== undefined ? importedJson.submitButton.enabled : true,
|
||||||
|
label: importedJson.submitButton.label || 'Submit',
|
||||||
|
category: importedJson.submitButton.category || 'primary',
|
||||||
|
color: importedJson.submitButton.color || 'primary'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle backward compatibility with old variant format
|
||||||
|
if (importedJson.submitButton.variant && !importedJson.submitButton.category && !importedJson.submitButton.color) {
|
||||||
|
const variantMapping = {
|
||||||
|
'primary': { category: 'primary', color: 'primary' },
|
||||||
|
'secondary': { category: 'secondary', color: 'secondary' },
|
||||||
|
'success': { category: 'primary', color: 'success' },
|
||||||
|
'warning': { category: 'primary', color: 'warning' },
|
||||||
|
'danger': { category: 'primary', color: 'danger' }
|
||||||
|
};
|
||||||
|
const mapping = variantMapping[importedJson.submitButton.variant] || { category: 'primary', color: 'primary' };
|
||||||
|
formStore.submitButton.category = mapping.category;
|
||||||
|
formStore.submitButton.color = mapping.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as having unsaved changes
|
// Mark as having unsaved changes
|
||||||
formStore.hasUnsavedChanges = true;
|
formStore.hasUnsavedChanges = true;
|
||||||
|
|
||||||
@ -1562,6 +2150,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);
|
||||||
@ -2056,6 +2647,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();
|
||||||
};
|
};
|
||||||
@ -2495,8 +3134,21 @@ watch(() => formStore.selectedComponent, (newComponent) => {
|
|||||||
label: newComponent.props.label || '',
|
label: newComponent.props.label || '',
|
||||||
name: newComponent.props.name || '',
|
name: newComponent.props.name || '',
|
||||||
placeholder: newComponent.props.placeholder || '',
|
placeholder: newComponent.props.placeholder || '',
|
||||||
|
rows: newComponent.props.rows || 3,
|
||||||
|
linkType: newComponent.props.linkType || 'none',
|
||||||
|
linkUrl: newComponent.props.linkUrl || '',
|
||||||
|
linkProcessId: newComponent.props.linkProcessId || '',
|
||||||
|
iframeDebug: newComponent.props.iframeDebug || false,
|
||||||
|
iframeHideComplete: newComponent.props.iframeHideComplete || false,
|
||||||
|
iframeTheme: newComponent.props.iframeTheme || '',
|
||||||
|
iframeCustomParams: newComponent.props.iframeCustomParams || '',
|
||||||
required: newComponent.props.validation?.includes('required') || false
|
required: newComponent.props.validation?.includes('required') || false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch published processes if this is a button component
|
||||||
|
if (newComponent.type === 'button') {
|
||||||
|
fetchPublishedProcesses();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
@ -2513,6 +3165,14 @@ const showQuickField = (fieldName) => {
|
|||||||
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
|
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
|
||||||
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
|
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button'],
|
||||||
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select'],
|
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select'],
|
||||||
|
rows: ['textarea'],
|
||||||
|
linkType: ['button'],
|
||||||
|
linkUrl: ['button'],
|
||||||
|
linkProcessId: ['button'],
|
||||||
|
iframeDebug: ['button'],
|
||||||
|
iframeHideComplete: ['button'],
|
||||||
|
iframeTheme: ['button'],
|
||||||
|
iframeCustomParams: ['button'],
|
||||||
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'],
|
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'],
|
||||||
required: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'date', 'time', 'datetime-local', 'file', 'otp', 'dropzone']
|
required: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'checkbox', 'radio', 'date', 'time', 'datetime-local', 'file', 'otp', 'dropzone']
|
||||||
};
|
};
|
||||||
@ -2527,7 +3187,8 @@ const getPlaceholder = (fieldName) => {
|
|||||||
const placeholders = {
|
const placeholders = {
|
||||||
label: `Enter ${formStore.selectedComponent.type} label`,
|
label: `Enter ${formStore.selectedComponent.type} label`,
|
||||||
name: `${formStore.selectedComponent.type}_field`,
|
name: `${formStore.selectedComponent.type}_field`,
|
||||||
placeholder: 'Enter placeholder text...'
|
placeholder: 'Enter placeholder text...',
|
||||||
|
rows: '3'
|
||||||
};
|
};
|
||||||
|
|
||||||
return placeholders[fieldName] || '';
|
return placeholders[fieldName] || '';
|
||||||
@ -2634,6 +3295,10 @@ const handleFormRestored = (restoredForm) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Submit button hover effects using CSS custom properties */
|
||||||
|
:deep(.formkit-input[type="submit"]:hover) {
|
||||||
|
background-color: var(--hover-bg) !important;
|
||||||
|
}
|
||||||
.form-name-input {
|
.form-name-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
@ -47,36 +47,70 @@
|
|||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 p-6 overflow-auto">
|
||||||
<!-- Filters and Search -->
|
<!-- Filters and Search -->
|
||||||
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
<div class="mb-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||||
<div class="flex-1 max-w-md">
|
<div class="flex flex-col lg:flex-row gap-4">
|
||||||
<FormKit
|
<div class="flex-1 max-w-md">
|
||||||
v-model="searchQuery"
|
<FormKit
|
||||||
type="text"
|
v-model="searchQuery"
|
||||||
placeholder="Search forms..."
|
type="text"
|
||||||
:classes="{
|
placeholder="Search forms by name or description..."
|
||||||
outer: 'mb-0',
|
:classes="{
|
||||||
wrapper: 'relative',
|
outer: 'mb-0',
|
||||||
inner: 'relative',
|
wrapper: 'relative',
|
||||||
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
inner: 'relative',
|
||||||
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||||||
}"
|
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
||||||
>
|
}"
|
||||||
<template #prefixIcon>
|
>
|
||||||
<Icon name="material-symbols:search" class="text-lg" />
|
<template #prefixIcon>
|
||||||
</template>
|
<Icon name="material-symbols:search" class="text-lg" />
|
||||||
</FormKit>
|
</template>
|
||||||
</div>
|
</FormKit>
|
||||||
|
</div>
|
||||||
<div class="flex gap-2">
|
|
||||||
<RsButton @click="clearFilters" variant="secondary" size="sm" :disabled="loading" v-if="searchQuery">
|
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
|
||||||
Clear Filters
|
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
<div class="min-w-48">
|
||||||
<Icon name="material-symbols:refresh" class="mr-1" />
|
<FormKit
|
||||||
Refresh
|
v-model="selectedCategory"
|
||||||
</RsButton>
|
type="select"
|
||||||
|
:options="categoryOptions"
|
||||||
|
placeholder="Filter by category"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-48">
|
||||||
|
<FormKit
|
||||||
|
v-model="selectedGroup"
|
||||||
|
type="select"
|
||||||
|
:options="groupOptions"
|
||||||
|
placeholder="Filter by group"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<RsButton
|
||||||
|
@click="clearFilters"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
:disabled="loading"
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
|
Clear
|
||||||
|
</RsButton>
|
||||||
|
|
||||||
|
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
||||||
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
||||||
|
Refresh
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -88,112 +122,233 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Forms Table -->
|
<!-- Forms Grid -->
|
||||||
<div v-else class="bg-white shadow-md rounded-lg overflow-hidden">
|
<div v-else-if="filteredForms.length > 0" class="grid gap-4">
|
||||||
<table class="w-full table-auto">
|
<div
|
||||||
<thead class="bg-gray-50 border-b">
|
v-for="form in paginatedForms"
|
||||||
<tr>
|
:key="form.id"
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||||
<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>
|
<div class="flex items-start justify-between">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
<div class="flex-1">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Updated</th>
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
<h3 class="text-lg font-semibold text-gray-900">{{ form.name || 'Untitled Form' }}</h3>
|
||||||
</tr>
|
<div class="flex gap-2">
|
||||||
</thead>
|
<RsBadge v-if="form.category" :variant="getCategoryColor(form.category)" size="sm">
|
||||||
<tbody class="divide-y divide-gray-200">
|
{{ form.category }}
|
||||||
<tr v-if="filteredForms.length === 0">
|
</RsBadge>
|
||||||
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
|
<RsBadge v-if="form.group" variant="secondary" size="sm">
|
||||||
<div class="flex flex-col items-center">
|
{{ form.group }}
|
||||||
<Icon name="material-symbols:description-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
</RsBadge>
|
||||||
<p class="text-lg font-medium mb-1">
|
|
||||||
{{ searchQuery ? 'No forms match your search' : 'No forms found' }}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm mb-2">
|
|
||||||
{{ searchQuery ? 'Try adjusting your search terms' : 'Create your first form to get started' }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Show current search if any -->
|
|
||||||
<div v-if="searchQuery" class="text-xs text-gray-400 mb-4">
|
|
||||||
<div>Search: "{{ searchQuery }}"</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<RsButton v-if="searchQuery" @click="clearFilters" variant="secondary" size="sm">
|
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
|
||||||
Clear Filters
|
|
||||||
</RsButton>
|
|
||||||
<RsButton v-if="!searchQuery" @click="createNewForm" variant="primary" size="sm">
|
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
|
||||||
Create New Form
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
<tr v-for="form in filteredForms" :key="form.id" class="hover:bg-gray-50">
|
<p class="text-gray-600 mb-3">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
{{ form.description || 'No description available' }}
|
||||||
<div class="font-medium text-gray-900">{{ form.name || 'Untitled Form' }}</div>
|
</p>
|
||||||
<div class="text-sm text-gray-500">ID: {{ form.id }}</div>
|
|
||||||
</td>
|
<div class="flex items-center gap-6 text-sm text-gray-500">
|
||||||
<td class="px-6 py-4">
|
<div class="flex items-center gap-1">
|
||||||
<div class="text-sm text-gray-700 max-w-xs truncate">
|
<Icon name="material-symbols:fingerprint" class="w-4 h-4" />
|
||||||
{{ form.description || 'No description' }}
|
ID: {{ form.id }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="flex items-center gap-1">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<Icon name="material-symbols:calendar-add-on-outline" class="w-4 h-4" />
|
||||||
<div class="text-sm text-gray-700">
|
Created {{ formatDate(form.createdAt) }}
|
||||||
{{ form.components ? form.components.length : 0 }} components
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="flex items-center gap-1">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<Icon name="material-symbols:schedule" class="w-4 h-4" />
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(form.createdAt) }}</div>
|
Updated {{ formatDate(form.updatedAt) }}
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(form.updatedAt) }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<div class="flex space-x-2 justify-end">
|
|
||||||
<button
|
|
||||||
@click="editForm(form.id)"
|
|
||||||
class="p-1 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded"
|
|
||||||
title="Edit Form"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:edit" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="duplicateForm(form)"
|
|
||||||
class="p-1 text-indigo-600 hover:text-indigo-900 hover:bg-indigo-50 rounded"
|
|
||||||
title="Duplicate Form"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:content-copy" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="exportForm(form)"
|
|
||||||
class="p-1 text-green-600 hover:text-green-900 hover:bg-green-50 rounded"
|
|
||||||
title="Export Form as JSON"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:download" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="confirmDelete(form.id)"
|
|
||||||
class="p-1 text-red-600 hover:text-red-900 hover:bg-red-50 rounded"
|
|
||||||
title="Delete Form"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:delete" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
<div class="flex items-center gap-2 ml-4">
|
||||||
|
<button
|
||||||
|
@click="editForm(form.id)"
|
||||||
|
class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
|
title="Edit Form"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:edit" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="duplicateForm(form)"
|
||||||
|
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
||||||
|
title="Duplicate Form"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:content-copy" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="exportForm(form)"
|
||||||
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
|
title="Export Form as JSON"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:download" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="confirmDelete(form.id)"
|
||||||
|
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
title="Delete Form"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:delete" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div v-if="filteredForms.length > 0 && totalPages > 1" class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<!-- Pagination Info -->
|
||||||
|
<div class="text-sm text-gray-700">
|
||||||
|
Showing {{ paginationInfo.start }}-{{ paginationInfo.end }} of {{ paginationInfo.total }} forms
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items per page selector -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-700">Items per page:</span>
|
||||||
|
<FormKit
|
||||||
|
:model-value="itemsPerPage"
|
||||||
|
@update:model-value="changeItemsPerPage"
|
||||||
|
type="select"
|
||||||
|
:options="[
|
||||||
|
{ label: '5', value: 5 },
|
||||||
|
{ label: '10', value: 10 },
|
||||||
|
{ label: '20', value: 20 },
|
||||||
|
{ label: '50', value: 50 }
|
||||||
|
]"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'px-2 py-1 border border-gray-300 rounded text-sm bg-white min-w-16'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<!-- Previous Button -->
|
||||||
|
<button
|
||||||
|
@click="prevPage"
|
||||||
|
:disabled="!hasPrevPage"
|
||||||
|
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:chevron-left" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Page Numbers -->
|
||||||
|
<template v-if="totalPages <= 7">
|
||||||
|
<!-- Show all pages if 7 or fewer -->
|
||||||
|
<button
|
||||||
|
v-for="page in totalPages"
|
||||||
|
:key="page"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
page === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- Show first page -->
|
||||||
|
<button
|
||||||
|
@click="goToPage(1)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
1 === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Show ellipsis if needed -->
|
||||||
|
<span v-if="currentPage > 3" class="px-2 text-gray-500">...</span>
|
||||||
|
|
||||||
|
<!-- Show pages around current page -->
|
||||||
|
<button
|
||||||
|
v-for="page in [currentPage - 1, currentPage, currentPage + 1].filter(p => p > 1 && p < totalPages)"
|
||||||
|
:key="page"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
page === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Show ellipsis if needed -->
|
||||||
|
<span v-if="currentPage < totalPages - 2" class="px-2 text-gray-500">...</span>
|
||||||
|
|
||||||
|
<!-- Show last page -->
|
||||||
|
<button
|
||||||
|
v-if="totalPages > 1"
|
||||||
|
@click="goToPage(totalPages)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
totalPages === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ totalPages }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Next Button -->
|
||||||
|
<button
|
||||||
|
@click="nextPage"
|
||||||
|
:disabled="!hasNextPage"
|
||||||
|
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:chevron-right" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else-if="filteredForms.length === 0" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||||
|
<Icon name="material-symbols:description-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
{{ hasActiveFilters ? 'No forms match your filters' : 'No forms found' }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
{{ hasActiveFilters ? 'Try adjusting your search or filter criteria' : 'Create your first form to get started' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Show current filters if any -->
|
||||||
|
<div v-if="hasActiveFilters" class="text-sm text-gray-400 mb-6 space-y-1">
|
||||||
|
<div v-if="searchQuery">Search: "{{ searchQuery }}"</div>
|
||||||
|
<div v-if="selectedCategory">Category: {{ selectedCategory }}</div>
|
||||||
|
<div v-if="selectedGroup">Group: {{ selectedGroup }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-3">
|
||||||
|
<RsButton v-if="hasActiveFilters" @click="clearFilters" variant="secondary">
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-2" />
|
||||||
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
<RsButton v-if="!hasActiveFilters" @click="createNewForm" variant="primary">
|
||||||
|
<Icon name="material-symbols:add" class="mr-2" />
|
||||||
|
Create New Form
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Global Loading Overlay -->
|
<!-- Global Loading Overlay -->
|
||||||
@ -385,6 +540,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,7 +555,33 @@ const jsonContent = ref('');
|
|||||||
const jsonValidationMessage = ref('');
|
const jsonValidationMessage = ref('');
|
||||||
const jsonIsValid = ref(false);
|
const jsonIsValid = ref(false);
|
||||||
|
|
||||||
// Filtered forms
|
// Pagination state
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const itemsPerPage = ref(5); // Set to 5 to test pagination
|
||||||
|
const totalItems = ref(0);
|
||||||
|
|
||||||
|
// 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 (all forms after applying filters)
|
||||||
const filteredForms = computed(() => {
|
const filteredForms = computed(() => {
|
||||||
let filtered = formStore.savedForms;
|
let filtered = formStore.savedForms;
|
||||||
|
|
||||||
@ -412,9 +595,52 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total items for pagination
|
||||||
|
totalItems.value = filtered.length;
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Paginated forms (forms for current page)
|
||||||
|
const paginatedForms = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage.value;
|
||||||
|
const end = start + itemsPerPage.value;
|
||||||
|
return filteredForms.value.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination computed properties
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNextPage = computed(() => {
|
||||||
|
return currentPage.value < totalPages.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPrevPage = computed(() => {
|
||||||
|
return currentPage.value > 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationInfo = computed(() => {
|
||||||
|
const start = totalItems.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1;
|
||||||
|
const end = Math.min(currentPage.value * itemsPerPage.value, totalItems.value);
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
total: totalItems.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatDate = (isoString) => {
|
const formatDate = (isoString) => {
|
||||||
if (!isoString) return '';
|
if (!isoString) return '';
|
||||||
@ -459,18 +685,17 @@ const duplicateForm = async (form) => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const newName = `${form.name || 'Form'} (Copy)`;
|
const newName = `${form.name || 'Form'} (Copy)`;
|
||||||
|
|
||||||
// Create a copy of the form
|
// Load the original form first
|
||||||
const duplicatedForm = {
|
|
||||||
...form,
|
|
||||||
id: null, // Will get new ID when saved
|
|
||||||
name: newName,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load the original form, duplicate it, and save as new
|
|
||||||
await formStore.loadForm(form.id);
|
await formStore.loadForm(form.id);
|
||||||
|
|
||||||
|
// Clear the current form ID to ensure a new form is created
|
||||||
|
formStore.currentFormId = null;
|
||||||
|
|
||||||
|
// Update the form name for the duplicate
|
||||||
formStore.setFormName(newName);
|
formStore.setFormName(newName);
|
||||||
|
|
||||||
|
// Mark as having unsaved changes and save as new form
|
||||||
|
formStore.hasUnsavedChanges = true;
|
||||||
await formStore.saveForm();
|
await formStore.saveForm();
|
||||||
|
|
||||||
// Refresh the form list
|
// Refresh the form list
|
||||||
@ -552,8 +777,10 @@ 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], () => {
|
||||||
|
// Reset to first page when filters change
|
||||||
|
currentPage.value = 1;
|
||||||
// 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 +793,49 @@ let searchTimeout = null;
|
|||||||
// Clear all filters
|
// Clear all filters
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
|
selectedCategory.value = '';
|
||||||
|
selectedGroup.value = '';
|
||||||
|
currentPage.value = 1; // Reset to first page
|
||||||
// loadForms will be called automatically by the watcher
|
// loadForms will be called automatically by the watcher
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pagination methods
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextPage = () => {
|
||||||
|
if (hasNextPage.value) {
|
||||||
|
currentPage.value++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevPage = () => {
|
||||||
|
if (hasPrevPage.value) {
|
||||||
|
currentPage.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeItemsPerPage = (newItemsPerPage) => {
|
||||||
|
itemsPerPage.value = newItemsPerPage;
|
||||||
|
currentPage.value = 1; // Reset to first page when changing items per page
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
@ -826,13 +1093,35 @@ const closeImportModal = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Custom styles for better UX */
|
/* Custom transitions and animations */
|
||||||
.hover\:bg-gray-50:hover {
|
.transition-colors {
|
||||||
transition: background-color 0.15s ease-in-out;
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-shadow {
|
||||||
|
transition-property: box-shadow;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism effect for cards */
|
||||||
|
.bg-white {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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>
|
||||||
|
@ -3149,10 +3149,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>
|
||||||
|
@ -29,6 +29,11 @@ const sortBy = ref('processCreatedDate');
|
|||||||
const sortOrder = ref('desc');
|
const sortOrder = ref('desc');
|
||||||
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
|
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const itemsPerPage = ref(20); // Default to 20 items per page
|
||||||
|
const totalItems = ref(0);
|
||||||
|
|
||||||
// Dashboard metrics and data
|
// Dashboard metrics and data
|
||||||
const dashboardMetrics = ref({
|
const dashboardMetrics = ref({
|
||||||
totalProcesses: 0,
|
totalProcesses: 0,
|
||||||
@ -66,9 +71,66 @@ const categoryOptions = [
|
|||||||
{ value: 'Procurement', label: 'Procurement' }
|
{ value: 'Procurement', label: 'Procurement' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Filtered processes
|
// Filtered processes (all processes after applying filters)
|
||||||
const filteredProcesses = computed(() => {
|
const filteredProcesses = computed(() => {
|
||||||
return processStore.processes;
|
let filtered = processStore.processes;
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
const query = searchQuery.value.toLowerCase().trim();
|
||||||
|
filtered = filtered.filter(process =>
|
||||||
|
process.name?.toLowerCase().includes(query) ||
|
||||||
|
process.description?.toLowerCase().includes(query) ||
|
||||||
|
process.category?.toLowerCase().includes(query) ||
|
||||||
|
process.creator?.userFullName?.toLowerCase().includes(query) ||
|
||||||
|
process.creator?.userUsername?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply status filter
|
||||||
|
if (statusFilter.value) {
|
||||||
|
filtered = filtered.filter(process => process.status === statusFilter.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply category filter
|
||||||
|
if (categoryFilter.value) {
|
||||||
|
filtered = filtered.filter(process => process.category === categoryFilter.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total items for pagination
|
||||||
|
totalItems.value = filtered.length;
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paginated processes (processes for current page)
|
||||||
|
const paginatedProcesses = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage.value;
|
||||||
|
const end = start + itemsPerPage.value;
|
||||||
|
return filteredProcesses.value.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination computed properties
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNextPage = computed(() => {
|
||||||
|
return currentPage.value < totalPages.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPrevPage = computed(() => {
|
||||||
|
return currentPage.value > 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationInfo = computed(() => {
|
||||||
|
const start = totalItems.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage.value + 1;
|
||||||
|
const end = Math.min(currentPage.value * itemsPerPage.value, totalItems.value);
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
total: totalItems.value
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load dashboard summary data from API
|
// Load dashboard summary data from API
|
||||||
@ -190,7 +252,8 @@ const loadProcesses = async () => {
|
|||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
sortBy: sortBy.value,
|
sortBy: sortBy.value,
|
||||||
sortOrder: sortOrder.value
|
sortOrder: sortOrder.value,
|
||||||
|
limit: 1000 // Set a high limit to get all processes
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statusFilter.value) {
|
if (statusFilter.value) {
|
||||||
@ -220,6 +283,8 @@ const loadProcesses = async () => {
|
|||||||
|
|
||||||
// Watch for changes in filters and reload processes
|
// Watch for changes in filters and reload processes
|
||||||
watch([searchQuery, statusFilter, categoryFilter], () => {
|
watch([searchQuery, statusFilter, categoryFilter], () => {
|
||||||
|
// Reset to first page when filters change
|
||||||
|
currentPage.value = 1;
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
loadProcesses();
|
loadProcesses();
|
||||||
@ -283,8 +348,24 @@ const publishProcess = async (processId) => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
await processStore.publishProcess(processId);
|
await processStore.publishProcess(processId);
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
|
toast.success('Process published successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing process:', error);
|
console.error('Error publishing process:', error);
|
||||||
|
toast.error('Failed to publish process: ' + (error.message || 'Unknown error'));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unpublishProcess = async (processId) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
await processStore.unpublishProcess(processId);
|
||||||
|
await loadProcesses();
|
||||||
|
toast.success('Process unpublished successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unpublishing process:', error);
|
||||||
|
toast.error('Failed to unpublish process: ' + (error.message || 'Unknown error'));
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -303,6 +384,14 @@ const restoreProcess = async (processId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = (processId) => {
|
const confirmDelete = (processId) => {
|
||||||
|
const process = processStore.processes.find(p => p.id === processId);
|
||||||
|
|
||||||
|
// If process is published, show a message instead of allowing deletion
|
||||||
|
if (process && process.status === 'published') {
|
||||||
|
toast.warning('Please unpublish the process first before deleting it.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
processToDelete.value = processId;
|
processToDelete.value = processId;
|
||||||
showDeleteConfirm.value = true;
|
showDeleteConfirm.value = true;
|
||||||
};
|
};
|
||||||
@ -325,6 +414,31 @@ const clearFilters = () => {
|
|||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
statusFilter.value = '';
|
statusFilter.value = '';
|
||||||
categoryFilter.value = '';
|
categoryFilter.value = '';
|
||||||
|
currentPage.value = 1; // Reset to first page
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pagination methods
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextPage = () => {
|
||||||
|
if (hasNextPage.value) {
|
||||||
|
currentPage.value++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevPage = () => {
|
||||||
|
if (hasPrevPage.value) {
|
||||||
|
currentPage.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeItemsPerPage = (newItemsPerPage) => {
|
||||||
|
itemsPerPage.value = newItemsPerPage;
|
||||||
|
currentPage.value = 1; // Reset to first page when changing items per page
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load processes on component mount
|
// Load processes on component mount
|
||||||
@ -629,8 +743,9 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
|
|
||||||
<!-- Processes Grid -->
|
<!-- Processes Grid -->
|
||||||
<div v-else-if="filteredProcesses.length > 0" class="grid gap-4">
|
<div v-else-if="filteredProcesses.length > 0" class="grid gap-4">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="process in filteredProcesses"
|
v-for="process in paginatedProcesses"
|
||||||
:key="process.id"
|
:key="process.id"
|
||||||
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||||
>
|
>
|
||||||
@ -710,13 +825,23 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
<button
|
<button
|
||||||
v-if="process.status === 'draft'"
|
v-if="process.status === 'draft'"
|
||||||
@click="publishProcess(process.id)"
|
@click="publishProcess(process.id)"
|
||||||
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
title="Publish Process"
|
title="Publish Process"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:publish" class="text-lg" />
|
<Icon name="material-symbols:publish" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="process.status === 'published'"
|
||||||
|
@click="unpublishProcess(process.id)"
|
||||||
|
class="p-2 text-orange-600 hover:text-orange-800 hover:bg-orange-50 rounded-lg transition-colors"
|
||||||
|
title="Unpublish Process (Move to Draft)"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:unpublished" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="duplicateProcess(process)"
|
@click="duplicateProcess(process)"
|
||||||
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
||||||
@ -728,8 +853,8 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
@click="confirmDelete(process.id)"
|
@click="confirmDelete(process.id)"
|
||||||
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
title="Delete Process"
|
:title="process.status === 'published' ? 'Unpublish first to delete this process' : 'Delete Process'"
|
||||||
:disabled="loading || process.status === 'published'"
|
:disabled="loading || process.status === 'published'"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:delete" class="text-lg" />
|
<Icon name="material-symbols:delete" class="text-lg" />
|
||||||
@ -740,8 +865,127 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div v-if="filteredProcesses.length > 0 && totalPages > 1" class="mt-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<!-- Pagination Info -->
|
||||||
|
<div class="text-sm text-gray-700">
|
||||||
|
Showing {{ paginationInfo.start }}-{{ paginationInfo.end }} of {{ paginationInfo.total }} processes
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items per page selector -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-700">Items per page:</span>
|
||||||
|
<FormKit
|
||||||
|
:model-value="itemsPerPage"
|
||||||
|
@update:model-value="changeItemsPerPage"
|
||||||
|
type="select"
|
||||||
|
:options="[
|
||||||
|
{ label: '5', value: 5 },
|
||||||
|
{ label: '10', value: 10 },
|
||||||
|
{ label: '20', value: 20 },
|
||||||
|
{ label: '50', value: 50 },
|
||||||
|
{ label: '100', value: 100 }
|
||||||
|
]"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'px-2 py-1 border border-gray-300 rounded text-sm bg-white min-w-16'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<!-- Previous Button -->
|
||||||
|
<button
|
||||||
|
@click="prevPage"
|
||||||
|
:disabled="!hasPrevPage"
|
||||||
|
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:chevron-left" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Page Numbers -->
|
||||||
|
<template v-if="totalPages <= 7">
|
||||||
|
<!-- Show all pages if 7 or fewer -->
|
||||||
|
<button
|
||||||
|
v-for="page in totalPages"
|
||||||
|
:key="page"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
page === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<!-- Show first page -->
|
||||||
|
<button
|
||||||
|
@click="goToPage(1)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
1 === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Show ellipsis if needed -->
|
||||||
|
<span v-if="currentPage > 3" class="px-2 text-gray-500">...</span>
|
||||||
|
|
||||||
|
<!-- Show pages around current page -->
|
||||||
|
<button
|
||||||
|
v-for="page in [currentPage - 1, currentPage, currentPage + 1].filter(p => p > 1 && p < totalPages)"
|
||||||
|
:key="page"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
page === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Show ellipsis if needed -->
|
||||||
|
<span v-if="currentPage < totalPages - 2" class="px-2 text-gray-500">...</span>
|
||||||
|
|
||||||
|
<!-- Show last page -->
|
||||||
|
<button
|
||||||
|
v-if="totalPages > 1"
|
||||||
|
@click="goToPage(totalPages)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 text-sm border rounded-lg transition-colors',
|
||||||
|
totalPages === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ totalPages }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Next Button -->
|
||||||
|
<button
|
||||||
|
@click="nextPage"
|
||||||
|
:disabled="!hasNextPage"
|
||||||
|
class="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:chevron-right" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
<div v-else-if="filteredProcesses.length === 0" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||||
<Icon name="material-symbols:folder-open-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<Icon name="material-symbols:folder-open-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||||
{{ (searchQuery || statusFilter || categoryFilter) ? 'No processes match your filters' : 'No processes found' }}
|
{{ (searchQuery || statusFilter || categoryFilter) ? 'No processes match your filters' : 'No processes found' }}
|
||||||
@ -790,7 +1034,7 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
This process will be moved to trash but not permanently deleted. You can restore it later from the "Deleted" filter if needed.
|
This process will be moved to trash but not permanently deleted. You can restore it later from the "Deleted" filter if needed.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Note: Published processes cannot be deleted and must be unpublished first.
|
Note: To delete a published process, first unpublish it using the orange unpublish button.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,6 +84,84 @@ watch(isProcessComplete, (newValue) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Capture URL parameters for variables that are configured to do so
|
||||||
|
const captureUrlParameters = () => {
|
||||||
|
try {
|
||||||
|
console.log('[Workflow] Capturing URL parameters...');
|
||||||
|
|
||||||
|
// Get current URL parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
console.log('[Workflow] Available URL parameters:', Object.fromEntries(urlParams.entries()));
|
||||||
|
|
||||||
|
// Check if we have process variables defined
|
||||||
|
const variableSource = process.value?.variables || process.value?.processVariables;
|
||||||
|
if (!variableSource) {
|
||||||
|
console.log('[Workflow] No process variables defined, skipping URL parameter capture');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through process variables to find ones configured for URL capture
|
||||||
|
Object.entries(variableSource).forEach(([variableName, variableConfig]) => {
|
||||||
|
if (variableConfig && variableConfig.captureFromUrl && variableConfig.urlParameterName) {
|
||||||
|
const paramName = variableConfig.urlParameterName;
|
||||||
|
const paramValue = urlParams.get(paramName);
|
||||||
|
|
||||||
|
if (paramValue !== null) {
|
||||||
|
// Convert the parameter value based on the variable type
|
||||||
|
let convertedValue = paramValue;
|
||||||
|
|
||||||
|
switch (variableConfig.type) {
|
||||||
|
case 'number':
|
||||||
|
case 'int':
|
||||||
|
case 'decimal':
|
||||||
|
convertedValue = Number(paramValue);
|
||||||
|
if (isNaN(convertedValue)) {
|
||||||
|
console.warn(`[Workflow] Could not convert URL parameter "${paramName}" value "${paramValue}" to number for variable "${variableName}"`);
|
||||||
|
convertedValue = variableConfig.value || 0; // Use default value
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
convertedValue = paramValue.toLowerCase() === 'true' || paramValue === '1';
|
||||||
|
break;
|
||||||
|
case 'object':
|
||||||
|
try {
|
||||||
|
convertedValue = JSON.parse(paramValue);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[Workflow] Could not parse URL parameter "${paramName}" value "${paramValue}" as JSON for variable "${variableName}"`);
|
||||||
|
convertedValue = variableConfig.value || {}; // Use default value
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
const dateValue = new Date(paramValue);
|
||||||
|
if (isNaN(dateValue.getTime())) {
|
||||||
|
console.warn(`[Workflow] Could not parse URL parameter "${paramName}" value "${paramValue}" as date for variable "${variableName}"`);
|
||||||
|
convertedValue = variableConfig.value || null; // Use default value
|
||||||
|
} else {
|
||||||
|
convertedValue = variableConfig.type === 'date' ? dateValue.toISOString().split('T')[0] : dateValue.toISOString();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
default:
|
||||||
|
convertedValue = String(paramValue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the variable value
|
||||||
|
processVariables.value[variableName] = convertedValue;
|
||||||
|
console.log(`[Workflow] Captured URL parameter "${paramName}" = "${paramValue}" -> variable "${variableName}" = `, convertedValue);
|
||||||
|
} else {
|
||||||
|
console.log(`[Workflow] URL parameter "${paramName}" not found for variable "${variableName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Workflow] Final process variables after URL capture:', processVariables.value);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Workflow] Error capturing URL parameters:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Load process data
|
// Load process data
|
||||||
const loadProcess = async () => {
|
const loadProcess = async () => {
|
||||||
try {
|
try {
|
||||||
@ -95,7 +173,6 @@ const loadProcess = async () => {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
process.value = response.process; // includes processDefinition
|
process.value = response.process; // includes processDefinition
|
||||||
console.log('[Workflow] Process loaded:', process.value.processName, process.value.processDefinition);
|
|
||||||
|
|
||||||
// Check if process is published
|
// Check if process is published
|
||||||
const status = process.value.processStatus || process.value.status || 'draft';
|
const status = process.value.processStatus || process.value.status || 'draft';
|
||||||
@ -104,8 +181,20 @@ const loadProcess = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize process variables from DB (process.processVariables)
|
// Initialize process variables from DB - extract values from variable definitions
|
||||||
processVariables.value = process.value.processVariables ? { ...process.value.processVariables } : {};
|
processVariables.value = {};
|
||||||
|
|
||||||
|
// Try both possible property names for variables
|
||||||
|
const variableSource = process.value.variables || process.value.processVariables;
|
||||||
|
if (variableSource) {
|
||||||
|
Object.entries(variableSource).forEach(([name, variableDefinition]) => {
|
||||||
|
// Set the actual value, defaulting to the variable's default value or empty string
|
||||||
|
processVariables.value[name] = variableDefinition.value || variableDefinition.defaultValue || '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture URL parameters for variables configured to do so
|
||||||
|
captureUrlParameters();
|
||||||
|
|
||||||
// Start the process execution (case instance)
|
// Start the process execution (case instance)
|
||||||
await startProcessExecution();
|
await startProcessExecution();
|
||||||
@ -567,11 +656,18 @@ function buildApiRequest(nodeData, variables) {
|
|||||||
// 4. Authorization
|
// 4. Authorization
|
||||||
if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') {
|
if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') {
|
||||||
const auth = nodeData.authorization;
|
const auth = nodeData.authorization;
|
||||||
|
|
||||||
if (auth.type === 'bearer' && auth.token) {
|
if (auth.type === 'bearer' && auth.token) {
|
||||||
headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`;
|
headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`;
|
||||||
} else if (auth.type === 'basic' && auth.username && auth.password) {
|
} else if (auth.type === 'basic') {
|
||||||
const token = btoa(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`);
|
if (auth.token) {
|
||||||
headers['Authorization'] = `Basic ${token}`;
|
// Basic Auth with token (JWT or other token)
|
||||||
|
headers['Authorization'] = `Basic ${substituteVariables(auth.token, variables)}`;
|
||||||
|
} else if (auth.username && auth.password) {
|
||||||
|
// Basic Auth with username/password
|
||||||
|
const token = btoa(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`);
|
||||||
|
headers['Authorization'] = `Basic ${token}`;
|
||||||
|
}
|
||||||
} else if (auth.type === 'apiKey' && auth.key && auth.value) {
|
} else if (auth.type === 'apiKey' && auth.key && auth.value) {
|
||||||
if (auth.in === 'header') {
|
if (auth.in === 'header') {
|
||||||
headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables);
|
headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables);
|
||||||
@ -624,26 +720,45 @@ const executeCurrentStep = async () => {
|
|||||||
if (currentNode.value?.type === 'api') {
|
if (currentNode.value?.type === 'api') {
|
||||||
console.log(`[Workflow] Executing API node: ${currentNode.value.data?.label || currentNode.value.label}`);
|
console.log(`[Workflow] Executing API node: ${currentNode.value.data?.label || currentNode.value.label}`);
|
||||||
const nodeData = currentNode.value.data || {};
|
const nodeData = currentNode.value.data || {};
|
||||||
// Use new structure if present
|
|
||||||
if (nodeData.body || nodeData.headers || nodeData.params || nodeData.authorization) {
|
// Use new structure if present (check for any new structure properties)
|
||||||
const { url, headers, body } = buildApiRequest(nodeData, processVariables.value);
|
if (nodeData.body !== undefined || nodeData.headers !== undefined || nodeData.params !== undefined || nodeData.authorization !== undefined) {
|
||||||
const apiMethod = nodeData.apiMethod || 'GET';
|
|
||||||
const outputVariable = nodeData.outputVariable || 'apiResponse';
|
const outputVariable = nodeData.outputVariable || 'apiResponse';
|
||||||
const errorVariable = nodeData.errorVariable || 'apiError';
|
const errorVariable = nodeData.errorVariable || 'apiError';
|
||||||
const continueOnError = nodeData.continueOnError || false;
|
const continueOnError = nodeData.continueOnError || false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await $fetch(url, {
|
// Use proxy endpoint to avoid CORS issues
|
||||||
method: apiMethod,
|
const response = await $fetch('/api/process/workflow-api-call', {
|
||||||
headers,
|
method: 'POST',
|
||||||
body: ['GET', 'HEAD'].includes(apiMethod) ? undefined : body,
|
body: {
|
||||||
|
nodeData,
|
||||||
|
processVariables: processVariables.value
|
||||||
|
}
|
||||||
});
|
});
|
||||||
processVariables.value[outputVariable] = response;
|
|
||||||
processVariables.value[errorVariable] = null;
|
if (response.success) {
|
||||||
console.log('[Workflow] API call success. Output variable set:', outputVariable, response);
|
processVariables.value[outputVariable] = response.data;
|
||||||
if (canAutoProgress(currentNode.value)) {
|
processVariables.value[errorVariable] = null;
|
||||||
moveToNextStep();
|
console.log('[Workflow] API call success. Output variable set:', outputVariable, response.data);
|
||||||
|
if (canAutoProgress(currentNode.value)) {
|
||||||
|
moveToNextStep();
|
||||||
|
} else {
|
||||||
|
console.log('[Workflow] API completed, multiple paths available - waiting for user choice');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[Workflow] API completed, multiple paths available - waiting for user choice');
|
processVariables.value[errorVariable] = response.error;
|
||||||
|
console.error('[Workflow] API call failed:', response.error);
|
||||||
|
if (continueOnError) {
|
||||||
|
if (canAutoProgress(currentNode.value)) {
|
||||||
|
moveToNextStep();
|
||||||
|
} else {
|
||||||
|
console.log('[Workflow] API failed but continuing, multiple paths available - waiting for user choice');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error.value = 'API call failed: ' + (response.error.message || response.error);
|
||||||
|
notifyParentOfError(error.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
processVariables.value[errorVariable] = err;
|
processVariables.value[errorVariable] = err;
|
||||||
@ -661,6 +776,7 @@ const executeCurrentStep = async () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: old structure
|
// Fallback: old structure
|
||||||
|
|
||||||
const {
|
const {
|
||||||
apiUrl,
|
apiUrl,
|
||||||
apiMethod = 'GET',
|
apiMethod = 'GET',
|
||||||
@ -914,6 +1030,14 @@ const handleScriptFieldChange = ({ fieldName, value }) => {
|
|||||||
formStore.updatePreviewFormData(formData.value);
|
formStore.updatePreviewFormData(formData.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle form data updates from ComponentPreview
|
||||||
|
const handleFormDataUpdate = (updatedData) => {
|
||||||
|
// Update the form data with the new data
|
||||||
|
formData.value = { ...formData.value, ...updatedData };
|
||||||
|
// Also update form store to keep them in sync
|
||||||
|
formStore.updatePreviewFormData(formData.value);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle conditional logic script generation
|
// Handle conditional logic script generation
|
||||||
const handleConditionalLogicGenerated = (generatedScript) => {
|
const handleConditionalLogicGenerated = (generatedScript) => {
|
||||||
console.log('[WorkflowExecution] Conditional logic script generated');
|
console.log('[WorkflowExecution] Conditional logic script generated');
|
||||||
@ -1102,7 +1226,38 @@ watch(currentStep, async (newStep) => {
|
|||||||
// Update form store with form components and data for ComponentPreview
|
// Update form store with form components and data for ComponentPreview
|
||||||
if (currentForm.value?.formComponents) {
|
if (currentForm.value?.formComponents) {
|
||||||
formStore.formComponents = currentForm.value.formComponents;
|
formStore.formComponents = currentForm.value.formComponents;
|
||||||
formStore.updatePreviewFormData(formData.value);
|
|
||||||
|
// Initialize repeating groups in form data
|
||||||
|
const updatedFormData = { ...formData.value };
|
||||||
|
currentForm.value.formComponents.forEach(component => {
|
||||||
|
if (component.type === 'repeating-group' && component.props?.name) {
|
||||||
|
const groupName = component.props.name;
|
||||||
|
const minItems = component.props.minItems || 1;
|
||||||
|
|
||||||
|
// If the field doesn't exist or is not an array, initialize it
|
||||||
|
if (!updatedFormData[groupName] || !Array.isArray(updatedFormData[groupName])) {
|
||||||
|
const initialGroups = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < minItems; i++) {
|
||||||
|
const newGroup = {};
|
||||||
|
|
||||||
|
// Add fields from configuration
|
||||||
|
if (component.props.fields) {
|
||||||
|
component.props.fields.forEach(field => {
|
||||||
|
newGroup[field.name] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialGroups.push(newGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedFormData[groupName] = initialGroups;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
formData.value = updatedFormData;
|
||||||
|
formStore.updatePreviewFormData(updatedFormData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (currentNode.value.type === "page-wrapper") {
|
} else if (currentNode.value.type === "page-wrapper") {
|
||||||
@ -1476,6 +1631,39 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
finalResult: evaluateConditionGroup(conditionGroup, variables)
|
finalResult: evaluateConditionGroup(conditionGroup, variables)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Get submit button variant for RsButton
|
||||||
|
const getSubmitButtonVariant = () => {
|
||||||
|
// If form has submit button configuration, use it
|
||||||
|
if (currentForm.value?.submitButton) {
|
||||||
|
const { category } = currentForm.value.submitButton;
|
||||||
|
return category || 'primary';
|
||||||
|
}
|
||||||
|
// Fallback to primary
|
||||||
|
return 'primary';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: Get submit button styles using theme colors
|
||||||
|
const getWorkflowSubmitButtonStyle = () => {
|
||||||
|
// If form has submit button configuration, use it
|
||||||
|
if (currentForm.value?.submitButton) {
|
||||||
|
const { category = 'primary', color = 'primary' } = currentForm.value.submitButton;
|
||||||
|
|
||||||
|
// Use CSS custom properties from theme.css with opacity based on category
|
||||||
|
const opacity = category === 'primary' ? '1' : '0.8';
|
||||||
|
const hoverOpacity = category === 'primary' ? '0.9' : '0.7';
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: `rgba(var(--color-${color}), ${opacity})`,
|
||||||
|
'--hover-bg': `rgba(var(--color-${color}), ${hoverOpacity})`,
|
||||||
|
color: 'white',
|
||||||
|
border: 'none'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default styling
|
||||||
|
return {};
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -1648,18 +1836,20 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
:component="component"
|
:component="component"
|
||||||
:is-preview="false"
|
:is-preview="false"
|
||||||
:field-states="fieldStates"
|
:field-states="fieldStates"
|
||||||
|
@form-data-updated="handleFormDataUpdate"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- Place submit button in a full-width row at the end of the grid -->
|
<!-- Submit button - respects form builder configuration -->
|
||||||
<div class="col-span-12 flex justify-start pt-6 border-t border-gray-200">
|
<div v-if="currentForm?.submitButton?.enabled !== false" class="col-span-12 flex justify-start pt-6 border-t border-gray-200">
|
||||||
<RsButton
|
<RsButton
|
||||||
@click="validateAndSubmit"
|
@click="validateAndSubmit"
|
||||||
:disabled="stepLoading"
|
:disabled="stepLoading"
|
||||||
variant="primary"
|
:variant="getSubmitButtonVariant()"
|
||||||
|
:style="getWorkflowSubmitButtonStyle()"
|
||||||
class="col-span-12"
|
class="col-span-12"
|
||||||
>
|
>
|
||||||
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
|
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
|
||||||
{{ stepLoading ? 'Processing...' : 'Submit' }}
|
{{ stepLoading ? 'Processing...' : (currentForm?.submitButton?.label || 'Submit') }}
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1972,4 +2162,9 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
transition-duration: 300ms;
|
transition-duration: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Submit button hover effects using CSS custom properties */
|
||||||
|
:deep(.rs-button:hover) {
|
||||||
|
background-color: var(--hover-bg) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -1,73 +1,82 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"user": {
|
"caseInstance": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"userID": {
|
"caseID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"userSecretKey": {
|
"caseUUID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caseName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"caseStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "active"
|
||||||
|
},
|
||||||
|
"caseVariables": {
|
||||||
"type": [
|
"type": [
|
||||||
|
"number",
|
||||||
"string",
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"userUsername": {
|
"caseSettings": {
|
||||||
"type": [
|
"type": [
|
||||||
|
"number",
|
||||||
"string",
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"userPassword": {
|
"caseDefinition": {
|
||||||
"type": [
|
"type": [
|
||||||
|
"number",
|
||||||
"string",
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"userFullName": {
|
"caseCreatedDate": {
|
||||||
"type": [
|
"type": "string",
|
||||||
"string",
|
"format": "date-time"
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"userEmail": {
|
"caseModifiedDate": {
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"userPhone": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"userStatus": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"userCreatedDate": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"userModifiedDate": {
|
"caseCompletedDate": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"caseInstance": {
|
"startedBy": {
|
||||||
"type": "array",
|
"anyOf": [
|
||||||
"items": {
|
{
|
||||||
"$ref": "#/definitions/caseInstance"
|
"$ref": "#/definitions/user"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"$ref": "#/definitions/process"
|
||||||
},
|
},
|
||||||
"caseTimeline": {
|
"caseTimeline": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -75,123 +84,45 @@
|
|||||||
"$ref": "#/definitions/caseTimeline"
|
"$ref": "#/definitions/caseTimeline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forms": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/form"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formHistoryEntries": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/formHistory"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"processes": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/process"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"processHistoryEntries": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/processHistory"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"task": {
|
"task": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/task"
|
"$ref": "#/definitions/task"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"userrole": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/userrole"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"startedCases": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/caseInstance"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"assignedTasks": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/task"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"caseTimelineEntries": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/caseTimeline"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"role": {
|
"caseTimeline": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"roleID": {
|
"timelineID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"roleName": {
|
"timelineType": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timelineDescription": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"roleDescription": {
|
"timelineDate": {
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"roleStatus": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"roleCreatedDate": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
],
|
|
||||||
"format": "date-time"
|
|
||||||
},
|
|
||||||
"roleModifiedDate": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null"
|
|
||||||
],
|
|
||||||
"format": "date-time"
|
|
||||||
},
|
|
||||||
"userrole": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/userrole"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"userrole": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"userRoleID": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"userRoleCreatedDate": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"role": {
|
"caseInstance": {
|
||||||
"$ref": "#/definitions/role"
|
"$ref": "#/definitions/caseInstance"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"$ref": "#/definitions/user"
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -267,6 +198,38 @@
|
|||||||
],
|
],
|
||||||
"default": "safe"
|
"default": "safe"
|
||||||
},
|
},
|
||||||
|
"submitButton": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formCategory": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formTags": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formGroup": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"creator": {
|
"creator": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@ -277,13 +240,13 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"history": {
|
"formHistory": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/formHistory"
|
"$ref": "#/definitions/formHistory"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tasks": {
|
"task": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/task"
|
"$ref": "#/definitions/task"
|
||||||
@ -350,6 +313,16 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"submitButton": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"versionNumber": {
|
"versionNumber": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@ -366,7 +339,7 @@
|
|||||||
"form": {
|
"form": {
|
||||||
"$ref": "#/definitions/form"
|
"$ref": "#/definitions/form"
|
||||||
},
|
},
|
||||||
"savedByUser": {
|
"user": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/user"
|
"$ref": "#/definitions/user"
|
||||||
@ -507,17 +480,11 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"history": {
|
"processHistory": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/processHistory"
|
"$ref": "#/definitions/processHistory"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"cases": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/caseInstance"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -625,7 +592,7 @@
|
|||||||
"process": {
|
"process": {
|
||||||
"$ref": "#/definitions/process"
|
"$ref": "#/definitions/process"
|
||||||
},
|
},
|
||||||
"savedByUser": {
|
"user": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/user"
|
"$ref": "#/definitions/user"
|
||||||
@ -637,93 +604,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caseInstance": {
|
"role": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"caseID": {
|
"roleID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"caseUUID": {
|
"roleName": {
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"caseName": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"caseStatus": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "active"
|
|
||||||
},
|
|
||||||
"caseVariables": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"number",
|
|
||||||
"string",
|
"string",
|
||||||
"boolean",
|
|
||||||
"object",
|
|
||||||
"array",
|
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"caseSettings": {
|
"roleDescription": {
|
||||||
"type": [
|
"type": [
|
||||||
"number",
|
|
||||||
"string",
|
"string",
|
||||||
"boolean",
|
|
||||||
"object",
|
|
||||||
"array",
|
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"caseDefinition": {
|
"roleStatus": {
|
||||||
"type": [
|
"type": [
|
||||||
"number",
|
|
||||||
"string",
|
"string",
|
||||||
"boolean",
|
|
||||||
"object",
|
|
||||||
"array",
|
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"caseCreatedDate": {
|
"roleCreatedDate": {
|
||||||
"type": "string",
|
|
||||||
"format": "date-time"
|
|
||||||
},
|
|
||||||
"caseModifiedDate": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"caseCompletedDate": {
|
"roleModifiedDate": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"process": {
|
"userrole": {
|
||||||
"$ref": "#/definitions/process"
|
|
||||||
},
|
|
||||||
"startedBy": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/user"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"tasks": {
|
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/task"
|
"$ref": "#/definitions/userrole"
|
||||||
}
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/caseTimeline"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -775,10 +697,10 @@
|
|||||||
],
|
],
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"case": {
|
"caseInstance": {
|
||||||
"$ref": "#/definitions/caseInstance"
|
"$ref": "#/definitions/caseInstance"
|
||||||
},
|
},
|
||||||
"assignedTo": {
|
"user": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/user"
|
"$ref": "#/definitions/user"
|
||||||
@ -800,51 +722,144 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caseTimeline": {
|
"user": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"timelineID": {
|
"userID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"timelineType": {
|
"userSecretKey": {
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"timelineDescription": {
|
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"timelineDate": {
|
"userUsername": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userPassword": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userFullName": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userEmail": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userPhone": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userStatus": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userCreatedDate": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"userModifiedDate": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"caseInstance": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/caseInstance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"caseTimeline": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/caseTimeline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/form"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formHistory": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/formHistory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/process"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"processHistory": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/processHistory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/task"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userrole": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/userrole"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userrole": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"userRoleID": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"userRoleCreatedDate": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"case": {
|
"role": {
|
||||||
"$ref": "#/definitions/caseInstance"
|
"$ref": "#/definitions/role"
|
||||||
},
|
},
|
||||||
"createdBy": {
|
"user": {
|
||||||
"anyOf": [
|
"$ref": "#/definitions/user"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/user"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"user": {
|
"caseInstance": {
|
||||||
"$ref": "#/definitions/user"
|
"$ref": "#/definitions/caseInstance"
|
||||||
},
|
},
|
||||||
"role": {
|
"caseTimeline": {
|
||||||
"$ref": "#/definitions/role"
|
"$ref": "#/definitions/caseTimeline"
|
||||||
},
|
|
||||||
"userrole": {
|
|
||||||
"$ref": "#/definitions/userrole"
|
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"$ref": "#/definitions/form"
|
"$ref": "#/definitions/form"
|
||||||
@ -858,14 +873,17 @@
|
|||||||
"processHistory": {
|
"processHistory": {
|
||||||
"$ref": "#/definitions/processHistory"
|
"$ref": "#/definitions/processHistory"
|
||||||
},
|
},
|
||||||
"caseInstance": {
|
"role": {
|
||||||
"$ref": "#/definitions/caseInstance"
|
"$ref": "#/definitions/role"
|
||||||
},
|
},
|
||||||
"task": {
|
"task": {
|
||||||
"$ref": "#/definitions/task"
|
"$ref": "#/definitions/task"
|
||||||
},
|
},
|
||||||
"caseTimeline": {
|
"user": {
|
||||||
"$ref": "#/definitions/caseTimeline"
|
"$ref": "#/definitions/user"
|
||||||
|
},
|
||||||
|
"userrole": {
|
||||||
|
"$ref": "#/definitions/userrole"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
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`);
|
@ -12,50 +12,42 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model user {
|
model caseInstance {
|
||||||
userID Int @id @default(autoincrement())
|
caseID Int @id @default(autoincrement())
|
||||||
userSecretKey String? @db.VarChar(255)
|
caseUUID String @unique @db.VarChar(36)
|
||||||
userUsername String? @db.VarChar(255)
|
processID Int
|
||||||
userPassword String? @db.VarChar(255)
|
caseName String @db.VarChar(255)
|
||||||
userFullName String? @db.VarChar(255)
|
caseStatus String @default("active") @db.VarChar(50)
|
||||||
userEmail String? @db.VarChar(255)
|
caseStartedBy Int?
|
||||||
userPhone String? @db.VarChar(255)
|
caseVariables Json?
|
||||||
userStatus String? @db.VarChar(255)
|
caseSettings Json?
|
||||||
userCreatedDate DateTime? @db.DateTime(0)
|
caseDefinition Json?
|
||||||
userModifiedDate DateTime? @db.DateTime(0)
|
caseCreatedDate DateTime @default(now()) @db.DateTime(0)
|
||||||
caseInstance caseInstance[]
|
caseModifiedDate DateTime? @db.DateTime(0)
|
||||||
caseTimeline caseTimeline[]
|
caseCompletedDate DateTime? @db.DateTime(0)
|
||||||
forms form[] @relation("FormCreator")
|
startedBy user? @relation(fields: [caseStartedBy], references: [userID])
|
||||||
formHistoryEntries formHistory[]
|
process process @relation(fields: [processID], references: [processID])
|
||||||
processes process[] @relation("ProcessCreator")
|
caseTimeline caseTimeline[]
|
||||||
processHistoryEntries processHistory[]
|
task task[]
|
||||||
task task[]
|
|
||||||
userrole userrole[]
|
@@index([processID], map: "FK_case_process")
|
||||||
startedCases caseInstance[] @relation("CaseStartedBy")
|
@@index([caseStartedBy], map: "FK_case_startedBy")
|
||||||
assignedTasks task[] @relation("TaskAssignedTo")
|
@@index([caseStatus], map: "IDX_case_status")
|
||||||
caseTimelineEntries caseTimeline[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model role {
|
model caseTimeline {
|
||||||
roleID Int @id @default(autoincrement())
|
timelineID Int @id @default(autoincrement())
|
||||||
roleName String? @db.VarChar(255)
|
caseID Int
|
||||||
roleDescription String? @db.VarChar(255)
|
timelineType String @db.VarChar(50)
|
||||||
roleStatus String? @db.VarChar(255)
|
timelineDescription String? @db.Text
|
||||||
roleCreatedDate DateTime? @db.DateTime(0)
|
timelineDate DateTime @default(now()) @db.DateTime(0)
|
||||||
roleModifiedDate DateTime? @db.DateTime(0)
|
timelineCreatedBy Int?
|
||||||
userrole userrole[]
|
caseInstance caseInstance @relation(fields: [caseID], references: [caseID])
|
||||||
}
|
user user? @relation(fields: [timelineCreatedBy], references: [userID])
|
||||||
|
|
||||||
model userrole {
|
@@index([caseID], map: "FK_caseTimeline_case")
|
||||||
userRoleID Int @id @default(autoincrement())
|
@@index([timelineCreatedBy], map: "FK_caseTimeline_createdBy")
|
||||||
userRoleUserID Int @default(0)
|
@@index([timelineDate], map: "IDX_caseTimeline_date")
|
||||||
userRoleRoleID Int @default(0)
|
|
||||||
userRoleCreatedDate DateTime @db.DateTime(0)
|
|
||||||
role role @relation(fields: [userRoleRoleID], references: [roleID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_role")
|
|
||||||
user user @relation(fields: [userRoleUserID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_user")
|
|
||||||
|
|
||||||
@@index([userRoleRoleID], map: "FK_userrole_role")
|
|
||||||
@@index([userRoleUserID], map: "FK_userrole_user")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model form {
|
model form {
|
||||||
@ -67,16 +59,23 @@ model form {
|
|||||||
formStatus String @default("active") @db.VarChar(50)
|
formStatus String @default("active") @db.VarChar(50)
|
||||||
formCreatedBy Int?
|
formCreatedBy Int?
|
||||||
formCreatedDate DateTime @default(now()) @db.DateTime(0)
|
formCreatedDate DateTime @default(now()) @db.DateTime(0)
|
||||||
formModifiedDate DateTime? @updatedAt @db.DateTime(0)
|
formModifiedDate DateTime? @db.DateTime(0)
|
||||||
customCSS String? @db.Text
|
customCSS String? @db.Text
|
||||||
customScript String? @db.LongText
|
customScript String? @db.LongText
|
||||||
formEvents Json?
|
formEvents Json?
|
||||||
scriptMode String? @default("safe") @db.VarChar(20)
|
scriptMode String? @default("safe") @db.VarChar(20)
|
||||||
creator user? @relation("FormCreator", fields: [formCreatedBy], references: [userID])
|
submitButton Json?
|
||||||
history formHistory[] @relation("FormHistoryEntries")
|
formCategory String? @db.VarChar(100)
|
||||||
tasks task[]
|
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([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 {
|
||||||
@ -91,17 +90,18 @@ model formHistory {
|
|||||||
customScript String? @db.LongText
|
customScript String? @db.LongText
|
||||||
formEvents Json?
|
formEvents Json?
|
||||||
scriptMode String? @db.VarChar(20)
|
scriptMode String? @db.VarChar(20)
|
||||||
|
submitButton Json?
|
||||||
versionNumber Int
|
versionNumber Int
|
||||||
changeDescription String? @db.Text
|
changeDescription String? @db.Text
|
||||||
savedBy Int?
|
savedBy Int?
|
||||||
savedDate DateTime @default(now()) @db.DateTime(0)
|
savedDate DateTime @default(now()) @db.DateTime(0)
|
||||||
form form @relation("FormHistoryEntries", fields: [formID], references: [formID], onDelete: Cascade)
|
form form @relation(fields: [formID], references: [formID], onDelete: Cascade)
|
||||||
savedByUser user? @relation(fields: [savedBy], references: [userID])
|
user user? @relation(fields: [savedBy], references: [userID])
|
||||||
|
|
||||||
@@index([formID], map: "FK_formHistory_form")
|
@@index([formID], map: "FK_formHistory_form")
|
||||||
@@index([savedBy], map: "FK_formHistory_savedBy")
|
@@index([savedBy], map: "FK_formHistory_savedBy")
|
||||||
@@index([formUUID], map: "IDX_formHistory_uuid")
|
|
||||||
@@index([savedDate], map: "IDX_formHistory_date")
|
@@index([savedDate], map: "IDX_formHistory_date")
|
||||||
|
@@index([formUUID], map: "IDX_formHistory_uuid")
|
||||||
}
|
}
|
||||||
|
|
||||||
model process {
|
model process {
|
||||||
@ -114,7 +114,7 @@ model process {
|
|||||||
processStatus String @default("draft") @db.VarChar(50)
|
processStatus String @default("draft") @db.VarChar(50)
|
||||||
processCreatedBy Int?
|
processCreatedBy Int?
|
||||||
processCreatedDate DateTime @default(now()) @db.DateTime(0)
|
processCreatedDate DateTime @default(now()) @db.DateTime(0)
|
||||||
processModifiedDate DateTime? @updatedAt @db.DateTime(0)
|
processModifiedDate DateTime? @db.DateTime(0)
|
||||||
isTemplate Boolean @default(false)
|
isTemplate Boolean @default(false)
|
||||||
processCategory String? @db.VarChar(100)
|
processCategory String? @db.VarChar(100)
|
||||||
processOwner String? @db.VarChar(255)
|
processOwner String? @db.VarChar(255)
|
||||||
@ -125,13 +125,12 @@ model process {
|
|||||||
templateCategory String? @db.VarChar(100)
|
templateCategory String? @db.VarChar(100)
|
||||||
processDeletedDate DateTime? @db.DateTime(0)
|
processDeletedDate DateTime? @db.DateTime(0)
|
||||||
caseInstance caseInstance[]
|
caseInstance caseInstance[]
|
||||||
creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID])
|
creator user? @relation(fields: [processCreatedBy], references: [userID])
|
||||||
history processHistory[] @relation("ProcessHistoryEntries")
|
processHistory processHistory[]
|
||||||
cases caseInstance[]
|
|
||||||
|
|
||||||
@@index([processCreatedBy], map: "FK_process_creator")
|
@@index([processCreatedBy], map: "FK_process_creator")
|
||||||
@@index([processStatus], map: "IDX_process_status")
|
|
||||||
@@index([processCategory], map: "IDX_process_category")
|
@@index([processCategory], map: "IDX_process_category")
|
||||||
|
@@index([processStatus], map: "IDX_process_status")
|
||||||
@@index([isTemplate], map: "IDX_process_template")
|
@@index([isTemplate], map: "IDX_process_template")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,72 +154,77 @@ model processHistory {
|
|||||||
changeDescription String? @db.Text
|
changeDescription String? @db.Text
|
||||||
savedBy Int?
|
savedBy Int?
|
||||||
savedDate DateTime @default(now()) @db.DateTime(0)
|
savedDate DateTime @default(now()) @db.DateTime(0)
|
||||||
process process @relation("ProcessHistoryEntries", fields: [processID], references: [processID], onDelete: Cascade)
|
process process @relation(fields: [processID], references: [processID], onDelete: Cascade)
|
||||||
savedByUser user? @relation(fields: [savedBy], references: [userID])
|
user user? @relation(fields: [savedBy], references: [userID])
|
||||||
|
|
||||||
@@index([processID], map: "FK_processHistory_process")
|
@@index([processID], map: "FK_processHistory_process")
|
||||||
@@index([savedBy], map: "FK_processHistory_savedBy")
|
@@index([savedBy], map: "FK_processHistory_savedBy")
|
||||||
@@index([processUUID], map: "IDX_processHistory_uuid")
|
|
||||||
@@index([savedDate], map: "IDX_processHistory_date")
|
@@index([savedDate], map: "IDX_processHistory_date")
|
||||||
|
@@index([processUUID], map: "IDX_processHistory_uuid")
|
||||||
}
|
}
|
||||||
|
|
||||||
model caseInstance {
|
model role {
|
||||||
caseID Int @id @default(autoincrement())
|
roleID Int @id @default(autoincrement())
|
||||||
caseUUID String @unique @db.VarChar(36)
|
roleName String? @db.VarChar(255)
|
||||||
processID Int
|
roleDescription String? @db.VarChar(255)
|
||||||
caseName String @db.VarChar(255)
|
roleStatus String? @db.VarChar(255)
|
||||||
caseStatus String @default("active") @db.VarChar(50)
|
roleCreatedDate DateTime? @db.DateTime(0)
|
||||||
caseStartedBy Int?
|
roleModifiedDate DateTime? @db.DateTime(0)
|
||||||
caseVariables Json?
|
userrole userrole[]
|
||||||
caseSettings Json?
|
|
||||||
caseDefinition Json?
|
|
||||||
caseCreatedDate DateTime @default(now()) @db.DateTime(0)
|
|
||||||
caseModifiedDate DateTime? @updatedAt @db.DateTime(0)
|
|
||||||
caseCompletedDate DateTime? @db.DateTime(0)
|
|
||||||
process process @relation(fields: [processID], references: [processID])
|
|
||||||
startedBy user? @relation("CaseStartedBy", fields: [caseStartedBy], references: [userID])
|
|
||||||
tasks task[]
|
|
||||||
timeline caseTimeline[]
|
|
||||||
|
|
||||||
@@index([processID], map: "FK_case_process")
|
|
||||||
@@index([caseStartedBy], map: "FK_case_startedBy")
|
|
||||||
@@index([caseStatus], map: "IDX_case_status")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model task {
|
model task {
|
||||||
taskID Int @id @default(autoincrement())
|
taskID Int @id @default(autoincrement())
|
||||||
taskUUID String @unique @db.VarChar(36)
|
taskUUID String @unique @db.VarChar(36)
|
||||||
caseID Int
|
caseID Int
|
||||||
taskName String @db.VarChar(255)
|
taskName String @db.VarChar(255)
|
||||||
taskType String @db.VarChar(50)
|
taskType String @db.VarChar(50)
|
||||||
taskStatus String @default("pending") @db.VarChar(50)
|
taskStatus String @default("pending") @db.VarChar(50)
|
||||||
taskAssignedTo Int?
|
taskAssignedTo Int?
|
||||||
taskFormID Int?
|
taskFormID Int?
|
||||||
taskData Json?
|
taskData Json?
|
||||||
taskCreatedDate DateTime @default(now()) @db.DateTime(0)
|
taskCreatedDate DateTime @default(now()) @db.DateTime(0)
|
||||||
taskModifiedDate DateTime? @updatedAt @db.DateTime(0)
|
taskModifiedDate DateTime? @db.DateTime(0)
|
||||||
taskCompletedDate DateTime? @db.DateTime(0)
|
taskCompletedDate DateTime? @db.DateTime(0)
|
||||||
case caseInstance @relation(fields: [caseID], references: [caseID])
|
caseInstance caseInstance @relation(fields: [caseID], references: [caseID])
|
||||||
assignedTo user? @relation("TaskAssignedTo", fields: [taskAssignedTo], references: [userID])
|
user user? @relation(fields: [taskAssignedTo], references: [userID])
|
||||||
form form? @relation(fields: [taskFormID], references: [formID])
|
form form? @relation(fields: [taskFormID], references: [formID])
|
||||||
|
|
||||||
@@index([caseID], map: "FK_task_case")
|
|
||||||
@@index([taskAssignedTo], map: "FK_task_assignedTo")
|
@@index([taskAssignedTo], map: "FK_task_assignedTo")
|
||||||
|
@@index([caseID], map: "FK_task_case")
|
||||||
@@index([taskFormID], map: "FK_task_form")
|
@@index([taskFormID], map: "FK_task_form")
|
||||||
@@index([taskStatus], map: "IDX_task_status")
|
@@index([taskStatus], map: "IDX_task_status")
|
||||||
}
|
}
|
||||||
|
|
||||||
model caseTimeline {
|
model user {
|
||||||
timelineID Int @id @default(autoincrement())
|
userID Int @id @default(autoincrement())
|
||||||
caseID Int
|
userSecretKey String? @db.VarChar(255)
|
||||||
timelineType String @db.VarChar(50)
|
userUsername String? @db.VarChar(255)
|
||||||
timelineDescription String? @db.Text
|
userPassword String? @db.VarChar(255)
|
||||||
timelineDate DateTime @default(now()) @db.DateTime(0)
|
userFullName String? @db.VarChar(255)
|
||||||
timelineCreatedBy Int?
|
userEmail String? @db.VarChar(255)
|
||||||
case caseInstance @relation(fields: [caseID], references: [caseID])
|
userPhone String? @db.VarChar(255)
|
||||||
createdBy user? @relation(fields: [timelineCreatedBy], references: [userID])
|
userStatus String? @db.VarChar(255)
|
||||||
|
userCreatedDate DateTime? @db.DateTime(0)
|
||||||
@@index([caseID], map: "FK_caseTimeline_case")
|
userModifiedDate DateTime? @db.DateTime(0)
|
||||||
@@index([timelineCreatedBy], map: "FK_caseTimeline_createdBy")
|
caseInstance caseInstance[]
|
||||||
@@index([timelineDate], map: "IDX_caseTimeline_date")
|
caseTimeline caseTimeline[]
|
||||||
|
form form[]
|
||||||
|
formHistory formHistory[]
|
||||||
|
process process[]
|
||||||
|
processHistory processHistory[]
|
||||||
|
task task[]
|
||||||
|
userrole userrole[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model userrole {
|
||||||
|
userRoleID Int @id @default(autoincrement())
|
||||||
|
userRoleUserID Int @default(0)
|
||||||
|
userRoleRoleID Int @default(0)
|
||||||
|
userRoleCreatedDate DateTime @db.DateTime(0)
|
||||||
|
role role @relation(fields: [userRoleRoleID], references: [roleID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_role")
|
||||||
|
user user @relation(fields: [userRoleUserID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "FK_userrole_user")
|
||||||
|
|
||||||
|
@@index([userRoleRoleID], map: "FK_userrole_role")
|
||||||
|
@@index([userRoleUserID], map: "FK_userrole_user")
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
customScript: currentForm.customScript,
|
customScript: currentForm.customScript,
|
||||||
formEvents: currentForm.formEvents,
|
formEvents: currentForm.formEvents,
|
||||||
scriptMode: currentForm.scriptMode,
|
scriptMode: currentForm.scriptMode,
|
||||||
|
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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -104,6 +105,22 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (body.scriptMode !== undefined) {
|
if (body.scriptMode !== undefined) {
|
||||||
updateData.scriptMode = body.scriptMode;
|
updateData.scriptMode = body.scriptMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Try to update by UUID first
|
||||||
let form;
|
let form;
|
||||||
|
@ -25,11 +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,
|
||||||
|
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: {
|
||||||
|
84
server/api/process/[id]/unpublish.post.js
Normal file
84
server/api/process/[id]/unpublish.post.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
// Initialize Prisma client
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// Get the process ID from the route parameter
|
||||||
|
const processId = getRouterParam(event, 'id');
|
||||||
|
|
||||||
|
if (!processId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process ID is required'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the ID is a UUID or numeric ID
|
||||||
|
const isUUID = processId.length === 36 && processId.includes('-');
|
||||||
|
|
||||||
|
// First, get the current process to validate it can be unpublished
|
||||||
|
const currentProcess = await prisma.process.findFirst({
|
||||||
|
where: isUUID
|
||||||
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentProcess) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the process is currently published
|
||||||
|
if (currentProcess.processStatus !== 'published') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process is not currently published'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the process status to draft
|
||||||
|
const unpublishedProcess = await prisma.process.update({
|
||||||
|
where: isUUID
|
||||||
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) },
|
||||||
|
data: {
|
||||||
|
processStatus: 'draft'
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
creator: {
|
||||||
|
select: {
|
||||||
|
userID: true,
|
||||||
|
userFullName: true,
|
||||||
|
userUsername: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Process unpublished successfully',
|
||||||
|
process: unpublishedProcess
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unpublishing process:', error);
|
||||||
|
|
||||||
|
// Handle specific Prisma errors
|
||||||
|
if (error.code === 'P2025') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to unpublish process',
|
||||||
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
@ -53,6 +53,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Calculate pagination
|
// Calculate pagination
|
||||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
const take = parseInt(limit);
|
const take = parseInt(limit);
|
||||||
|
|
||||||
|
// If limit is very high (like 1000), return all processes without pagination
|
||||||
|
// This allows the frontend to handle client-side pagination
|
||||||
|
const shouldReturnAll = parseInt(limit) >= 1000;
|
||||||
|
|
||||||
// Build orderBy clause
|
// Build orderBy clause
|
||||||
const orderBy = {};
|
const orderBy = {};
|
||||||
@ -63,8 +67,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
prisma.process.findMany({
|
prisma.process.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy,
|
orderBy,
|
||||||
skip,
|
skip: shouldReturnAll ? 0 : skip,
|
||||||
take,
|
take: shouldReturnAll ? undefined : take,
|
||||||
select: {
|
select: {
|
||||||
processID: true,
|
processID: true,
|
||||||
processUUID: true,
|
processUUID: true,
|
||||||
@ -93,9 +97,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Calculate pagination info
|
// Calculate pagination info
|
||||||
const totalPages = Math.ceil(totalCount / take);
|
const totalPages = shouldReturnAll ? 1 : Math.ceil(totalCount / take);
|
||||||
const hasNextPage = parseInt(page) < totalPages;
|
const hasNextPage = shouldReturnAll ? false : parseInt(page) < totalPages;
|
||||||
const hasPrevPage = parseInt(page) > 1;
|
const hasPrevPage = shouldReturnAll ? false : parseInt(page) > 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
198
server/api/process/workflow-api-call.post.js
Normal file
198
server/api/process/workflow-api-call.post.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Workflow API Call Proxy Endpoint
|
||||||
|
*
|
||||||
|
* This endpoint acts as a proxy for API calls made during workflow execution.
|
||||||
|
* It handles the new API node structure with proper authorization and avoids CORS issues.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Helper function to substitute variables in a string
|
||||||
|
function substituteVariables(str, variables) {
|
||||||
|
if (typeof str !== 'string') return str;
|
||||||
|
|
||||||
|
// Replace {{variable}} first
|
||||||
|
str = str.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, varName) => {
|
||||||
|
const value = variables[varName];
|
||||||
|
if (value === undefined || value === null) return '';
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then replace {variable}
|
||||||
|
str = str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => {
|
||||||
|
const value = variables[varName];
|
||||||
|
if (value === undefined || value === null) return '';
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build API request from node data
|
||||||
|
function buildApiRequest(nodeData, variables) {
|
||||||
|
// 1. URL (with param substitution)
|
||||||
|
let url = substituteVariables(nodeData.apiUrl, variables);
|
||||||
|
|
||||||
|
// 2. Params (for GET, DELETE, etc.)
|
||||||
|
let params = Array.isArray(nodeData.params) ? nodeData.params : [];
|
||||||
|
if (params.length) {
|
||||||
|
const query = params
|
||||||
|
.filter(p => p.key)
|
||||||
|
.map(p => `${encodeURIComponent(substituteVariables(p.key, variables))}=${encodeURIComponent(substituteVariables(p.value, variables))}`)
|
||||||
|
.join('&');
|
||||||
|
if (query) {
|
||||||
|
url += (url.includes('?') ? '&' : '?') + query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Headers
|
||||||
|
let headers = {};
|
||||||
|
if (Array.isArray(nodeData.headers)) {
|
||||||
|
nodeData.headers.forEach(h => {
|
||||||
|
if (h.key) headers[substituteVariables(h.key, variables)] = substituteVariables(h.value, variables);
|
||||||
|
});
|
||||||
|
} else if (typeof nodeData.headers === 'object') {
|
||||||
|
headers = { ...nodeData.headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Authorization
|
||||||
|
if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') {
|
||||||
|
const auth = nodeData.authorization;
|
||||||
|
if (auth.type === 'bearer' && auth.token) {
|
||||||
|
headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`;
|
||||||
|
} else if (auth.type === 'basic') {
|
||||||
|
if (auth.token) {
|
||||||
|
// Basic Auth with token (JWT or other token)
|
||||||
|
headers['Authorization'] = `Basic ${substituteVariables(auth.token, variables)}`;
|
||||||
|
} else if (auth.username && auth.password) {
|
||||||
|
// Basic Auth with username/password
|
||||||
|
const token = Buffer.from(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`).toString('base64');
|
||||||
|
headers['Authorization'] = `Basic ${token}`;
|
||||||
|
}
|
||||||
|
} else if (auth.type === 'apiKey' && auth.key && auth.value) {
|
||||||
|
if (auth.in === 'header') {
|
||||||
|
headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables);
|
||||||
|
} else if (auth.in === 'query') {
|
||||||
|
url += (url.includes('?') ? '&' : '?') + `${encodeURIComponent(substituteVariables(auth.key, variables))}=${encodeURIComponent(substituteVariables(auth.value, variables))}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Body
|
||||||
|
let body;
|
||||||
|
if (nodeData.body && nodeData.body.type && nodeData.body.type !== 'none') {
|
||||||
|
if (['form-data', 'x-www-form-urlencoded'].includes(nodeData.body.type)) {
|
||||||
|
const dataArr = Array.isArray(nodeData.body.data) ? nodeData.body.data : [];
|
||||||
|
if (nodeData.body.type === 'form-data') {
|
||||||
|
// For server-side, we'll use URLSearchParams for form-data
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
dataArr.forEach(item => {
|
||||||
|
if (item.key) formData.append(substituteVariables(item.key, variables), substituteVariables(item.value, variables));
|
||||||
|
});
|
||||||
|
body = formData.toString();
|
||||||
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
} else {
|
||||||
|
// x-www-form-urlencoded
|
||||||
|
body = dataArr
|
||||||
|
.filter(item => item.key)
|
||||||
|
.map(item => `${encodeURIComponent(substituteVariables(item.key, variables))}=${encodeURIComponent(substituteVariables(item.value, variables))}`)
|
||||||
|
.join('&');
|
||||||
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
}
|
||||||
|
} else if (nodeData.body.type === 'raw') {
|
||||||
|
body = substituteVariables(nodeData.body.data, variables);
|
||||||
|
// Try to detect JSON
|
||||||
|
if (body && body.trim().startsWith('{')) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url, headers, body };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// Get request body
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
// Extract node configuration and process variables
|
||||||
|
const { nodeData, processVariables } = body;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!nodeData || !nodeData.apiUrl) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: 'Invalid API node configuration. Missing apiUrl.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the API request
|
||||||
|
const { url, headers, body: requestBody } = buildApiRequest(nodeData, processVariables);
|
||||||
|
const apiMethod = nodeData.apiMethod || 'GET';
|
||||||
|
const outputVariable = nodeData.outputVariable || 'apiResponse';
|
||||||
|
const errorVariable = nodeData.errorVariable || 'apiError';
|
||||||
|
const continueOnError = nodeData.continueOnError || false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Prepare fetch options
|
||||||
|
const fetchOptions = {
|
||||||
|
method: apiMethod,
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add body for non-GET requests
|
||||||
|
if (!['GET', 'HEAD'].includes(apiMethod) && requestBody) {
|
||||||
|
fetchOptions.body = requestBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the API call
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
|
||||||
|
// Get response data
|
||||||
|
let responseData;
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
responseData = await response.json();
|
||||||
|
} else {
|
||||||
|
responseData = await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare result
|
||||||
|
const result = {
|
||||||
|
success: response.ok,
|
||||||
|
data: responseData,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: Object.fromEntries([...response.headers.entries()])
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
result.error = {
|
||||||
|
message: `API call failed with status ${response.status}`,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
data: responseData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: error.message || 'An error occurred while making the API call',
|
||||||
|
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
@ -26,11 +26,24 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
},
|
},
|
||||||
scriptMode: 'safe', // 'safe' or 'advanced'
|
scriptMode: 'safe', // 'safe' or 'advanced'
|
||||||
|
|
||||||
|
// Submit button configuration
|
||||||
|
submitButton: {
|
||||||
|
enabled: true,
|
||||||
|
label: 'Submit',
|
||||||
|
category: 'primary', // primary, secondary
|
||||||
|
color: 'primary' // primary, secondary, success, info, warning, danger
|
||||||
|
},
|
||||||
|
|
||||||
// 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: {
|
||||||
@ -364,6 +377,30 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.formComponents.push(newComponent);
|
this.formComponents.push(newComponent);
|
||||||
|
|
||||||
|
// Initialize preview form data for repeating groups
|
||||||
|
if (newComponent.type === 'repeating-group' && newComponent.props?.name) {
|
||||||
|
const groupName = newComponent.props.name;
|
||||||
|
const minItems = newComponent.props.minItems || 1;
|
||||||
|
|
||||||
|
const initialGroups = [];
|
||||||
|
for (let i = 0; i < minItems; i++) {
|
||||||
|
const newGroup = {};
|
||||||
|
|
||||||
|
// Add fields from configuration
|
||||||
|
if (newComponent.props.fields) {
|
||||||
|
newComponent.props.fields.forEach(field => {
|
||||||
|
newGroup[field.name] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialGroups.push(newGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFormData = { ...this.previewFormData, [groupName]: initialGroups };
|
||||||
|
this.updatePreviewFormData(updatedFormData);
|
||||||
|
}
|
||||||
|
|
||||||
// Explicitly select the new component
|
// Explicitly select the new component
|
||||||
this.selectedComponentId = newComponentId;
|
this.selectedComponentId = newComponentId;
|
||||||
this.hasUnsavedChanges = true;
|
this.hasUnsavedChanges = true;
|
||||||
@ -638,15 +675,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) {
|
||||||
@ -668,11 +708,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,
|
||||||
// Add user info and change description for history tracking
|
submitButton: this.submitButton,
|
||||||
savedBy: 1, // TODO: Get from authenticated user
|
formCategory: this.formCategory,
|
||||||
changeDescription: this.lastChangeDescription || null
|
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
|
// Determine if this is a new form or an update
|
||||||
@ -746,10 +790,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 || '';
|
||||||
@ -761,6 +810,12 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
onValidation: false
|
onValidation: false
|
||||||
};
|
};
|
||||||
this.scriptMode = result.form.scriptMode || 'safe';
|
this.scriptMode = result.form.scriptMode || 'safe';
|
||||||
|
this.submitButton = result.form.submitButton || {
|
||||||
|
enabled: true,
|
||||||
|
label: 'Submit',
|
||||||
|
category: 'primary',
|
||||||
|
color: 'primary'
|
||||||
|
};
|
||||||
|
|
||||||
// Transform components from DB format to store format
|
// Transform components from DB format to store format
|
||||||
if (Array.isArray(result.form.formComponents)) {
|
if (Array.isArray(result.form.formComponents)) {
|
||||||
@ -770,6 +825,33 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize preview form data with repeating groups
|
||||||
|
const initialFormData = {};
|
||||||
|
this.formComponents.forEach(component => {
|
||||||
|
if (component.type === 'repeating-group' && component.props?.name) {
|
||||||
|
const groupName = component.props.name;
|
||||||
|
const minItems = component.props.minItems || 1;
|
||||||
|
|
||||||
|
const initialGroups = [];
|
||||||
|
for (let i = 0; i < minItems; i++) {
|
||||||
|
const newGroup = {};
|
||||||
|
|
||||||
|
// Add fields from configuration
|
||||||
|
if (component.props.fields) {
|
||||||
|
component.props.fields.forEach(field => {
|
||||||
|
newGroup[field.name] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialGroups.push(newGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialFormData[groupName] = initialGroups;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updatePreviewFormData(initialFormData);
|
||||||
|
|
||||||
// Clear and initialize history when loading a form
|
// Clear and initialize history when loading a form
|
||||||
this.actionHistory = [];
|
this.actionHistory = [];
|
||||||
this.currentHistoryIndex = -1;
|
this.currentHistoryIndex = -1;
|
||||||
@ -820,10 +902,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 = '';
|
||||||
@ -840,6 +927,9 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
this.actionHistory = [];
|
this.actionHistory = [];
|
||||||
this.currentHistoryIndex = -1;
|
this.currentHistoryIndex = -1;
|
||||||
|
|
||||||
|
// Clear preview form data
|
||||||
|
this.updatePreviewFormData({});
|
||||||
|
|
||||||
// Record the initial empty state
|
// Record the initial empty state
|
||||||
this.recordHistory('new_form', {
|
this.recordHistory('new_form', {
|
||||||
message: 'Created a new empty form'
|
message: 'Created a new empty form'
|
||||||
|
@ -633,6 +633,39 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpublish a process (change status from published to draft)
|
||||||
|
*/
|
||||||
|
async unpublishProcess(processId) {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/api/process/${processId}/unpublish`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Update local state if process exists in the array
|
||||||
|
const process = this.processes.find(p => p.id === processId);
|
||||||
|
if (process) {
|
||||||
|
process.status = 'draft';
|
||||||
|
process.updatedAt = response.process.processModifiedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current process if it's the same one
|
||||||
|
if (this.currentProcess && this.currentProcess.id === processId) {
|
||||||
|
this.currentProcess.status = 'draft';
|
||||||
|
this.currentProcess.updatedAt = response.process.processModifiedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Failed to unpublish process');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unpublishing process:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duplicate a process
|
* Duplicate a process
|
||||||
*/
|
*/
|
||||||
@ -1342,7 +1375,9 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
type: variable.type || 'string',
|
type: variable.type || 'string',
|
||||||
scope: variable.scope || 'global',
|
scope: variable.scope || 'global',
|
||||||
value: variable.value,
|
value: variable.value,
|
||||||
description: variable.description || ''
|
description: variable.description || '',
|
||||||
|
captureFromUrl: variable.captureFromUrl || false,
|
||||||
|
urlParameterName: variable.urlParameterName || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
this.unsavedChanges = true;
|
this.unsavedChanges = true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user