fix: shift edit time slot dropdown loading state and test coverage

The time slot dropdown in the shift edit dialog could flash the
"create a time slot first" alert during loading, and show raw ULIDs
when time slot data hadn't loaded yet. Fixed by:
- Adding loading state indicator to the time slot dropdown
- Using the shift's existing time_slot object as a fallback item
  while the full list is fetching
- Showing the dropdown (with loading spinner) instead of the
  misleading "no time slots" alert during fetch

Added test coverage for time_slot_id validation on shift updates:
- Update with valid same-event time slot (200)
- Update with cross-org time slot (422)
- Update on sub-event with parent festival time slot (200)
- Store/update responses include nested time_slot object

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 20:46:36 +02:00
parent ea159a34fe
commit cf02500453
3 changed files with 132 additions and 15 deletions

View File

@@ -28,7 +28,7 @@ const sectionIdRef = computed(() => props.sectionId)
const isEditing = computed(() => !!props.shift)
const isSubEventRef = computed(() => props.isSubEvent)
const { data: timeSlots } = useTimeSlotList(orgIdRef, eventIdRef, { includeParent: isSubEventRef })
const { data: timeSlots, isLoading: timeSlotsLoading } = useTimeSlotList(orgIdRef, eventIdRef, { includeParent: isSubEventRef })
const { mutate: createShift, isPending: isCreating } = useCreateShift(orgIdRef, eventIdRef, sectionIdRef)
const { mutate: updateShift, isPending: isUpdating } = useUpdateShift(orgIdRef, eventIdRef, sectionIdRef)
@@ -74,15 +74,25 @@ watch(
{ immediate: true },
)
function formatTimeSlotItem(ts: { id: string; name: string; date: string; start_time: string; end_time: string }) {
return {
title: `${ts.name}${ts.date} ${ts.start_time}${ts.end_time}`,
value: ts.id,
}
}
const timeSlotItems = computed(() => {
if (!timeSlots.value?.length) return []
// While loading, show the current shift's time slot so the dropdown doesn't flash a raw ULID
if (!timeSlots.value?.length) {
if (props.shift?.time_slot) {
return [formatTimeSlotItem(props.shift.time_slot)]
}
return []
}
const hasFestival = timeSlots.value.some(ts => ts.source === 'festival')
if (!hasFestival) {
return timeSlots.value.map(ts => ({
title: `${ts.name}${ts.date} ${ts.start_time}${ts.end_time}`,
value: ts.id,
}))
return timeSlots.value.map(formatTimeSlotItem)
}
const subEventSlots = timeSlots.value.filter(ts => ts.source !== 'festival')
@@ -91,18 +101,12 @@ const timeSlotItems = computed(() => {
if (subEventSlots.length) {
items.push({ title: subEventSlots[0]?.event_name ?? 'Programma', type: 'subheader' })
items.push(...subEventSlots.map(ts => ({
title: `${ts.name}${ts.date} ${ts.start_time}${ts.end_time}`,
value: ts.id,
})))
items.push(...subEventSlots.map(formatTimeSlotItem))
}
if (festivalSlots.length) {
items.push({ title: festivalSlots[0]?.event_name ?? 'Festival', type: 'subheader' })
items.push(...festivalSlots.map(ts => ({
title: `${ts.name}${ts.date} ${ts.start_time}${ts.end_time}`,
value: ts.id,
})))
items.push(...festivalSlots.map(formatTimeSlotItem))
}
return items
@@ -192,10 +196,11 @@ function onSubmit() {
<VRow>
<VCol cols="12">
<AppSelect
v-if="timeSlotItems.length"
v-if="timeSlotsLoading || timeSlotItems.length"
v-model="form.time_slot_id"
label="Time Slot"
:items="timeSlotItems"
:loading="timeSlotsLoading"
:rules="[requiredValidator]"
:error-messages="errors.time_slot_id"
/>