diff --git a/apps/blocks/src/components/site/site-sidebar.tsx b/apps/blocks/src/components/site/site-sidebar.tsx index 36f77cb..1ef936b 100644 --- a/apps/blocks/src/components/site/site-sidebar.tsx +++ b/apps/blocks/src/components/site/site-sidebar.tsx @@ -247,7 +247,7 @@ export const SiteSidebar = forwardRef(function Si
  • diff --git a/packages/blocks-ui/src/__tests__/registry.test.tsx b/packages/blocks-ui/src/__tests__/registry.test.tsx index f6f9d20..93acf68 100644 --- a/packages/blocks-ui/src/__tests__/registry.test.tsx +++ b/packages/blocks-ui/src/__tests__/registry.test.tsx @@ -132,6 +132,44 @@ describe('defaultBlockRegistry', () => { expect(document.querySelector('[data-custom="yes"]')).not.toBeNull(); }); + it('keeps the stored zone when a minute-precision datetime edit is written back', () => { + const onChange = vi.fn(); + render( + , + ); + + const input = screen.getByLabelText(/Published at/) as HTMLInputElement; + expect(input.value).toBe('2026-08-22T10:30'); + + fireEvent.change(input, { target: { value: '2026-08-22T11:45' } }); + expect(onChange).toHaveBeenCalledWith({ published_at: '2026-08-22T11:45:00+02:00' }); + }); + + it('stores an absent number as null rather than NaN', () => { + const onChange = vi.fn(); + render( + , + ); + + const input = screen.getByLabelText(/Reading time/) as HTMLInputElement; + + fireEvent.change(input, { target: { value: '12' } }); + expect(onChange).toHaveBeenLastCalledWith({ reading_time: 12 }); + + // A browser reports an unparseable number as an empty value. + fireEvent.change(input, { target: { value: '1e' } }); + expect(onChange).toHaveBeenLastCalledWith({ reading_time: null }); + }); + it('leaves data blocks unregistered, so an unsatisfied node stays visible', () => { expect(widgetRegistry.DataTable).toBeUndefined(); expect(missingTypes(defaultBlockRegistry, ['DataTable'])).toEqual(['DataTable']); diff --git a/packages/blocks-ui/src/widgets.tsx b/packages/blocks-ui/src/widgets.tsx index 225c325..32bb14f 100644 --- a/packages/blocks-ui/src/widgets.tsx +++ b/packages/blocks-ui/src/widgets.tsx @@ -34,6 +34,23 @@ function numericConstraints(props: UINodeProps) { }; } +/** An empty number input is absent, and a half-typed one is not yet a number. */ +function numberValue(raw: string): number | string | null { + if (raw === '') return null; + const parsed = Number(raw); + return Number.isFinite(parsed) ? parsed : raw; +} + +const ZONE_SUFFIX = /(?:Z|[+-]\d{2}:?\d{2})$/; + +/** Re-attaches the zone the stored value carried to a minute-precision edit. */ +function zonedValue(local: string, zone: string): string | null { + if (local === '') return null; + if (!zone) return local; + const seconds = local.length > 16 ? '' : ':00'; + return `${local}${seconds}${zone}`; +} + /** A text-ish input; `inputType` carries the HTML type a format implies. */ function TextInput({ props, type }: { props: UINodeProps; type?: string }) { const field = useNodeField(props); @@ -114,8 +131,7 @@ export function NumberInputBlock({ props }: BlockProps) { name={field.name} type="number" value={textValue(field.value)} - // An empty number input is absent, not zero. - onChange={(event) => field.setValue(event.target.value === '' ? null : Number(event.target.value))} + onChange={(event) => field.setValue(numberValue(event.target.value))} disabled={field.disabled} required={field.required} {...(field.placeholder ? { placeholder: field.placeholder } : {})} @@ -219,12 +235,14 @@ export function DatePickerBlock({ props }: BlockProps) { /** * `datetime-local` needs `YYYY-MM-DDTHH:mm`, while a document (and Postgres) - * speaks ISO-8601 with a zone, so the value is trimmed for display only. + * speaks ISO-8601 with a zone, so the value is trimmed for display and the + * incoming zone is reapplied on write-back rather than dropped. */ export function DateTimePickerBlock({ props }: BlockProps) { const field = useNodeField(props); const raw = textValue(field.value); const local = raw.length > 16 ? raw.slice(0, 16) : raw; + const zone = ZONE_SUFFIX.exec(raw)?.[0] ?? ''; return ( @@ -233,7 +251,9 @@ export function DateTimePickerBlock({ props }: BlockProps) { name={field.name} type="datetime-local" value={local} - onChange={(event) => field.setValue(event.target.value)} + onChange={(event) => + field.setValue(zonedValue(event.target.value, zone)) + } disabled={field.disabled} required={field.required} />