feat(portal): implement TAG_PICKER, AVAILABILITY_PICKER, SECTION_PRIORITY field types
- FieldTagPicker: VAutocomplete multiple with grouped category slots, empty/null category normalised to "Overig", empty-state info alert when the server delivers no tags. - FieldAvailabilityPicker: date-grouped checkbox list, festival-aware via usePublicFormTimeSlots. Event-name subheaders only surface when the time-slots span multiple events. Time format strips seconds. - FieldSectionPriority: tap-to-rank + drag-to-reorder via vuedraggable for desktop; mobile tap-only. Renumbers priorities on every mutation. Self-heals malformed modelValue. UI soft cap via validation_rules.max_priorities clamped to the backend hard cap of 5. - FieldRenderer: three new types removed from isStubbed. - publicFormInjection: page-level provide/inject for the public token. - IdentityMatchBanner: prefers backend-provided Dutch copy with frontend defaults as defensive fallback. - FormConfirmation wires the banner inline. - usePublicFormTimeSlots and usePublicFormSections TanStack composables. - 40 new Vitest assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
apps/portal/src/composables/api/usePublicFormSections.ts
Normal file
27
apps/portal/src/composables/api/usePublicFormSections.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { PublicFormSectionOption } from '@/types/formBuilder'
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
// Sibling endpoint for SECTION_PRIORITY — festival-aware and dedup-by-name
|
||||
// per PublicFormController::sections (show_in_registration=true, standard).
|
||||
export function usePublicFormSections(token: Ref<string>) {
|
||||
return useQuery({
|
||||
queryKey: ['public-form', token, 'sections'],
|
||||
queryFn: async (): Promise<PublicFormSectionOption[]> => {
|
||||
const t = token.value
|
||||
if (!t) throw new Error('Missing public_token')
|
||||
const { data } = await apiClient.get<ApiResponse<PublicFormSectionOption[]>>(
|
||||
`/public/forms/${t}/sections`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: computed(() => !!token.value),
|
||||
staleTime: 1000 * 60 * 5,
|
||||
})
|
||||
}
|
||||
28
apps/portal/src/composables/api/usePublicFormTimeSlots.ts
Normal file
28
apps/portal/src/composables/api/usePublicFormTimeSlots.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { PublicFormTimeSlot } from '@/types/formBuilder'
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
// Sibling endpoint for AVAILABILITY_PICKER — festival-aware per
|
||||
// PublicFormController::timeSlots (parent + children, VOLUNTEER only).
|
||||
// Cached for 5 minutes; data is effectively static during a session.
|
||||
export function usePublicFormTimeSlots(token: Ref<string>) {
|
||||
return useQuery({
|
||||
queryKey: ['public-form', token, 'time-slots'],
|
||||
queryFn: async (): Promise<PublicFormTimeSlot[]> => {
|
||||
const t = token.value
|
||||
if (!t) throw new Error('Missing public_token')
|
||||
const { data } = await apiClient.get<ApiResponse<PublicFormTimeSlot[]>>(
|
||||
`/public/forms/${t}/time-slots`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: computed(() => !!token.value),
|
||||
staleTime: 1000 * 60 * 5,
|
||||
})
|
||||
}
|
||||
21
apps/portal/src/composables/publicFormInjection.ts
Normal file
21
apps/portal/src/composables/publicFormInjection.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
import { inject, provide } from 'vue'
|
||||
|
||||
// Page-level provide/inject for the public form token. Sibling-endpoint
|
||||
// fetches (time-slots, sections) read it instead of receiving it as a
|
||||
// prop through FieldRenderer, which would couple every renderer to every
|
||||
// new sibling resource.
|
||||
export const PUBLIC_FORM_TOKEN_KEY: InjectionKey<Ref<string>> = Symbol('PublicFormToken')
|
||||
|
||||
export function providePublicFormToken(token: Ref<string>): void {
|
||||
provide(PUBLIC_FORM_TOKEN_KEY, token)
|
||||
}
|
||||
|
||||
export function usePublicFormToken(): Ref<string> {
|
||||
const token = inject(PUBLIC_FORM_TOKEN_KEY)
|
||||
if (!token) {
|
||||
throw new Error('usePublicFormToken: no token provided. Did you forget providePublicFormToken in the page?')
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
Reference in New Issue
Block a user