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"
|
||||
:mask="component.props.mask || undefined" :digits="component.props.digits || 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',
|
||||
options: 'space-y-0.5'
|
||||
} : {}" :class="{
|
||||
@ -335,7 +336,21 @@
|
||||
{{ component.props.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"
|
||||
class="button-component">
|
||||
<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
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
@ -218,6 +218,19 @@
|
||||
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
|
||||
v-if="showField('readonly')"
|
||||
type="switch"
|
||||
@ -553,6 +566,79 @@
|
||||
: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' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Action Script -->
|
||||
<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'],
|
||||
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'],
|
||||
rows: ['textarea'],
|
||||
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'],
|
||||
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)
|
||||
})
|
||||
|
||||
// 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
|
||||
const compatibilityGroups = {
|
||||
// Text-based inputs (can switch between each other)
|
||||
@ -2610,6 +2725,20 @@ const getDefaultPropsForType = (type) => {
|
||||
},
|
||||
paragraph: {
|
||||
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' }"
|
||||
/>
|
||||
</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>
|
||||
@ -1171,6 +1226,25 @@ const pendingNavigation = ref(null);
|
||||
const navigationTarget = ref(null);
|
||||
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
|
||||
const getSubmitButtonStyles = (category, color) => {
|
||||
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 || '',
|
||||
name: newComponent.props.name || '',
|
||||
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
|
||||
};
|
||||
|
||||
// Fetch published processes if this is a button component
|
||||
if (newComponent.type === 'button') {
|
||||
fetchPublishedProcesses();
|
||||
}
|
||||
}
|
||||
}, { 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'],
|
||||
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'],
|
||||
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'],
|
||||
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 = {
|
||||
label: `Enter ${formStore.selectedComponent.type} label`,
|
||||
name: `${formStore.selectedComponent.type}_field`,
|
||||
placeholder: 'Enter placeholder text...'
|
||||
placeholder: 'Enter placeholder text...',
|
||||
rows: '3'
|
||||
};
|
||||
|
||||
return placeholders[fieldName] || '';
|
||||
|
Loading…
x
Reference in New Issue
Block a user