Enhance Info Display Component Functionality

- Updated ComponentPreview.vue to include optional component labels and help text for improved user guidance.
- Enhanced layout options for the info display component, adding support for side-by-side, grid, and horizontal layouts, with button functionality for fields.
- Modified FormBuilderComponents.vue to update the description and default properties for the info display component, including new fields for button configuration.
- Improved FormBuilderConfiguration.vue to add essential settings for the info display, including component title, name, label, help text, and layout style.
- Enhanced FormBuilderFieldSettingsModal.vue to support configuration of information fields, including field type selection (text or button) and button-specific settings.
- Added robust error handling for button click actions in the info display, ensuring a seamless user experience when interacting with links.
- Refined styles across components for better visual consistency and usability.
This commit is contained in:
Afiq 2025-08-07 14:45:35 +08:00
parent d0cef85b72
commit bc7daed988
4 changed files with 505 additions and 49 deletions

View File

@ -66,6 +66,16 @@
<!-- Information Display -->
<div v-else-if="component.type === 'info-display'" class="py-2">
<!-- Component Label -->
<div v-if="component.props.label && component.props.label !== 'Info Display'" class="mb-2">
<label class="text-sm font-medium text-gray-700">{{ component.props.label }}</label>
</div>
<!-- Help Text -->
<div v-if="component.props.help" class="mb-2 text-xs text-gray-600">
{{ component.props.help }}
</div>
<div class="info-display-container rounded" :style="{
backgroundColor: component.props.backgroundColor || '#f9fafb',
border: component.props.showBorder ? '1px solid #e5e7eb' : 'none'
@ -77,25 +87,84 @@
<!-- Fields Display -->
<div class="p-4">
<div v-if="component.props.layout === 'grid'" class="grid grid-cols-2 gap-4">
<!-- Side by Side Layout -->
<div v-if="component.props.layout === 'side-by-side'" class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
<dd class="text-sm text-gray-900 mt-1">{{ field.value }}</dd>
<dt class="text-sm font-medium text-gray-600 mb-2">{{ field.label }}</dt>
<dd class="text-sm text-gray-900">
<span v-if="field.type === 'button' && field.url" class="block">
<button
@click="handleInfoButtonClick(field.url, field.openInNewTab)"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
:target="field.openInNewTab ? '_blank' : '_self'"
>
<Icon v-if="field.icon" :name="field.icon" class="w-4 h-4 mr-1" />
{{ field.value }}
</button>
</span>
<span v-else>{{ field.value }}</span>
</dd>
</div>
</div>
<!-- Grid Layout -->
<div v-else-if="component.props.layout === 'grid'" class="grid grid-cols-2 gap-4">
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
<dd class="text-sm text-gray-900 mt-1">
<span v-if="field.type === 'button' && field.url" class="block">
<button
@click="handleInfoButtonClick(field.url, field.openInNewTab)"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
:target="field.openInNewTab ? '_blank' : '_self'"
>
<Icon v-if="field.icon" :name="field.icon" class="w-4 h-4 mr-1" />
{{ field.value }}
</button>
</span>
<span v-else>{{ field.value }}</span>
</dd>
</div>
</div>
<!-- Horizontal Layout -->
<div v-else-if="component.props.layout === 'horizontal'" class="space-y-2">
<div v-for="(field, index) in component.props.fields" :key="index"
class="flex justify-between items-center">
<dt class="text-sm font-medium text-gray-600">{{ field.label }}:</dt>
<dd class="text-sm text-gray-900">{{ field.value }}</dd>
<dd class="text-sm text-gray-900">
<span v-if="field.type === 'button' && field.url" class="block">
<button
@click="handleInfoButtonClick(field.url, field.openInNewTab)"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
:target="field.openInNewTab ? '_blank' : '_self'"
>
<Icon v-if="field.icon" :name="field.icon" class="w-4 h-4 mr-1" />
{{ field.value }}
</button>
</span>
<span v-else>{{ field.value }}</span>
</dd>
</div>
</div>
<!-- Vertical Layout (Default) -->
<div v-else class="space-y-3">
<div v-for="(field, index) in component.props.fields" :key="index" class="field-item">
<dt class="text-sm font-medium text-gray-600">{{ field.label }}</dt>
<dd class="text-sm text-gray-900 mt-1">{{ field.value }}</dd>
<dd class="text-sm text-gray-900 mt-1">
<span v-if="field.type === 'button' && field.url" class="block">
<button
@click="handleInfoButtonClick(field.url, field.openInNewTab)"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
:target="field.openInNewTab ? '_blank' : '_self'"
>
<Icon v-if="field.icon" :name="field.icon" class="w-4 h-4 mr-1" />
{{ field.value }}
</button>
</span>
<span v-else>{{ field.value }}</span>
</dd>
</div>
</div>
</div>
@ -1470,6 +1539,21 @@ const updateRepeatingGroupField = (groupName, groupIndex, fieldName, value) => {
// Duplicate RepeatingGroupContainer definition removed
// Handle info display button clicks
const handleInfoButtonClick = (url, openInNewTab = false) => {
if (!url) return;
try {
if (openInNewTab) {
window.open(url, '_blank', 'noopener,noreferrer');
} else {
window.location.href = url;
}
} catch (error) {
console.error('Error opening URL:', error);
}
};
// Update table data for repeating-table component
const updateTableData = (newData) => {
const tableName = props.component.props.name;

View File

@ -707,17 +707,22 @@ const availableComponents = [
name: 'Info Display',
category: 'Layout',
icon: 'material-symbols:info-outline',
description: 'Display read-only information in key-value format',
description: 'Display read-only information in key-value format with optional buttons',
defaultProps: {
name: 'info_display',
title: 'Information',
help: 'Display important information in an organized format',
fields: [
{ label: 'Customer Name', value: 'John Doe', key: 'customer_name' },
{ label: 'Email', value: 'john@example.com', key: 'customer_email' },
{ label: 'Phone', value: '+1-234-567-8900', key: 'customer_phone' }
{ label: 'Customer Name', value: 'John Doe', key: 'customer_name', type: 'text' },
{ label: 'Email', value: 'john@example.com', key: 'customer_email', type: 'text' },
{ label: 'Phone', value: '+1-234-567-8900', key: 'customer_phone', type: 'text' },
{ label: 'View Profile', value: 'View Details', key: 'profile_link', type: 'button', url: 'https://example.com/profile', openInNewTab: true, icon: 'material-symbols:link' }
],
layout: 'vertical', // vertical, horizontal, grid
layout: 'side-by-side', // vertical, horizontal, grid, side-by-side
showBorder: true,
backgroundColor: '#f9fafb'
backgroundColor: '#f9fafb',
width: '100%',
gridColumn: 'span 12'
}
},
{

View File

@ -589,45 +589,79 @@
<!-- Information Display Configuration -->
<template v-if="component.type === 'info-display'">
<FormKit
type="text"
label="Title"
name="title"
v-model="configModel.title"
help="Title displayed at the top"
/>
<FormKit
type="select"
label="Layout"
name="layout"
v-model="configModel.layout"
:options="[
{ label: 'Vertical (Label above value)', value: 'vertical' },
{ label: 'Horizontal (Label: Value)', value: 'horizontal' },
{ label: 'Grid (2 columns)', value: 'grid' }
]"
help="How to display the information fields"
/>
<div class="grid grid-cols-2 gap-4">
<!-- Essential Settings for Info Display -->
<div class="space-y-4">
<FormKit
type="checkbox"
label="Show Border"
name="showBorder"
v-model="configModel.showBorder"
help="Show border around the information display"
type="text"
label="Component Title"
name="title"
v-model="configModel.title"
help="Title displayed at the top of the information display"
placeholder="e.g., User Information, Contact Details"
/>
<FormKit
type="color"
label="Background Color"
name="backgroundColor"
v-model="configModel.backgroundColor"
help="Background color"
type="text"
label="Component Name (Internal)"
name="name"
v-model="configModel.name"
help="Used internally to identify this component"
validation="required|alpha_numeric"
placeholder="e.g., user_info, contact_details"
/>
<FormKit
type="textarea"
label="Help Text"
name="help"
v-model="configModel.help"
help="Additional information or instructions for users"
placeholder="e.g., This section displays your profile information"
rows="2"
/>
<FormKit
type="select"
label="Layout Style"
name="layout"
v-model="configModel.layout"
:options="[
{ label: 'Vertical (Label above value)', value: 'vertical' },
{ label: 'Horizontal (Label: Value)', value: 'horizontal' },
{ label: 'Grid (2 columns)', value: 'grid' },
{ label: 'Side by Side (2 columns)', value: 'side-by-side' }
]"
help="How to display the information fields"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="checkbox"
label="Show Border"
name="showBorder"
v-model="configModel.showBorder"
help="Show border around the information display"
/>
<FormKit
type="color"
label="Background Color"
name="backgroundColor"
v-model="configModel.backgroundColor"
help="Background color for the information display"
/>
</div>
<FormKit
type="text"
label="Component Label"
name="label"
v-model="configModel.label"
help="Label displayed above the component (optional)"
placeholder="e.g., User Information Section"
/>
<!-- Information Fields Management -->
<div>
<div class="flex justify-between items-center mb-2">
@ -661,7 +695,23 @@
:classes="{ outer: 'mb-0' }"
/>
</div>
<div class="flex items-center">
<!-- Field Type Selection -->
<div class="mb-2">
<FormKit
type="select"
label="Field Type"
v-model="field.type"
:options="[
{ label: 'Text', value: 'text' },
{ label: 'Button', value: 'button' }
]"
:classes="{ outer: 'mb-0' }"
/>
</div>
<!-- Text Field Value -->
<div v-if="field.type === 'text'" class="flex items-center">
<FormKit
type="text"
placeholder="Value (e.g., John Doe)"
@ -676,6 +726,50 @@
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
</button>
</div>
<!-- Button Field Configuration -->
<div v-if="field.type === 'button'" class="space-y-2">
<div class="flex items-center">
<FormKit
type="text"
placeholder="Button Text (e.g., View Details)"
v-model="field.value"
:classes="{ outer: 'mb-0 flex-1' }"
/>
<button
class="ml-2 p-1 text-gray-400 hover:text-red-500 rounded"
@click="removeInfoField(index)"
title="Remove field"
>
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
</button>
</div>
<div class="grid grid-cols-1 gap-2">
<FormKit
type="text"
placeholder="URL (e.g., https://example.com)"
v-model="field.url"
:classes="{ outer: 'mb-0' }"
/>
<div class="flex items-center space-x-4">
<FormKit
type="checkbox"
label="Open in new tab"
v-model="field.openInNewTab"
:classes="{ outer: 'mb-0' }"
/>
<FormKit
type="text"
placeholder="Icon (optional, e.g., material-symbols:link)"
v-model="field.icon"
:classes="{ outer: 'mb-0 flex-1' }"
/>
</div>
</div>
</div>
</div>
<div v-if="!configModel.fields || configModel.fields.length === 0" class="p-3 text-center text-gray-500 text-xs">
@ -1103,7 +1197,11 @@ const addInfoField = () => {
configModel.value.fields.push({
label: `Field ${configModel.value.fields.length + 1}`,
value: 'Value',
key: `field_${configModel.value.fields.length + 1}`
key: `field_${configModel.value.fields.length + 1}`,
type: 'text', // text, button
url: '', // for button type
openInNewTab: false, // for button type
icon: '' // for button type
});
};

View File

@ -156,6 +156,68 @@
/>
</template>
<!-- Info Display-specific Essential Settings -->
<template v-else-if="component.type === 'info-display'">
<div class="space-y-4">
<FormKit
type="text"
label="Component Title"
name="title"
v-model="configModel.title"
help="Title displayed at the top of the information display"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., User Information, Contact Details"
/>
<FormKit
type="text"
label="Component Name (Internal)"
name="name"
v-model="configModel.name"
help="Used internally to identify this component"
validation="required|alpha_numeric"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., user_info, contact_details"
/>
<FormKit
type="text"
label="Component Label"
name="label"
v-model="configModel.label"
help="Label displayed above the component (optional)"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., User Information Section"
/>
<FormKit
type="textarea"
label="Help Text"
name="help"
v-model="configModel.help"
help="Additional information or instructions for users"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., This section displays your profile information"
rows="2"
/>
<FormKit
type="select"
label="Layout Style"
name="layout"
v-model="configModel.layout"
:options="[
{ label: 'Vertical (Label above value)', value: 'vertical' },
{ label: 'Horizontal (Label: Value)', value: 'horizontal' },
{ label: 'Grid (2 columns)', value: 'grid' },
{ label: 'Side by Side (2 columns)', value: 'side-by-side' }
]"
help="How to display the information fields"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
</template>
<!-- Standard form field Essential Settings -->
<template v-else>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -2143,6 +2205,188 @@ if (this.element.querySelector('.file-upload')) {
</div>
</div>
</template>
<!-- Info Display Specific Settings -->
<template v-if="component.type === 'info-display'">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<FormKit
type="checkbox"
label="Show Border"
name="showBorder"
v-model="configModel.showBorder"
help="Show border around the information display"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="color"
label="Background Color"
name="backgroundColor"
v-model="configModel.backgroundColor"
help="Background color for the information display"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
<!-- Information Fields Management -->
<div class="info-fields-section">
<div class="flex justify-between items-center mb-4">
<div>
<h5 class="text-sm font-semibold text-gray-800">Information Fields</h5>
<p class="text-xs text-gray-500 mt-1">Configure the information that will be displayed</p>
</div>
<button
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
@click="addInfoField"
>
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
Add Field
</button>
</div>
<!-- Fields List -->
<div v-if="configModel.fields && configModel.fields.length > 0" class="space-y-4">
<div
v-for="(field, index) in configModel.fields"
:key="index"
class="field-card bg-white border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors"
>
<!-- Field Header -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">
<Icon
:name="field.type === 'button' ? 'material-symbols:smart-button' : 'material-symbols:text-fields'"
class="w-4 h-4 text-blue-600"
/>
</div>
<div>
<h6 class="text-sm font-medium text-gray-800">
{{ field.label || `Field ${index + 1}` }}
</h6>
<p class="text-xs text-gray-500">
{{ field.type === 'button' ? 'Button Field' : 'Text Field' }}
</p>
</div>
</div>
<button
class="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
@click="removeInfoField(index)"
title="Remove field"
>
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
</button>
</div>
<!-- Field Configuration -->
<div class="space-y-3">
<!-- Basic Info -->
<div class="grid grid-cols-2 gap-3">
<FormKit
type="text"
label="Display Label"
v-model="field.label"
placeholder="e.g., Customer Name"
:classes="{ outer: 'mb-0' }"
/>
<FormKit
type="text"
label="Field Key"
v-model="field.key"
placeholder="e.g., customer_name"
:classes="{ outer: 'mb-0' }"
/>
</div>
<!-- Field Type -->
<FormKit
type="select"
label="Field Type"
v-model="field.type"
:options="[
{ label: '📝 Text Information', value: 'text' },
{ label: '🔗 Action Button', value: 'button' }
]"
:classes="{ outer: 'mb-0' }"
/>
<!-- Text Field Configuration -->
<div v-if="field.type === 'text'">
<FormKit
type="text"
label="Display Value"
v-model="field.value"
placeholder="e.g., John Doe, 555-123-4567"
help="The text that will be shown to users"
:classes="{ outer: 'mb-0' }"
/>
</div>
<!-- Button Field Configuration -->
<div v-if="field.type === 'button'" class="space-y-3 p-3 bg-blue-50 rounded-lg border border-blue-200">
<div class="flex items-center mb-2">
<Icon name="material-symbols:settings" class="w-4 h-4 text-blue-600 mr-2" />
<span class="text-sm font-medium text-blue-800">Button Settings</span>
</div>
<FormKit
type="text"
label="Button Text"
v-model="field.value"
placeholder="e.g., View Profile, Download PDF"
help="Text displayed on the button"
:classes="{ outer: 'mb-0' }"
/>
<FormKit
type="text"
label="Button URL"
v-model="field.url"
placeholder="e.g., https://example.com/profile"
help="Where the button should redirect to"
:classes="{ outer: 'mb-0' }"
/>
<div class="grid grid-cols-2 gap-3">
<FormKit
type="checkbox"
label="Open in new tab"
v-model="field.openInNewTab"
help="Opens link in new window"
:classes="{ outer: 'mb-0' }"
/>
<FormKit
type="text"
label="Icon (optional)"
v-model="field.icon"
placeholder="material-symbols:link"
help="Icon name to display"
:classes="{ outer: 'mb-0' }"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="empty-fields-state text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
<Icon name="material-symbols:info-outline" class="w-12 h-12 text-gray-400 mx-auto mb-3" />
<h6 class="text-sm font-medium text-gray-600 mb-1">No information fields yet</h6>
<p class="text-xs text-gray-500 mb-4">Add fields to display information to your users</p>
<button
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
@click="addInfoField"
>
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
Add Your First Field
</button>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
@ -2337,6 +2581,8 @@ if (this.element.querySelector('.file-upload')) {
placeholder="e.g., required|email|length:3,50"
/>
<!-- Validation Rules Guide -->
<ValidationRulesHelp />
</div>
@ -2702,10 +2948,10 @@ const showField = (fieldName) => {
if (!props.component) return false
const fieldConfig = {
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
label: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display'],
name: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display'],
placeholder: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', 'dynamic-list'],
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml'],
help: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'hidden', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display'],
value: ['heading', 'paragraph', 'hidden'],
width: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'mask', 'select', 'searchSelect', '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', 'customHtml'],
rows: ['textarea'],
@ -2720,7 +2966,7 @@ const showField = (fieldName) => {
const hasOptions = computed(() => showField('options'))
const hasSpecificSettings = computed(() => {
if (!props.component) return false
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml']
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'button', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group', 'customHtml', 'info-display']
return specificTypes.includes(props.component.type)
})
@ -3210,6 +3456,29 @@ const removeCondition = (index) => {
}
}
// Info Display field management
const addInfoField = () => {
if (!configModel.value.fields) {
configModel.value.fields = []
}
configModel.value.fields.push({
label: `Field ${configModel.value.fields.length + 1}`,
value: 'Value',
key: `field_${configModel.value.fields.length + 1}`,
type: 'text', // text, button
url: '', // for button type
openInNewTab: false, // for button type
icon: '' // for button type
})
}
const removeInfoField = (index) => {
if (configModel.value.fields && configModel.value.fields.length > index) {
configModel.value.fields.splice(index, 1)
}
}
const generateConditionalLogicCode = () => {
if (!configModel.value.conditionalLogic || !configModel.value.conditionalLogic.conditions.length) {
return '// No conditions defined'