feat: registration form field display_width and option descriptions
Add configurable column widths (full/half) and optional descriptions for radio/select/checkbox options on registration form fields. - Migration adds display_width column to both tables - FieldDisplayWidth enum with smart defaults per field type - normalized_options accessor for backwards-compatible option format - Portal form renderer uses display_width for VRow/VCol grid layout - Radio/select/checkbox options render with descriptions - Admin field editor supports display_width toggle and description input - System templates updated with appropriate widths and descriptions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1063,7 +1063,7 @@ async function onSubmit() {
|
||||
v-for="field in group.fields"
|
||||
:key="field.id"
|
||||
cols="12"
|
||||
:md="field.field_type === 'textarea' || field.field_type === 'checkbox' ? '12' : '6'"
|
||||
:md="field.display_width === 'half' ? 6 : 12"
|
||||
>
|
||||
<div :id="`reg-field-${field.slug}`">
|
||||
<VTextField
|
||||
@@ -1112,13 +1112,26 @@ async function onSubmit() {
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
:items="field.options ?? []"
|
||||
:items="field.normalized_options ?? []"
|
||||
item-title="label"
|
||||
item-value="label"
|
||||
:label="field.label + (field.is_required ? ' *' : '')"
|
||||
:hint="field.help_text ?? undefined"
|
||||
persistent-hint
|
||||
:error-messages="fieldErrors[field.slug]"
|
||||
:clearable="!field.is_required"
|
||||
/>
|
||||
>
|
||||
<template #item="{ props: itemProps, item }">
|
||||
<VListItem v-bind="itemProps">
|
||||
<template
|
||||
v-if="item.raw.description"
|
||||
#subtitle
|
||||
>
|
||||
{{ item.raw.description }}
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VSelect>
|
||||
|
||||
<VSelect
|
||||
v-else-if="field.field_type === 'multiselect'"
|
||||
@@ -1129,12 +1142,25 @@ async function onSubmit() {
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
:items="field.options ?? []"
|
||||
:items="field.normalized_options ?? []"
|
||||
item-title="label"
|
||||
item-value="label"
|
||||
:label="field.label + (field.is_required ? ' *' : '')"
|
||||
:hint="field.help_text ?? undefined"
|
||||
persistent-hint
|
||||
:error-messages="fieldErrors[field.slug]"
|
||||
/>
|
||||
>
|
||||
<template #item="{ props: itemProps, item }">
|
||||
<VListItem v-bind="itemProps">
|
||||
<template
|
||||
v-if="item.raw.description"
|
||||
#subtitle
|
||||
>
|
||||
{{ item.raw.description }}
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VSelect>
|
||||
|
||||
<div v-else-if="field.field_type === 'checkbox'">
|
||||
<div class="text-body-2 d-block mb-1 text-high-emphasis">
|
||||
@@ -1147,14 +1173,25 @@ async function onSubmit() {
|
||||
{{ field.help_text }}
|
||||
</p>
|
||||
<VCheckbox
|
||||
v-for="opt in (field.options ?? [])"
|
||||
:key="opt"
|
||||
:model-value="isCheckboxChecked(field.slug, opt)"
|
||||
v-for="opt in (field.normalized_options ?? [])"
|
||||
:key="opt.label"
|
||||
:model-value="isCheckboxChecked(field.slug, opt.label)"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
:label="opt"
|
||||
@update:model-value="(v: boolean | null) => toggleCheckboxOption(field.slug, opt, v)"
|
||||
/>
|
||||
@update:model-value="(v: boolean | null) => toggleCheckboxOption(field.slug, opt.label, v)"
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
<span class="text-body-1">{{ opt.label }}</span>
|
||||
<p
|
||||
v-if="opt.description"
|
||||
class="text-body-2 text-medium-emphasis mt-1 mb-0"
|
||||
>
|
||||
{{ opt.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</VCheckbox>
|
||||
<div
|
||||
v-if="fieldErrors[field.slug]"
|
||||
class="text-caption text-error"
|
||||
@@ -1180,13 +1217,24 @@ async function onSubmit() {
|
||||
:error-messages="fieldErrors[field.slug]"
|
||||
>
|
||||
<VRadio
|
||||
v-for="opt in (field.options ?? [])"
|
||||
:key="opt"
|
||||
:label="opt"
|
||||
:value="opt"
|
||||
v-for="opt in (field.normalized_options ?? [])"
|
||||
:key="opt.label"
|
||||
:value="opt.label"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
>
|
||||
<template #label>
|
||||
<div>
|
||||
<span class="text-body-1">{{ opt.label }}</span>
|
||||
<p
|
||||
v-if="opt.description"
|
||||
class="text-body-2 text-medium-emphasis mt-1 mb-0"
|
||||
>
|
||||
{{ opt.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</VRadio>
|
||||
</VRadioGroup>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,22 +9,31 @@ export type RegistrationFieldType =
|
||||
| 'number'
|
||||
| 'tag_picker'
|
||||
|
||||
export type FieldDisplayWidth = 'full' | 'half'
|
||||
|
||||
export interface RegistrationTagOption {
|
||||
id: string
|
||||
name: string
|
||||
category: string | null
|
||||
}
|
||||
|
||||
export interface NormalizedOption {
|
||||
label: string
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export interface RegistrationField {
|
||||
id: string
|
||||
label: string
|
||||
slug: string
|
||||
field_type: RegistrationFieldType
|
||||
options: string[] | null
|
||||
options: (string | { label: string; description?: string | null })[] | null
|
||||
normalized_options: NormalizedOption[] | null
|
||||
tag_category: string | null
|
||||
is_required: boolean
|
||||
section: string | null
|
||||
help_text: string | null
|
||||
display_width: FieldDisplayWidth
|
||||
available_tags?: RegistrationTagOption[]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user