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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user