Enhance Button Configuration and Link Functionality in Form Builder
- Introduced new settings for button components in the FormBuilderFieldSettingsModal, allowing users to configure link types (none, custom URL, process link) and corresponding URLs or process IDs. - Added functionality to dynamically generate button links based on user selections, improving the flexibility of button actions in the form builder. - Updated ComponentPreview.vue to conditionally render buttons as links or regular buttons based on the new configuration options. - Enhanced the form builder interface to include fields for specifying the number of rows for textareas, improving usability and customization. - Implemented fetching of published processes for linking, ensuring users can select from available processes when configuring button actions.
This commit is contained in:
parent
a006b66d02
commit
3f452a46a3
@ -32,7 +32,8 @@
|
|||||||
: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="{
|
} : {}" :class="{
|
||||||
@ -335,7 +336,21 @@
|
|||||||
{{ component.props.label }}
|
{{ component.props.label }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<RsButton :type="component.props.buttonType || 'button'" :variant="component.props.variant || 'primary'"
|
<!-- 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"
|
:size="component.props.size || 'md'" :disabled="component.props.disabled || false" @click="handleButtonClick"
|
||||||
class="button-component">
|
class="button-component">
|
||||||
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
|
<span v-if="component.props.showButtonText !== false">{{ component.props.buttonText || component.props.label || 'Button' }}</span>
|
||||||
@ -1245,6 +1260,24 @@ 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 } = props.component.props;
|
||||||
|
|
||||||
|
if (linkType === 'url' && linkUrl) {
|
||||||
|
return linkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkType === 'process' && linkProcessId) {
|
||||||
|
// Generate the process workflow URL
|
||||||
|
return `${window.location.origin}/workflow/${linkProcessId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -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"
|
||||||
@ -553,6 +566,79 @@
|
|||||||
:classes="{ outer: 'field-wrapper' }"
|
:classes="{ outer: 'field-wrapper' }"
|
||||||
placeholder="e.g., Submit, Save, Continue"
|
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' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Button Action Script -->
|
<!-- Button Action Script -->
|
||||||
<div class="mt-6 space-y-3">
|
<div class="mt-6 space-y-3">
|
||||||
@ -1849,6 +1935,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']
|
||||||
@ -1892,6 +1979,34 @@ 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 })
|
||||||
|
|
||||||
// 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)
|
||||||
@ -2610,6 +2725,20 @@ 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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,6 +504,61 @@
|
|||||||
: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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1171,6 +1226,25 @@ 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 = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get submit button CSS classes and styles based on category and color
|
// Helper function to get submit button CSS classes and styles based on category and color
|
||||||
const getSubmitButtonStyles = (category, color) => {
|
const getSubmitButtonStyles = (category, color) => {
|
||||||
const baseClasses = 'px-4 py-2 rounded font-medium transition-all duration-200 text-white border-0';
|
const baseClasses = 'px-4 py-2 rounded font-medium transition-all duration-200 text-white border-0';
|
||||||
@ -2789,8 +2863,17 @@ 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 || '',
|
||||||
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 });
|
||||||
|
|
||||||
@ -2807,6 +2890,10 @@ 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'],
|
||||||
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']
|
||||||
};
|
};
|
||||||
@ -2821,7 +2908,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] || '';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user