Enhance Form Builder Submit Button Configuration and Variable Handling
- Introduced a new submit button configuration in the form builder, allowing users to enable/disable the default submit button and customize its label, category, and color. - Updated VariableBrowser.vue to support object property path input for variables, including validation and error handling for property paths. - Enhanced ApiNodeConfiguration.vue to prevent object path creation for output and error variables. - Improved workflow page to respect form builder submit button settings, ensuring consistent behavior across the application. - Added helper functions for managing submit button styles and variants, enhancing the overall user experience.
This commit is contained in:
parent
707d8fe3b0
commit
8f84b00a9e
@ -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();
|
||||||
|
@ -295,16 +295,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>
|
||||||
@ -576,6 +580,102 @@
|
|||||||
</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">
|
||||||
@ -995,6 +1095,23 @@ const pendingNavigation = ref(null);
|
|||||||
const navigationTarget = ref(null);
|
const navigationTarget = ref(null);
|
||||||
const activeSettingsTab = ref('info');
|
const activeSettingsTab = ref('info');
|
||||||
|
|
||||||
|
// 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 +1149,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' },
|
||||||
@ -1062,7 +1180,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 +1388,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;
|
||||||
|
|
||||||
@ -2634,6 +2777,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;
|
||||||
|
@ -1488,6 +1488,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>
|
||||||
@ -1662,16 +1695,17 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
:field-states="fieldStates"
|
:field-states="fieldStates"
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
@ -1964,4 +1998,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>
|
@ -198,6 +198,16 @@
|
|||||||
],
|
],
|
||||||
"default": "safe"
|
"default": "safe"
|
||||||
},
|
},
|
||||||
|
"submitButton": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"creator": {
|
"creator": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@ -281,6 +291,16 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"submitButton": {
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"versionNumber": {
|
"versionNumber": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
@ -64,6 +64,7 @@ model form {
|
|||||||
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)
|
||||||
|
submitButton Json?
|
||||||
creator user? @relation(fields: [formCreatedBy], references: [userID])
|
creator user? @relation(fields: [formCreatedBy], references: [userID])
|
||||||
formHistory formHistory[]
|
formHistory formHistory[]
|
||||||
task task[]
|
task task[]
|
||||||
@ -83,6 +84,7 @@ 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?
|
||||||
|
@ -58,6 +58,7 @@ 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: body.savedBy || currentForm.formCreatedBy,
|
||||||
@ -104,6 +105,10 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to update by UUID first
|
// Try to update by UUID first
|
||||||
let form;
|
let form;
|
||||||
|
@ -29,7 +29,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,6 +26,14 @@ 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: {},
|
||||||
|
|
||||||
@ -670,6 +678,7 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
|||||||
customCSS: this.formCustomCSS,
|
customCSS: this.formCustomCSS,
|
||||||
formEvents: this.formEvents,
|
formEvents: this.formEvents,
|
||||||
scriptMode: this.scriptMode,
|
scriptMode: this.scriptMode,
|
||||||
|
submitButton: this.submitButton,
|
||||||
// Add user info and change description for history tracking
|
// Add user info and change description for history tracking
|
||||||
savedBy: 1, // TODO: Get from authenticated user
|
savedBy: 1, // TODO: Get from authenticated user
|
||||||
changeDescription: this.lastChangeDescription || null
|
changeDescription: this.lastChangeDescription || null
|
||||||
@ -761,6 +770,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)) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user