fix(admin): datetime popover clock, unsaved confirm, inferred list dates

BM UAT on v0.1.1 showed a date-only calendar for datetime fields, a stuck discard dialog, and raw ISO timestamps when columns.yaml omitted type. The picker now edits time in the popover, confirm sits above the calendar, and omitted time.Time / Date / TimeOfDay columns compile as datetime / date / time.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-03 05:58:24 +02:00
parent 8031fd1c16
commit 55314e41f8
14 changed files with 227 additions and 32 deletions

View File

@@ -21,7 +21,7 @@ import {
TimeFieldInput,
TimeFieldRoot,
} from 'reka-ui'
import type { DateValue, Time } from '@internationalized/date'
import { CalendarDateTime, Time, type DateValue } from '@internationalized/date'
import { Calendar, ChevronLeft, ChevronRight, Clock, X } from '@lucide/vue'
import { currentLocale, t } from '../../../app/i18n'
import {
@@ -127,11 +127,64 @@ function onUpdate(next: DateValue | Time | undefined | null): void {
emit('update:modelValue', text)
}
/** A calendar day click replaces the instant; keep the time already in the field. */
function onDatePickerUpdate(next: DateValue | undefined | null): void {
if (!next || mode.value !== 'datetime') {
onUpdate(next)
return
}
const previous = dateValue.value
const clock = popoverTime.value
const dateChanged =
!previous ||
previous.year !== next.year ||
previous.month !== next.month ||
previous.day !== next.day
const timeDiffers = !('hour' in next) || next.hour !== clock.hour || next.minute !== clock.minute
if (dateChanged && timeDiffers) {
next = withClock(next, clock)
}
pickedDate.value = next
onUpdate(next)
}
function clear(): void {
emit('update:modelValue', null)
scheduleRefresh()
}
/** The clock shown in the datetime popover, or midnight when the day is empty. */
const pickedDate = ref<DateValue | null>(null)
watch(dateValue, (value) => {
pickedDate.value = value
}, { immediate: true })
const popoverTime = computed((): Time => {
const current = pickedDate.value ?? dateValue.value
if (current && 'hour' in current) {
return new Time(current.hour, current.minute)
}
return new Time(0, 0)
})
function withClock(value: DateValue, clock: Time): DateValue {
if ('hour' in value) {
return (value as CalendarDateTime).set({ hour: clock.hour, minute: clock.minute, second: 0, millisecond: 0 })
}
return new CalendarDateTime(value.year, value.month, value.day, clock.hour, clock.minute)
}
function onPopoverTime(next: { hour: number; minute: number } | undefined | null): void {
if (!next) {
return
}
const clock = next instanceof Time ? next : new Time(next.hour, next.minute)
const current = (pickedDate.value ?? dateValue.value ?? placeholderValue) as DateValue
const updated = withClock(current, clock)
pickedDate.value = updated
onUpdate(updated)
}
function ucfirst(text: string): string {
return text.charAt(0).toLocaleUpperCase(locale.value) + text.slice(1)
}
@@ -148,7 +201,7 @@ function segmentClass(part: string): string {
return 'text-muted'
}
if (part === 'timeZoneName') {
return 'min-w-0 truncate text-[13px] text-muted'
return 'ml-2 min-w-0 truncate text-[13px] text-muted'
}
return 'rounded-[4px] px-0.5 tabular-nums outline-none focus:bg-sel focus:text-text focus-visible:outline-none data-[placeholder]:text-placeholder focus:data-[placeholder]:text-text'
}
@@ -226,9 +279,9 @@ const iconButton =
:week-starts-on="weekStartsOn"
:name="field.name"
:required="field.required || undefined"
close-on-select
:close-on-select="mode === 'date'"
prevent-deselect
@update:model-value="onUpdate"
@update:model-value="onDatePickerUpdate"
>
<div
ref="container"
@@ -312,6 +365,32 @@ const iconButton =
</DatePickerGridBody>
</DatePickerGrid>
</DatePickerCalendar>
<div
v-if="mode === 'datetime'"
data-datepicker-time
class="mt-3 flex items-center gap-2 border-t border-border pt-3"
>
<Clock :size="16" class="shrink-0 text-muted" aria-hidden="true" />
<TimeFieldRoot
v-slot="{ segments }"
:model-value="popoverTime"
:locale="locale"
:hour-cycle="hourCycle"
granularity="minute"
hide-time-zone
class="flex min-w-0 items-center"
@update:model-value="onPopoverTime"
>
<TimeFieldInput
v-for="item in segments"
:key="item.part"
:part="item.part"
:class="segmentClass(item.part)"
>
{{ item.value }}
</TimeFieldInput>
</TimeFieldRoot>
</div>
</DatePickerContent>
</DatePickerRoot>
</template>