fix(portal): review display, hover overlay, and drag ghost for complex field types
- Extract formatFieldValue helper for shared use between review step
and confirmation page — one source of truth for TAG_PICKER,
AVAILABILITY_PICKER, and SECTION_PRIORITY display, so the raw-ID
and [object Object] leaks from two parallel stringifiers can't
regress on either side.
- TAG_PICKER: lookup via field.available_tags (server-inlined).
- AVAILABILITY_PICKER: lookup via usePublicFormTimeSlots, strip
seconds. "Laden…" while the cache warms.
- SECTION_PRIORITY: defensive shape-guard prevents [object Object]
leaks, sorted priority-prefixed rendering ("1. Bar, 2. Hospitality").
- Subtle primary-tinted hover (4% primary, primary border) replacing
the near-black Vuetify default overlay on unranked section cards.
- Explicit ghost-class / drag-class / chosen-class on vuedraggable
with solid drag-clone + elevation shadow and a 30%-opacity silhouette
at the origin, so mid-drag text no longer overlaps.
- 17 new formatFieldValue unit assertions + 2 new FieldSectionPriority
assertions locking in the draggable classes and the disabled-card
toggle at max.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,7 +187,10 @@ function sectionNameFor(id: string): string {
|
||||
v-model="ranked"
|
||||
item-key="section_id"
|
||||
handle=".section-priority-handle"
|
||||
:animation="180"
|
||||
ghost-class="section-priority-ghost"
|
||||
chosen-class="section-priority-chosen"
|
||||
drag-class="section-priority-drag"
|
||||
:animation="150"
|
||||
:delay="100"
|
||||
:delay-on-touch-only="true"
|
||||
class="section-priority-ranked mb-4"
|
||||
@@ -318,12 +321,13 @@ function sectionNameFor(id: string): string {
|
||||
assistive tech, which Vuetify's :disabled would do. */
|
||||
.section-priority-unranked-card {
|
||||
cursor: pointer;
|
||||
transition: background-color 120ms;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.section-priority-unranked-card:hover,
|
||||
.section-priority-unranked-card:focus-visible {
|
||||
background-color: rgb(var(--v-theme-surface-variant));
|
||||
.section-priority-unranked-card:hover:not(.section-priority-unranked-disabled),
|
||||
.section-priority-unranked-card:focus-visible:not(.section-priority-unranked-disabled) {
|
||||
background-color: rgb(var(--v-theme-primary) / 0.04);
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.section-priority-unranked-disabled {
|
||||
@@ -331,13 +335,29 @@ function sectionNameFor(id: string): string {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.section-priority-unranked-disabled:hover,
|
||||
.section-priority-unranked-disabled:focus-visible {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.section-priority-rank {
|
||||
min-inline-size: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* vuedraggable drag states — SortableJS applies these classes. The
|
||||
defaults leave the drag-clone semi-transparent while the ghost
|
||||
stays solid at the origin, which produces text overlap mid-drag. */
|
||||
.section-priority-ghost {
|
||||
opacity: 0.3;
|
||||
background: rgb(var(--v-theme-surface-bright));
|
||||
}
|
||||
|
||||
.section-priority-drag {
|
||||
/* !important overrides SortableJS's inline opacity: 0.8 default so
|
||||
the drag-clone reads as solid + elevated. */
|
||||
opacity: 1 !important;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.section-priority-chosen {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import IdentityMatchBanner from './IdentityMatchBanner.vue'
|
||||
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
|
||||
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
|
||||
import { formatFieldValue } from '@/composables/formatFieldValue'
|
||||
import type { FormStep } from '@/composables/useFormSteps'
|
||||
import { usePublicFormToken } from '@/composables/publicFormInjection'
|
||||
import { FormFieldType } from '@/types/formBuilder'
|
||||
import type { FormValues, PublicFormField, PublicFormSubmissionIdentityMatch } from '@/types/formBuilder'
|
||||
|
||||
@@ -12,13 +16,20 @@ const props = defineProps<{
|
||||
identityMatch?: PublicFormSubmissionIdentityMatch | null
|
||||
}>()
|
||||
|
||||
function displayValue(field: PublicFormField): string {
|
||||
const v = props.values[field.slug]
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
if (Array.isArray(v)) return v.length > 0 ? v.map(String).join(', ') : '—'
|
||||
if (typeof v === 'boolean') return v ? 'Ja' : 'Nee'
|
||||
// TanStack Query calls — these hit the same cache the field components
|
||||
// populated during the form render (5-minute staleTime), so there's no
|
||||
// extra network round-trip on the confirmation page.
|
||||
const token = usePublicFormToken()
|
||||
const timeSlotsQuery = usePublicFormTimeSlots(token)
|
||||
const sectionsQuery = usePublicFormSections(token)
|
||||
|
||||
return String(v)
|
||||
function displayValue(field: PublicFormField): string {
|
||||
return formatFieldValue(
|
||||
field,
|
||||
props.values[field.slug],
|
||||
timeSlotsQuery.data.value,
|
||||
sectionsQuery.data.value,
|
||||
)
|
||||
}
|
||||
|
||||
function isAnswerable(field: PublicFormField): boolean {
|
||||
|
||||
Reference in New Issue
Block a user