From 07a59dc64366ecb534fab6bf5de7898e39c152c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:41:14 +0100 Subject: [PATCH 01/16] Implement format for fields of type date --- .../components/dataviews-filters/filter.tsx | 12 +++++- packages/dataviews/src/field-types/date.tsx | 7 +--- .../src/stories/field-types.story.tsx | 19 ++++++++- .../dataviews/src/test/normalize-fields.ts | 42 +++++++++++++++++++ packages/dataviews/src/types/field-api.ts | 7 ++++ .../dataviews/src/utils/normalize-fields.ts | 11 +++++ 6 files changed, 90 insertions(+), 8 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-filters/filter.tsx b/packages/dataviews/src/components/dataviews-filters/filter.tsx index c02f1ea9b7a062..d93f9fdc78fe0a 100644 --- a/packages/dataviews/src/components/dataviews-filters/filter.tsx +++ b/packages/dataviews/src/components/dataviews-filters/filter.tsx @@ -19,6 +19,7 @@ import { import { __, sprintf } from '@wordpress/i18n'; import { useRef, createInterpolateElement } from '@wordpress/element'; import { closeSmall } from '@wordpress/icons'; +import { dateI18n } from '@wordpress/date'; const ENTER = 'Enter'; const SPACE = ' '; @@ -498,7 +499,16 @@ export default function Filter( { const field = fields.find( ( f ) => f.id === filter.field ); let label = filterInView.value; - if ( field?.type === 'datetime' && typeof label === 'string' ) { + if ( field?.type === 'date' && typeof label === 'string' ) { + try { + const dateValue = parseDateTime( label ); + if ( dateValue !== null ) { + label = dateI18n( field.format, label ); + } + } catch ( e ) { + label = filterInView.value; + } + } else if ( field?.type === 'datetime' && typeof label === 'string' ) { try { const dateValue = parseDateTime( label ); if ( dateValue !== null ) { diff --git a/packages/dataviews/src/field-types/date.tsx b/packages/dataviews/src/field-types/date.tsx index b6a2ccae440a77..5e5ea9f4485422 100644 --- a/packages/dataviews/src/field-types/date.tsx +++ b/packages/dataviews/src/field-types/date.tsx @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { dateI18n, getDate, getSettings } from '@wordpress/date'; +import { dateI18n } from '@wordpress/date'; /** * Internal dependencies @@ -24,9 +24,6 @@ import { OPERATOR_BETWEEN, } from '../constants'; -const getFormattedDate = ( dateToDisplay: string | null ) => - dateI18n( getSettings().formats.date, getDate( dateToDisplay ) ); - function sort( a: any, b: any, direction: SortDirection ) { const timeA = new Date( a ).getTime(); const timeB = new Date( b ).getTime(); @@ -51,7 +48,7 @@ export default { return ''; } - return getFormattedDate( value ); + return dateI18n( field.format, value ); }, enableSorting: true, filterBy: { diff --git a/packages/dataviews/src/stories/field-types.story.tsx b/packages/dataviews/src/stories/field-types.story.tsx index c25aef6f14c7ef..66cca0368d822d 100644 --- a/packages/dataviews/src/stories/field-types.story.tsx +++ b/packages/dataviews/src/stories/field-types.story.tsx @@ -832,14 +832,19 @@ export const DateComponent = ( { type, Edit, asyncElements, + format, }: { type: PanelTypes; Edit: ControlTypes; asyncElements: boolean; + format?: string; } ) => { const dateFields = useMemo( - () => fields.filter( ( field ) => field.type === 'date' ), - [] + () => + fields + .filter( ( field ) => field.type === 'date' ) + .map( ( field ) => ( format ? { ...field, format } : field ) ), + [ format ] ); return ( @@ -852,6 +857,16 @@ export const DateComponent = ( { ); }; DateComponent.storyName = 'date'; +DateComponent.args = { + format: '', +}; +DateComponent.argTypes = { + format: { + control: 'text', + description: + 'Custom PHP date format string (e.g., "F j, Y" for "November 6, 2010"). Leave empty to use WordPress default.', + }, +}; export const EmailComponent = ( { type, diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts index 817d6a42a09444..1a07be263f2786 100644 --- a/packages/dataviews/src/test/normalize-fields.ts +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -333,4 +333,46 @@ describe( 'normalizeFields: default getValue', () => { } ); } ); } ); + + describe( 'format normalization', () => { + it( 'applies default format when not provided for date fields', () => { + const fields: Field< {} >[] = [ + { + id: 'publishDate', + type: 'date', + }, + ]; + const normalizedFields = normalizeFields( fields ); + expect( normalizedFields[ 0 ].format ).toBeDefined(); + expect( typeof normalizedFields[ 0 ].format ).toBe( 'string' ); + } ); + + it( 'preserves custom format when provided', () => { + const fields: Field< {} >[] = [ + { + id: 'publishDate', + type: 'date', + format: 'F j, Y', + }, + ]; + const normalizedFields = normalizeFields( fields ); + expect( normalizedFields[ 0 ].format ).toBe( 'F j, Y' ); + } ); + + it( 'does not add format for non-date field types', () => { + const fields: Field< {} >[] = [ + { + id: 'title', + type: 'text', + }, + { + id: 'count', + type: 'integer', + }, + ]; + const normalizedFields = normalizeFields( fields ); + expect( normalizedFields[ 0 ].format ).toBeUndefined(); + expect( normalizedFields[ 1 ].format ).toBeUndefined(); + } ); + } ); } ); diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index 17a6f48323449d..6d8f48c888999f 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -308,6 +308,12 @@ export type Field< Item > = { * Used for editing operations to update field values. */ setValue?: ( args: { item: Item; value: any } ) => DeepPartial< Item >; + + /** + * Format string for fields of type date. + * If not provided, defaults to WordPress date format settings. + */ + format?: string; }; export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { @@ -324,6 +330,7 @@ export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { enableSorting: boolean; filterBy: NormalizedFilterByConfig | false; readOnly: boolean; + format: string; }; /** diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index 961ee50a7898dd..b2b11da478bd6f 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -3,6 +3,11 @@ */ import type { FunctionComponent } from 'react'; +/** + * WordPress dependencies + */ +import { getSettings } from '@wordpress/date'; + /** * Internal dependencies */ @@ -200,6 +205,12 @@ export default function normalizeFields< Item >( true, filterBy, readOnly: field.readOnly ?? fieldTypeDefinition.readOnly ?? false, + format: + field.type === 'date' && + field.format !== undefined && + typeof field.format === 'string' + ? field.format + : getSettings().formats.date, }; } ); } From ecf849577922161704e0501fac2f98e34a690152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:58:40 +0100 Subject: [PATCH 02/16] Rename format to displayFormat and make it an object --- .../components/dataviews-filters/filter.tsx | 2 +- packages/dataviews/src/field-types/date.tsx | 2 +- .../src/stories/field-types.story.tsx | 19 +++++++++++------ .../dataviews/src/test/normalize-fields.ts | 21 ++++++++++++------- packages/dataviews/src/types/field-api.ts | 11 +++++++--- .../dataviews/src/utils/normalize-fields.ts | 14 +++++++------ 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-filters/filter.tsx b/packages/dataviews/src/components/dataviews-filters/filter.tsx index d93f9fdc78fe0a..63482089cfa7df 100644 --- a/packages/dataviews/src/components/dataviews-filters/filter.tsx +++ b/packages/dataviews/src/components/dataviews-filters/filter.tsx @@ -503,7 +503,7 @@ export default function Filter( { try { const dateValue = parseDateTime( label ); if ( dateValue !== null ) { - label = dateI18n( field.format, label ); + label = dateI18n( field.displayFormat.date, label ); } } catch ( e ) { label = filterInView.value; diff --git a/packages/dataviews/src/field-types/date.tsx b/packages/dataviews/src/field-types/date.tsx index 5e5ea9f4485422..ca429be7d00e22 100644 --- a/packages/dataviews/src/field-types/date.tsx +++ b/packages/dataviews/src/field-types/date.tsx @@ -48,7 +48,7 @@ export default { return ''; } - return dateI18n( field.format, value ); + return dateI18n( field.displayFormat.date, value ); }, enableSorting: true, filterBy: { diff --git a/packages/dataviews/src/stories/field-types.story.tsx b/packages/dataviews/src/stories/field-types.story.tsx index 66cca0368d822d..80af00d2563374 100644 --- a/packages/dataviews/src/stories/field-types.story.tsx +++ b/packages/dataviews/src/stories/field-types.story.tsx @@ -832,19 +832,26 @@ export const DateComponent = ( { type, Edit, asyncElements, - format, + displayFormatDate, }: { type: PanelTypes; Edit: ControlTypes; asyncElements: boolean; - format?: string; + displayFormatDate?: string; } ) => { const dateFields = useMemo( () => fields .filter( ( field ) => field.type === 'date' ) - .map( ( field ) => ( format ? { ...field, format } : field ) ), - [ format ] + .map( ( field ) => + displayFormatDate + ? { + ...field, + displayFormat: { date: displayFormatDate }, + } + : field + ), + [ displayFormatDate ] ); return ( @@ -858,10 +865,10 @@ export const DateComponent = ( { }; DateComponent.storyName = 'date'; DateComponent.args = { - format: '', + displayFormatDate: '', }; DateComponent.argTypes = { - format: { + displayFormatDate: { control: 'text', description: 'Custom PHP date format string (e.g., "F j, Y" for "November 6, 2010"). Leave empty to use WordPress default.', diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts index 1a07be263f2786..a05f1f977af080 100644 --- a/packages/dataviews/src/test/normalize-fields.ts +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -334,7 +334,7 @@ describe( 'normalizeFields: default getValue', () => { } ); } ); - describe( 'format normalization', () => { + describe( 'displayFormat normalization', () => { it( 'applies default format when not provided for date fields', () => { const fields: Field< {} >[] = [ { @@ -343,8 +343,11 @@ describe( 'normalizeFields: default getValue', () => { }, ]; const normalizedFields = normalizeFields( fields ); - expect( normalizedFields[ 0 ].format ).toBeDefined(); - expect( typeof normalizedFields[ 0 ].format ).toBe( 'string' ); + expect( normalizedFields[ 0 ].displayFormat ).toBeDefined(); + expect( normalizedFields[ 0 ].displayFormat.date ).toBeDefined(); + expect( typeof normalizedFields[ 0 ].displayFormat.date ).toBe( + 'string' + ); } ); it( 'preserves custom format when provided', () => { @@ -352,14 +355,16 @@ describe( 'normalizeFields: default getValue', () => { { id: 'publishDate', type: 'date', - format: 'F j, Y', + displayFormat: { + date: 'F j, Y', + }, }, ]; const normalizedFields = normalizeFields( fields ); - expect( normalizedFields[ 0 ].format ).toBe( 'F j, Y' ); + expect( normalizedFields[ 0 ].displayFormat.date ).toBe( 'F j, Y' ); } ); - it( 'does not add format for non-date field types', () => { + it( 'always adds displayFormat.date for all field types', () => { const fields: Field< {} >[] = [ { id: 'title', @@ -371,8 +376,8 @@ describe( 'normalizeFields: default getValue', () => { }, ]; const normalizedFields = normalizeFields( fields ); - expect( normalizedFields[ 0 ].format ).toBeUndefined(); - expect( normalizedFields[ 1 ].format ).toBeUndefined(); + expect( normalizedFields[ 0 ].displayFormat.date ).toBeDefined(); + expect( normalizedFields[ 1 ].displayFormat.date ).toBeDefined(); } ); } ); } ); diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index 6d8f48c888999f..ae25cc58bb9d9c 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -310,10 +310,13 @@ export type Field< Item > = { setValue?: ( args: { item: Item; value: any } ) => DeepPartial< Item >; /** - * Format string for fields of type date. + * Display format configuration for fields. + * For date fields, contains a date property with the format string. * If not provided, defaults to WordPress date format settings. */ - format?: string; + displayFormat?: { + date: string; + }; }; export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { @@ -330,7 +333,9 @@ export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { enableSorting: boolean; filterBy: NormalizedFilterByConfig | false; readOnly: boolean; - format: string; + displayFormat: { + date: string; + }; }; /** diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index b2b11da478bd6f..cede7d91f1933e 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -205,12 +205,14 @@ export default function normalizeFields< Item >( true, filterBy, readOnly: field.readOnly ?? fieldTypeDefinition.readOnly ?? false, - format: - field.type === 'date' && - field.format !== undefined && - typeof field.format === 'string' - ? field.format - : getSettings().formats.date, + displayFormat: { + date: + field.type === 'date' && + field.displayFormat?.date !== undefined && + typeof field.displayFormat.date === 'string' + ? field.displayFormat.date + : getSettings().formats.date, + }, }; } ); } From 035c9481494072eaeaa8a9e0fd76762874e14766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:21:55 +0100 Subject: [PATCH 03/16] Add weekStartsOn property --- .../dataviews/src/dataform-controls/date.tsx | 7 ++- .../src/stories/field-types.story.tsx | 51 +++++++++++++++---- .../dataviews/src/test/normalize-fields.ts | 18 ++++++- packages/dataviews/src/types/field-api.ts | 23 ++++++--- .../dataviews/src/utils/normalize-fields.ts | 5 ++ 5 files changed, 82 insertions(+), 22 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/date.tsx b/packages/dataviews/src/dataform-controls/date.tsx index 1b98c2f43b7e22..fbc79dcc58b43d 100644 --- a/packages/dataviews/src/dataform-controls/date.tsx +++ b/packages/dataviews/src/dataform-controls/date.tsx @@ -320,7 +320,6 @@ function CalendarDateControl< Item >( { const { timezone: { string: timezoneString }, - l10n: { startOfWeek }, } = getSettings(); const displayLabel = isValid?.required @@ -396,7 +395,7 @@ function CalendarDateControl< Item >( { month={ calendarMonth } onMonthChange={ setCalendarMonth } timeZone={ timezoneString || undefined } - weekStartsOn={ startOfWeek } + weekStartsOn={ field.displayFormat.weekStartsOn } /> @@ -521,7 +520,7 @@ function CalendarDateRangeControl< Item >( { [ value, updateDateRange ] ); - const { timezone, l10n } = getSettings(); + const { timezone } = getSettings(); const displayLabel = field.isValid?.required ? `${ label } (${ __( 'Required' ) })` @@ -609,7 +608,7 @@ function CalendarDateRangeControl< Item >( { month={ calendarMonth } onMonthChange={ setCalendarMonth } timeZone={ timezone.string || undefined } - weekStartsOn={ l10n.startOfWeek } + weekStartsOn={ field.displayFormat.weekStartsOn } /> diff --git a/packages/dataviews/src/stories/field-types.story.tsx b/packages/dataviews/src/stories/field-types.story.tsx index 80af00d2563374..74548f5fd571a2 100644 --- a/packages/dataviews/src/stories/field-types.story.tsx +++ b/packages/dataviews/src/stories/field-types.story.tsx @@ -833,25 +833,42 @@ export const DateComponent = ( { Edit, asyncElements, displayFormatDate, + displayFormatWeekStartsOn, }: { type: PanelTypes; Edit: ControlTypes; asyncElements: boolean; displayFormatDate?: string; + displayFormatWeekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; } ) => { const dateFields = useMemo( () => fields .filter( ( field ) => field.type === 'date' ) - .map( ( field ) => - displayFormatDate - ? { - ...field, - displayFormat: { date: displayFormatDate }, - } - : field - ), - [ displayFormatDate ] + .map( ( field ) => { + if ( + displayFormatDate || + displayFormatWeekStartsOn !== undefined + ) { + const displayFormat: { + date?: string; + weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; + } = {}; + if ( displayFormatDate ) { + displayFormat.date = displayFormatDate; + } + if ( displayFormatWeekStartsOn !== undefined ) { + displayFormat.weekStartsOn = + displayFormatWeekStartsOn; + } + return { + ...field, + displayFormat, + }; + } + return field; + } ), + [ displayFormatDate, displayFormatWeekStartsOn ] ); return ( @@ -866,6 +883,7 @@ export const DateComponent = ( { DateComponent.storyName = 'date'; DateComponent.args = { displayFormatDate: '', + displayFormatWeekStartsOn: undefined, }; DateComponent.argTypes = { displayFormatDate: { @@ -873,6 +891,21 @@ DateComponent.argTypes = { description: 'Custom PHP date format string (e.g., "F j, Y" for "November 6, 2010"). Leave empty to use WordPress default.', }, + displayFormatWeekStartsOn: { + control: 'select', + options: { + Default: undefined, + Sunday: 0, + Monday: 1, + Tuesday: 2, + Wednesday: 3, + Thursday: 4, + Friday: 5, + Saturday: 6, + }, + description: + 'Day that the week starts on. Leave as Default to use WordPress default.', + }, }; export const EmailComponent = ( { diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts index a05f1f977af080..d669354b4b824a 100644 --- a/packages/dataviews/src/test/normalize-fields.ts +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -348,6 +348,12 @@ describe( 'normalizeFields: default getValue', () => { expect( typeof normalizedFields[ 0 ].displayFormat.date ).toBe( 'string' ); + expect( + normalizedFields[ 0 ].displayFormat.weekStartsOn + ).toBeDefined(); + expect( + typeof normalizedFields[ 0 ].displayFormat.weekStartsOn + ).toBe( 'number' ); } ); it( 'preserves custom format when provided', () => { @@ -357,14 +363,18 @@ describe( 'normalizeFields: default getValue', () => { type: 'date', displayFormat: { date: 'F j, Y', + weekStartsOn: 1, }, }, ]; const normalizedFields = normalizeFields( fields ); expect( normalizedFields[ 0 ].displayFormat.date ).toBe( 'F j, Y' ); + expect( normalizedFields[ 0 ].displayFormat.weekStartsOn ).toBe( + 1 + ); } ); - it( 'always adds displayFormat.date for all field types', () => { + it( 'always adds displayFormat.date and displayFormat.weekStartsOn for all field types', () => { const fields: Field< {} >[] = [ { id: 'title', @@ -378,6 +388,12 @@ describe( 'normalizeFields: default getValue', () => { const normalizedFields = normalizeFields( fields ); expect( normalizedFields[ 0 ].displayFormat.date ).toBeDefined(); expect( normalizedFields[ 1 ].displayFormat.date ).toBeDefined(); + expect( + normalizedFields[ 0 ].displayFormat.weekStartsOn + ).toBeDefined(); + expect( + normalizedFields[ 1 ].displayFormat.weekStartsOn + ).toBeDefined(); } ); } ); } ); diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index ae25cc58bb9d9c..adb0859c6b8755 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -311,12 +311,21 @@ export type Field< Item > = { /** * Display format configuration for fields. - * For date fields, contains a date property with the format string. - * If not provided, defaults to WordPress date format settings. */ - displayFormat?: { - date: string; - }; + displayFormat?: DisplayFormatDate; +}; + +/** + * Format for date fields: + * + * - date: the format string (e.g., 'F j, Y' for WordPress default format like 'March 10, 2023') + * - weekStartsOn: to specify the first day of the week (0 is Sunday). + * + * If not provided, defaults to WordPress date format settings. + */ +type DisplayFormatDate = { + date?: string; + weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; }; export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { @@ -333,9 +342,7 @@ export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { enableSorting: boolean; filterBy: NormalizedFilterByConfig | false; readOnly: boolean; - displayFormat: { - date: string; - }; + displayFormat: Required< DisplayFormatDate >; }; /** diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index cede7d91f1933e..5cd941b9370c8a 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -212,6 +212,11 @@ export default function normalizeFields< Item >( typeof field.displayFormat.date === 'string' ? field.displayFormat.date : getSettings().formats.date, + weekStartsOn: + field.type === 'date' && + field.displayFormat?.weekStartsOn !== undefined + ? field.displayFormat.weekStartsOn + : getSettings().l10n.startOfWeek, }, }; } ); From 70f44f9de6b4b0bca308a65cb4e0c4b875148d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:36:07 +0100 Subject: [PATCH 04/16] Use day names instead of numbers for the API --- .../dataviews/src/dataform-controls/date.tsx | 9 ++- .../src/stories/field-types.story.tsx | 32 ++++++++--- .../dataviews/src/test/normalize-fields.ts | 6 +- packages/dataviews/src/types/field-api.ts | 11 +++- .../dataviews/src/utils/normalize-fields.ts | 5 +- .../dataviews/src/utils/week-starts-on.ts | 57 +++++++++++++++++++ 6 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 packages/dataviews/src/utils/week-starts-on.ts diff --git a/packages/dataviews/src/dataform-controls/date.tsx b/packages/dataviews/src/dataform-controls/date.tsx index fbc79dcc58b43d..17e1ed1a95b5e6 100644 --- a/packages/dataviews/src/dataform-controls/date.tsx +++ b/packages/dataviews/src/dataform-controls/date.tsx @@ -51,6 +51,7 @@ import type { NormalizedField, } from '../types'; import getCustomValidity from './utils/get-custom-validity'; +import { weekStartsOnToNumber } from '../utils/week-starts-on'; const { DateCalendar, DateRangeCalendar } = unlock( componentsPrivateApis ); @@ -395,7 +396,9 @@ function CalendarDateControl< Item >( { month={ calendarMonth } onMonthChange={ setCalendarMonth } timeZone={ timezoneString || undefined } - weekStartsOn={ field.displayFormat.weekStartsOn } + weekStartsOn={ weekStartsOnToNumber( + field.displayFormat.weekStartsOn + ) } /> @@ -608,7 +611,9 @@ function CalendarDateRangeControl< Item >( { month={ calendarMonth } onMonthChange={ setCalendarMonth } timeZone={ timezone.string || undefined } - weekStartsOn={ field.displayFormat.weekStartsOn } + weekStartsOn={ weekStartsOnToNumber( + field.displayFormat.weekStartsOn + ) } /> diff --git a/packages/dataviews/src/stories/field-types.story.tsx b/packages/dataviews/src/stories/field-types.story.tsx index 74548f5fd571a2..7295328905e13b 100644 --- a/packages/dataviews/src/stories/field-types.story.tsx +++ b/packages/dataviews/src/stories/field-types.story.tsx @@ -839,7 +839,14 @@ export const DateComponent = ( { Edit: ControlTypes; asyncElements: boolean; displayFormatDate?: string; - displayFormatWeekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; + displayFormatWeekStartsOn?: + | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday'; } ) => { const dateFields = useMemo( () => @@ -852,7 +859,14 @@ export const DateComponent = ( { ) { const displayFormat: { date?: string; - weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; + weekStartsOn?: + | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday'; } = {}; if ( displayFormatDate ) { displayFormat.date = displayFormatDate; @@ -895,13 +909,13 @@ DateComponent.argTypes = { control: 'select', options: { Default: undefined, - Sunday: 0, - Monday: 1, - Tuesday: 2, - Wednesday: 3, - Thursday: 4, - Friday: 5, - Saturday: 6, + Sunday: 'sunday', + Monday: 'monday', + Tuesday: 'tuesday', + Wednesday: 'wednesday', + Thursday: 'thursday', + Friday: 'friday', + Saturday: 'saturday', }, description: 'Day that the week starts on. Leave as Default to use WordPress default.', diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts index d669354b4b824a..0c4843b4855a5f 100644 --- a/packages/dataviews/src/test/normalize-fields.ts +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -353,7 +353,7 @@ describe( 'normalizeFields: default getValue', () => { ).toBeDefined(); expect( typeof normalizedFields[ 0 ].displayFormat.weekStartsOn - ).toBe( 'number' ); + ).toBe( 'string' ); } ); it( 'preserves custom format when provided', () => { @@ -363,14 +363,14 @@ describe( 'normalizeFields: default getValue', () => { type: 'date', displayFormat: { date: 'F j, Y', - weekStartsOn: 1, + weekStartsOn: 'monday', }, }, ]; const normalizedFields = normalizeFields( fields ); expect( normalizedFields[ 0 ].displayFormat.date ).toBe( 'F j, Y' ); expect( normalizedFields[ 0 ].displayFormat.weekStartsOn ).toBe( - 1 + 'monday' ); } ); diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index adb0859c6b8755..daa20d7f3deb48 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -319,13 +319,20 @@ export type Field< Item > = { * Format for date fields: * * - date: the format string (e.g., 'F j, Y' for WordPress default format like 'March 10, 2023') - * - weekStartsOn: to specify the first day of the week (0 is Sunday). + * - weekStartsOn: to specify the first day of the week ('sunday', 'monday', etc.). * * If not provided, defaults to WordPress date format settings. */ type DisplayFormatDate = { date?: string; - weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; + weekStartsOn?: + | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday'; }; export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index 5cd941b9370c8a..4a8f08011287e5 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -26,6 +26,7 @@ import { SINGLE_SELECTION_OPERATORS, } from '../constants'; import hasElements from './has-elements'; +import { numberToWeekStartsOn } from './week-starts-on'; const getValueFromId = ( id: string ) => @@ -216,7 +217,9 @@ export default function normalizeFields< Item >( field.type === 'date' && field.displayFormat?.weekStartsOn !== undefined ? field.displayFormat.weekStartsOn - : getSettings().l10n.startOfWeek, + : numberToWeekStartsOn( + getSettings().l10n.startOfWeek + ), }, }; } ); diff --git a/packages/dataviews/src/utils/week-starts-on.ts b/packages/dataviews/src/utils/week-starts-on.ts new file mode 100644 index 00000000000000..9b833dbd8a5494 --- /dev/null +++ b/packages/dataviews/src/utils/week-starts-on.ts @@ -0,0 +1,57 @@ +/** + * Converts a weekStartsOn string to a number (0-6). + * + * @param day - The day name ('sunday', 'monday', etc.) + * @return The corresponding number (0 for Sunday, 1 for Monday, etc.) + */ +export function weekStartsOnToNumber( + day: + | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday' +): 0 | 1 | 2 | 3 | 4 | 5 | 6 { + const mapping = { + sunday: 0, + monday: 1, + tuesday: 2, + wednesday: 3, + thursday: 4, + friday: 5, + saturday: 6, + } as const; + + return mapping[ day ]; +} + +/** + * Converts a weekStartsOn number (0-6) to a string. + * + * @param day - The day number (0 for Sunday, 1 for Monday, etc.) + * @return The corresponding day name ('sunday', 'monday', etc.) + */ +export function numberToWeekStartsOn( + day: 0 | 1 | 2 | 3 | 4 | 5 | 6 +): + | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday' { + const mapping = { + 0: 'sunday', + 1: 'monday', + 2: 'tuesday', + 3: 'wednesday', + 4: 'thursday', + 5: 'friday', + 6: 'saturday', + } as const; + + return mapping[ day ]; +} From 21df561e3386d10fefb48df20f187292ff789d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:38:11 +0100 Subject: [PATCH 05/16] Add changelog entry --- packages/dataviews/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index dfd29b578c4b31..f949cb9c75b1ae 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,11 +2,15 @@ ## Unreleased +### Enhancements + +- Improve docs for Edit component. [#73202](https://github.com/WordPress/gutenberg/pull/73202) +- Field API: introduce the `displayFormat` prop to format the `date` field type. [#72999](https://github.com/WordPress/gutenberg/pull/72999) + ## 10.3.0 (2025-11-12) ### Enhancements -- Improve docs for Edit component. [#73202](https://github.com/WordPress/gutenberg/pull/73202) - DataForm: add new details layout. [#72355](https://github.com/WordPress/gutenberg/pull/72355) - DatViews list layout: remove link variant from primary actions's button. [#72920](https://github.com/WordPress/gutenberg/pull/72920) - DataForm: simplify form normalization. [#72848](https://github.com/WordPress/gutenberg/pull/72848) From 19a0decb1c86d0201b6fe7fc7b6a4f520644863b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:40:58 +0100 Subject: [PATCH 06/16] Update README --- packages/dataviews/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 5cbcd61ed35c7c..a6b2fab5a36b01 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1675,6 +1675,30 @@ Example: } ``` +### `displayFormat` + +Display format configuration for fields. Currently supported for date fields. This configuration affects how the field is displayed in the `render` method, the `Edit` control, and filter controls. + +- Type: `object`. +- Optional. +- Properties: + - `date`: The format string using PHP date format (e.g., 'F j, Y' for 'March 10, 2023'). Optional, defaults to WordPress date format settings. + - `weekStartsOn`: Specifies the first day of the week for calendar controls. One of `'sunday'`, `'monday'`, `'tuesday'`, `'wednesday'`, `'thursday'`, `'friday'`, `'saturday'`. Optional, defaults to WordPress date format settings. + +Example: + +```js +{ + id: 'publishDate', + type: 'date', + label: 'Publish Date', + displayFormat: { + date: 'F j, Y', + weekStartsOn: 'monday', + }, +} +``` + ## Form Field API ### `id` From 2845809034fb9f09e0a2cd7a3eba241bac060d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:16:39 +0100 Subject: [PATCH 07/16] displayFormat is an empty object for non-date field types --- .../dataviews/src/dataform-controls/date.tsx | 23 +++++--- packages/dataviews/src/field-types/date.tsx | 9 ++++ .../dataviews/src/test/normalize-fields.ts | 12 ++--- packages/dataviews/src/types/field-api.ts | 15 +++++- .../dataviews/src/utils/normalize-fields.ts | 52 +++++++++++++------ 5 files changed, 76 insertions(+), 35 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/date.tsx b/packages/dataviews/src/dataform-controls/date.tsx index 17e1ed1a95b5e6..2a8f0bd46feb49 100644 --- a/packages/dataviews/src/dataform-controls/date.tsx +++ b/packages/dataviews/src/dataform-controls/date.tsx @@ -258,11 +258,17 @@ function CalendarDateControl< Item >( { hideLabelFromVision, validity, }: DataFormControlProps< Item > ) { - const { id, label, setValue, getValue, isValid } = field; + const { id, type, label, setValue, getValue, isValid, displayFormat } = + field; const [ selectedPresetId, setSelectedPresetId ] = useState< string | null >( null ); + let weekStartsOn; + if ( type === 'date' ) { + weekStartsOn = weekStartsOnToNumber( displayFormat.weekStartsOn ); + } + const fieldValue = getValue( { item: data } ); const value = typeof fieldValue === 'string' ? fieldValue : undefined; const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => { @@ -396,9 +402,7 @@ function CalendarDateControl< Item >( { month={ calendarMonth } onMonthChange={ setCalendarMonth } timeZone={ timezoneString || undefined } - weekStartsOn={ weekStartsOnToNumber( - field.displayFormat.weekStartsOn - ) } + weekStartsOn={ weekStartsOn } /> @@ -413,7 +417,7 @@ function CalendarDateRangeControl< Item >( { hideLabelFromVision, validity, }: DataFormControlProps< Item > ) { - const { id, label, getValue, setValue } = field; + const { id, type, label, getValue, setValue, displayFormat } = field; let value: DateRange; const fieldValue = getValue( { item: data } ); if ( @@ -424,6 +428,11 @@ function CalendarDateRangeControl< Item >( { value = fieldValue as DateRange; } + let weekStartsOn; + if ( type === 'date' ) { + weekStartsOn = weekStartsOnToNumber( displayFormat.weekStartsOn ); + } + const onChangeCallback = useCallback( ( newValue: DateRange ) => { onChange( @@ -611,9 +620,7 @@ function CalendarDateRangeControl< Item >( { month={ calendarMonth } onMonthChange={ setCalendarMonth } timeZone={ timezone.string || undefined } - weekStartsOn={ weekStartsOnToNumber( - field.displayFormat.weekStartsOn - ) } + weekStartsOn={ weekStartsOn } /> diff --git a/packages/dataviews/src/field-types/date.tsx b/packages/dataviews/src/field-types/date.tsx index ca429be7d00e22..9cb69d40a06754 100644 --- a/packages/dataviews/src/field-types/date.tsx +++ b/packages/dataviews/src/field-types/date.tsx @@ -48,6 +48,15 @@ export default { return ''; } + // Not all fields have displayFormat, but date fields do. + // + // At runtime, this method will never be called for non-date fields. + // However, the type system does not know this, so we need to check it. + // There's an opportunity here to improve the type system. + if ( field.type !== 'date' ) { + return ''; + } + return dateI18n( field.displayFormat.date, value ); }, enableSorting: true, diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts index 0c4843b4855a5f..6a056facb2a1de 100644 --- a/packages/dataviews/src/test/normalize-fields.ts +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -374,7 +374,7 @@ describe( 'normalizeFields: default getValue', () => { ); } ); - it( 'always adds displayFormat.date and displayFormat.weekStartsOn for all field types', () => { + it( 'adds empty displayFormat for non-date field types', () => { const fields: Field< {} >[] = [ { id: 'title', @@ -386,14 +386,8 @@ describe( 'normalizeFields: default getValue', () => { }, ]; const normalizedFields = normalizeFields( fields ); - expect( normalizedFields[ 0 ].displayFormat.date ).toBeDefined(); - expect( normalizedFields[ 1 ].displayFormat.date ).toBeDefined(); - expect( - normalizedFields[ 0 ].displayFormat.weekStartsOn - ).toBeDefined(); - expect( - normalizedFields[ 1 ].displayFormat.weekStartsOn - ).toBeDefined(); + expect( normalizedFields[ 0 ].displayFormat ).toEqual( {} ); + expect( normalizedFields[ 1 ].displayFormat ).toEqual( {} ); } ); } ); } ); diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index daa20d7f3deb48..241cf864d1002a 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -335,7 +335,7 @@ type DisplayFormatDate = { | 'saturday'; }; -export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { +type NormalizedFieldBase< Item > = Omit< Field< Item >, 'Edit' > & { label: string; header: string | ReactElement; getValue: ( args: { item: Item } ) => any; @@ -349,9 +349,22 @@ export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { enableSorting: boolean; filterBy: NormalizedFilterByConfig | false; readOnly: boolean; +}; + +export type NormalizedFieldDate< Item > = NormalizedFieldBase< Item > & { + type: 'date'; displayFormat: Required< DisplayFormatDate >; }; +export type NormalizedFieldGeneric< Item > = NormalizedFieldBase< Item > & { + type?: Exclude< FieldType, 'date' >; + displayFormat: {}; +}; + +export type NormalizedField< Item > = + | NormalizedFieldDate< Item > + | NormalizedFieldGeneric< Item >; + /** * A collection of dataview fields for a data type. */ diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index 4a8f08011287e5..aab6a8221b3510 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -188,8 +188,15 @@ export default function normalizeFields< Item >( const filterBy = getFilterBy( field, fieldTypeDefinition ); - return { - ...field, + // TypeScript is unable to figure out that we're returning the proper types + // (either NormalizedFieldDate or NormalizedFieldGeneric) + // when the type is part of the object spread below. + // + // Hence, why we remove it and add back it later. + const { type, ...fieldWithoutType } = field; + + const baseField = { + ...fieldWithoutType, label: field.label || field.id, header: field.header || field.label || field.id, getValue, @@ -206,21 +213,32 @@ export default function normalizeFields< Item >( true, filterBy, readOnly: field.readOnly ?? fieldTypeDefinition.readOnly ?? false, - displayFormat: { - date: - field.type === 'date' && - field.displayFormat?.date !== undefined && - typeof field.displayFormat.date === 'string' - ? field.displayFormat.date - : getSettings().formats.date, - weekStartsOn: - field.type === 'date' && - field.displayFormat?.weekStartsOn !== undefined - ? field.displayFormat.weekStartsOn - : numberToWeekStartsOn( - getSettings().l10n.startOfWeek - ), - }, + }; + + if ( field.type === 'date' ) { + return { + ...baseField, + type: 'date' as const, + displayFormat: { + date: + field.displayFormat?.date !== undefined && + typeof field.displayFormat.date === 'string' + ? field.displayFormat.date + : getSettings().formats.date, + weekStartsOn: + field.displayFormat?.weekStartsOn !== undefined + ? field.displayFormat.weekStartsOn + : numberToWeekStartsOn( + getSettings().l10n.startOfWeek + ), + }, + }; + } + + return { + ...baseField, + type: field.type, + displayFormat: {}, }; } ); } From 13e5f23e882c7cf7a5ae9f63e6b3258683d02ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:23:58 +0100 Subject: [PATCH 08/16] Rename displayFormat to format --- packages/dataviews/CHANGELOG.md | 2 +- packages/dataviews/README.md | 4 +-- .../components/dataviews-filters/filter.tsx | 2 +- .../dataviews/src/dataform-controls/date.tsx | 17 ++++----- packages/dataviews/src/field-types/date.tsx | 4 +-- .../src/stories/field-types.story.tsx | 36 +++++++++---------- .../dataviews/src/test/normalize-fields.ts | 28 +++++++-------- packages/dataviews/src/types/field-api.ts | 8 ++--- .../dataviews/src/utils/normalize-fields.ts | 14 ++++---- 9 files changed, 54 insertions(+), 61 deletions(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index f949cb9c75b1ae..737209f256955d 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -5,7 +5,7 @@ ### Enhancements - Improve docs for Edit component. [#73202](https://github.com/WordPress/gutenberg/pull/73202) -- Field API: introduce the `displayFormat` prop to format the `date` field type. [#72999](https://github.com/WordPress/gutenberg/pull/72999) +- Field API: introduce the `format` prop to format the `date` field type. [#72999](https://github.com/WordPress/gutenberg/pull/72999) ## 10.3.0 (2025-11-12) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index a6b2fab5a36b01..fd6045cb410dee 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1675,7 +1675,7 @@ Example: } ``` -### `displayFormat` +### `format` Display format configuration for fields. Currently supported for date fields. This configuration affects how the field is displayed in the `render` method, the `Edit` control, and filter controls. @@ -1692,7 +1692,7 @@ Example: id: 'publishDate', type: 'date', label: 'Publish Date', - displayFormat: { + format: { date: 'F j, Y', weekStartsOn: 'monday', }, diff --git a/packages/dataviews/src/components/dataviews-filters/filter.tsx b/packages/dataviews/src/components/dataviews-filters/filter.tsx index 63482089cfa7df..6ac02cf44aa74e 100644 --- a/packages/dataviews/src/components/dataviews-filters/filter.tsx +++ b/packages/dataviews/src/components/dataviews-filters/filter.tsx @@ -503,7 +503,7 @@ export default function Filter( { try { const dateValue = parseDateTime( label ); if ( dateValue !== null ) { - label = dateI18n( field.displayFormat.date, label ); + label = dateI18n( field.format.date, label ); } } catch ( e ) { label = filterInView.value; diff --git a/packages/dataviews/src/dataform-controls/date.tsx b/packages/dataviews/src/dataform-controls/date.tsx index 2a8f0bd46feb49..4a1e84f2a278de 100644 --- a/packages/dataviews/src/dataform-controls/date.tsx +++ b/packages/dataviews/src/dataform-controls/date.tsx @@ -3,7 +3,7 @@ */ import clsx from 'clsx'; import { - format, + format as formatDateFns, isValid as isValidDate, subMonths, subDays, @@ -148,7 +148,9 @@ const formatDate = ( date?: Date | string ): string => { if ( ! date ) { return ''; } - return typeof date === 'string' ? date : format( date, 'yyyy-MM-dd' ); + return typeof date === 'string' + ? date + : formatDateFns( date, 'yyyy-MM-dd' ); }; function ValidatedDateControl< Item >( { @@ -258,15 +260,14 @@ function CalendarDateControl< Item >( { hideLabelFromVision, validity, }: DataFormControlProps< Item > ) { - const { id, type, label, setValue, getValue, isValid, displayFormat } = - field; + const { id, type, label, setValue, getValue, isValid, format } = field; const [ selectedPresetId, setSelectedPresetId ] = useState< string | null >( null ); let weekStartsOn; if ( type === 'date' ) { - weekStartsOn = weekStartsOnToNumber( displayFormat.weekStartsOn ); + weekStartsOn = weekStartsOnToNumber( format.weekStartsOn ); } const fieldValue = getValue( { item: data } ); @@ -288,7 +289,7 @@ function CalendarDateControl< Item >( { const onSelectDate = useCallback( ( newDate: Date | undefined | null ) => { const dateValue = newDate - ? format( newDate, 'yyyy-MM-dd' ) + ? formatDateFns( newDate, 'yyyy-MM-dd' ) : undefined; onChangeCallback( dateValue ); setSelectedPresetId( null ); @@ -417,7 +418,7 @@ function CalendarDateRangeControl< Item >( { hideLabelFromVision, validity, }: DataFormControlProps< Item > ) { - const { id, type, label, getValue, setValue, displayFormat } = field; + const { id, type, label, getValue, setValue, format } = field; let value: DateRange; const fieldValue = getValue( { item: data } ); if ( @@ -430,7 +431,7 @@ function CalendarDateRangeControl< Item >( { let weekStartsOn; if ( type === 'date' ) { - weekStartsOn = weekStartsOnToNumber( displayFormat.weekStartsOn ); + weekStartsOn = weekStartsOnToNumber( format.weekStartsOn ); } const onChangeCallback = useCallback( diff --git a/packages/dataviews/src/field-types/date.tsx b/packages/dataviews/src/field-types/date.tsx index 9cb69d40a06754..afaef22b276ebf 100644 --- a/packages/dataviews/src/field-types/date.tsx +++ b/packages/dataviews/src/field-types/date.tsx @@ -48,7 +48,7 @@ export default { return ''; } - // Not all fields have displayFormat, but date fields do. + // Not all fields have format, but date fields do. // // At runtime, this method will never be called for non-date fields. // However, the type system does not know this, so we need to check it. @@ -57,7 +57,7 @@ export default { return ''; } - return dateI18n( field.displayFormat.date, value ); + return dateI18n( field.format.date, value ); }, enableSorting: true, filterBy: { diff --git a/packages/dataviews/src/stories/field-types.story.tsx b/packages/dataviews/src/stories/field-types.story.tsx index 7295328905e13b..4ab180067df057 100644 --- a/packages/dataviews/src/stories/field-types.story.tsx +++ b/packages/dataviews/src/stories/field-types.story.tsx @@ -832,14 +832,14 @@ export const DateComponent = ( { type, Edit, asyncElements, - displayFormatDate, - displayFormatWeekStartsOn, + formatDate, + formatWeekStartsOn, }: { type: PanelTypes; Edit: ControlTypes; asyncElements: boolean; - displayFormatDate?: string; - displayFormatWeekStartsOn?: + formatDate?: string; + formatWeekStartsOn?: | 'sunday' | 'monday' | 'tuesday' @@ -853,11 +853,8 @@ export const DateComponent = ( { fields .filter( ( field ) => field.type === 'date' ) .map( ( field ) => { - if ( - displayFormatDate || - displayFormatWeekStartsOn !== undefined - ) { - const displayFormat: { + if ( formatDate || formatWeekStartsOn !== undefined ) { + const format: { date?: string; weekStartsOn?: | 'sunday' @@ -868,21 +865,20 @@ export const DateComponent = ( { | 'friday' | 'saturday'; } = {}; - if ( displayFormatDate ) { - displayFormat.date = displayFormatDate; + if ( formatDate ) { + format.date = formatDate; } - if ( displayFormatWeekStartsOn !== undefined ) { - displayFormat.weekStartsOn = - displayFormatWeekStartsOn; + if ( formatWeekStartsOn !== undefined ) { + format.weekStartsOn = formatWeekStartsOn; } return { ...field, - displayFormat, + format, }; } return field; } ), - [ displayFormatDate, displayFormatWeekStartsOn ] + [ formatDate, formatWeekStartsOn ] ); return ( @@ -896,16 +892,16 @@ export const DateComponent = ( { }; DateComponent.storyName = 'date'; DateComponent.args = { - displayFormatDate: '', - displayFormatWeekStartsOn: undefined, + formatDate: '', + formatWeekStartsOn: undefined, }; DateComponent.argTypes = { - displayFormatDate: { + formatDate: { control: 'text', description: 'Custom PHP date format string (e.g., "F j, Y" for "November 6, 2010"). Leave empty to use WordPress default.', }, - displayFormatWeekStartsOn: { + formatWeekStartsOn: { control: 'select', options: { Default: undefined, diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts index 6a056facb2a1de..946abeff64cf82 100644 --- a/packages/dataviews/src/test/normalize-fields.ts +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -334,7 +334,7 @@ describe( 'normalizeFields: default getValue', () => { } ); } ); - describe( 'displayFormat normalization', () => { + describe( 'format normalization', () => { it( 'applies default format when not provided for date fields', () => { const fields: Field< {} >[] = [ { @@ -343,17 +343,13 @@ describe( 'normalizeFields: default getValue', () => { }, ]; const normalizedFields = normalizeFields( fields ); - expect( normalizedFields[ 0 ].displayFormat ).toBeDefined(); - expect( normalizedFields[ 0 ].displayFormat.date ).toBeDefined(); - expect( typeof normalizedFields[ 0 ].displayFormat.date ).toBe( + expect( normalizedFields[ 0 ].format ).toBeDefined(); + expect( normalizedFields[ 0 ].format.date ).toBeDefined(); + expect( typeof normalizedFields[ 0 ].format.date ).toBe( 'string' ); + expect( normalizedFields[ 0 ].format.weekStartsOn ).toBeDefined(); + expect( typeof normalizedFields[ 0 ].format.weekStartsOn ).toBe( 'string' ); - expect( - normalizedFields[ 0 ].displayFormat.weekStartsOn - ).toBeDefined(); - expect( - typeof normalizedFields[ 0 ].displayFormat.weekStartsOn - ).toBe( 'string' ); } ); it( 'preserves custom format when provided', () => { @@ -361,20 +357,20 @@ describe( 'normalizeFields: default getValue', () => { { id: 'publishDate', type: 'date', - displayFormat: { + format: { date: 'F j, Y', weekStartsOn: 'monday', }, }, ]; const normalizedFields = normalizeFields( fields ); - expect( normalizedFields[ 0 ].displayFormat.date ).toBe( 'F j, Y' ); - expect( normalizedFields[ 0 ].displayFormat.weekStartsOn ).toBe( + expect( normalizedFields[ 0 ].format.date ).toBe( 'F j, Y' ); + expect( normalizedFields[ 0 ].format.weekStartsOn ).toBe( 'monday' ); } ); - it( 'adds empty displayFormat for non-date field types', () => { + it( 'adds empty format for non-date field types', () => { const fields: Field< {} >[] = [ { id: 'title', @@ -386,8 +382,8 @@ describe( 'normalizeFields: default getValue', () => { }, ]; const normalizedFields = normalizeFields( fields ); - expect( normalizedFields[ 0 ].displayFormat ).toEqual( {} ); - expect( normalizedFields[ 1 ].displayFormat ).toEqual( {} ); + expect( normalizedFields[ 0 ].format ).toEqual( {} ); + expect( normalizedFields[ 1 ].format ).toEqual( {} ); } ); } ); } ); diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index 241cf864d1002a..e30cf9cb07039a 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -312,7 +312,7 @@ export type Field< Item > = { /** * Display format configuration for fields. */ - displayFormat?: DisplayFormatDate; + format?: FormatDate; }; /** @@ -323,7 +323,7 @@ export type Field< Item > = { * * If not provided, defaults to WordPress date format settings. */ -type DisplayFormatDate = { +type FormatDate = { date?: string; weekStartsOn?: | 'sunday' @@ -353,12 +353,12 @@ type NormalizedFieldBase< Item > = Omit< Field< Item >, 'Edit' > & { export type NormalizedFieldDate< Item > = NormalizedFieldBase< Item > & { type: 'date'; - displayFormat: Required< DisplayFormatDate >; + format: Required< FormatDate >; }; export type NormalizedFieldGeneric< Item > = NormalizedFieldBase< Item > & { type?: Exclude< FieldType, 'date' >; - displayFormat: {}; + format: {}; }; export type NormalizedField< Item > = diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index aab6a8221b3510..95d9c8e9a61305 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -219,15 +219,15 @@ export default function normalizeFields< Item >( return { ...baseField, type: 'date' as const, - displayFormat: { + format: { date: - field.displayFormat?.date !== undefined && - typeof field.displayFormat.date === 'string' - ? field.displayFormat.date + field.format?.date !== undefined && + typeof field.format.date === 'string' + ? field.format.date : getSettings().formats.date, weekStartsOn: - field.displayFormat?.weekStartsOn !== undefined - ? field.displayFormat.weekStartsOn + field.format?.weekStartsOn !== undefined + ? field.format.weekStartsOn : numberToWeekStartsOn( getSettings().l10n.startOfWeek ), @@ -238,7 +238,7 @@ export default function normalizeFields< Item >( return { ...baseField, type: field.type, - displayFormat: {}, + format: {}, }; } ); } From 9ffb93aa15f17fb549986979731b130243e70764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:30:31 +0100 Subject: [PATCH 09/16] Fix: pass a date object, not a string, to prevent momentlib from messing up timezone --- .../dataviews/src/components/dataviews-filters/filter.tsx | 4 ++-- packages/dataviews/src/field-types/date.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-filters/filter.tsx b/packages/dataviews/src/components/dataviews-filters/filter.tsx index 6ac02cf44aa74e..bb21598b43b225 100644 --- a/packages/dataviews/src/components/dataviews-filters/filter.tsx +++ b/packages/dataviews/src/components/dataviews-filters/filter.tsx @@ -19,7 +19,7 @@ import { import { __, sprintf } from '@wordpress/i18n'; import { useRef, createInterpolateElement } from '@wordpress/element'; import { closeSmall } from '@wordpress/icons'; -import { dateI18n } from '@wordpress/date'; +import { dateI18n, getDate } from '@wordpress/date'; const ENTER = 'Enter'; const SPACE = ' '; @@ -503,7 +503,7 @@ export default function Filter( { try { const dateValue = parseDateTime( label ); if ( dateValue !== null ) { - label = dateI18n( field.format.date, label ); + label = dateI18n( field.format.date, getDate( label ) ); } } catch ( e ) { label = filterInView.value; diff --git a/packages/dataviews/src/field-types/date.tsx b/packages/dataviews/src/field-types/date.tsx index afaef22b276ebf..fa23f660c4b464 100644 --- a/packages/dataviews/src/field-types/date.tsx +++ b/packages/dataviews/src/field-types/date.tsx @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { dateI18n } from '@wordpress/date'; +import { dateI18n, getDate } from '@wordpress/date'; /** * Internal dependencies @@ -57,7 +57,7 @@ export default { return ''; } - return dateI18n( field.format.date, value ); + return dateI18n( field.format.date, getDate( value ) ); }, enableSorting: true, filterBy: { From 1145dd09a420f413808b2e99370874469e0c3ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 10:37:20 +0100 Subject: [PATCH 10/16] Rename for clarity --- .../dataviews/src/dataform-controls/date.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/date.tsx b/packages/dataviews/src/dataform-controls/date.tsx index 4a1e84f2a278de..ae17fd3bd65869 100644 --- a/packages/dataviews/src/dataform-controls/date.tsx +++ b/packages/dataviews/src/dataform-controls/date.tsx @@ -3,7 +3,7 @@ */ import clsx from 'clsx'; import { - format as formatDateFns, + format, isValid as isValidDate, subMonths, subDays, @@ -148,9 +148,7 @@ const formatDate = ( date?: Date | string ): string => { if ( ! date ) { return ''; } - return typeof date === 'string' - ? date - : formatDateFns( date, 'yyyy-MM-dd' ); + return typeof date === 'string' ? date : format( date, 'yyyy-MM-dd' ); }; function ValidatedDateControl< Item >( { @@ -260,14 +258,22 @@ function CalendarDateControl< Item >( { hideLabelFromVision, validity, }: DataFormControlProps< Item > ) { - const { id, type, label, setValue, getValue, isValid, format } = field; + const { + id, + type, + label, + setValue, + getValue, + isValid, + format: fieldFormat, + } = field; const [ selectedPresetId, setSelectedPresetId ] = useState< string | null >( null ); let weekStartsOn; if ( type === 'date' ) { - weekStartsOn = weekStartsOnToNumber( format.weekStartsOn ); + weekStartsOn = weekStartsOnToNumber( fieldFormat.weekStartsOn ); } const fieldValue = getValue( { item: data } ); @@ -289,7 +295,7 @@ function CalendarDateControl< Item >( { const onSelectDate = useCallback( ( newDate: Date | undefined | null ) => { const dateValue = newDate - ? formatDateFns( newDate, 'yyyy-MM-dd' ) + ? format( newDate, 'yyyy-MM-dd' ) : undefined; onChangeCallback( dateValue ); setSelectedPresetId( null ); @@ -418,7 +424,7 @@ function CalendarDateRangeControl< Item >( { hideLabelFromVision, validity, }: DataFormControlProps< Item > ) { - const { id, type, label, getValue, setValue, format } = field; + const { id, type, label, getValue, setValue, format: fieldFormat } = field; let value: DateRange; const fieldValue = getValue( { item: data } ); if ( @@ -431,7 +437,7 @@ function CalendarDateRangeControl< Item >( { let weekStartsOn; if ( type === 'date' ) { - weekStartsOn = weekStartsOnToNumber( format.weekStartsOn ); + weekStartsOn = weekStartsOnToNumber( fieldFormat.weekStartsOn ); } const onChangeCallback = useCallback( From b191428854e10a164cb1df8d87421c355245c169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:12:25 +0100 Subject: [PATCH 11/16] weekStartsOn: address feedback --- .../dataviews/src/utils/week-starts-on.ts | 73 +++++++++---------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/packages/dataviews/src/utils/week-starts-on.ts b/packages/dataviews/src/utils/week-starts-on.ts index 9b833dbd8a5494..f31c4569072e06 100644 --- a/packages/dataviews/src/utils/week-starts-on.ts +++ b/packages/dataviews/src/utils/week-starts-on.ts @@ -1,30 +1,37 @@ +type DayNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6; +type DayString = + | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday'; +const days: DayString[] = [ + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', +]; +const DEFAULT_DAY_STRING = 'sunday'; +const DEFAULT_DAY_NUMBER = 0; + /** * Converts a weekStartsOn string to a number (0-6). * * @param day - The day name ('sunday', 'monday', etc.) * @return The corresponding number (0 for Sunday, 1 for Monday, etc.) */ -export function weekStartsOnToNumber( - day: - | 'sunday' - | 'monday' - | 'tuesday' - | 'wednesday' - | 'thursday' - | 'friday' - | 'saturday' -): 0 | 1 | 2 | 3 | 4 | 5 | 6 { - const mapping = { - sunday: 0, - monday: 1, - tuesday: 2, - wednesday: 3, - thursday: 4, - friday: 5, - saturday: 6, - } as const; +export function weekStartsOnToNumber( day: DayString ): DayNumber { + const index = days.indexOf( day ); + if ( index === -1 ) { + return DEFAULT_DAY_NUMBER; + } - return mapping[ day ]; + return index as DayNumber; } /** @@ -33,25 +40,11 @@ export function weekStartsOnToNumber( * @param day - The day number (0 for Sunday, 1 for Monday, etc.) * @return The corresponding day name ('sunday', 'monday', etc.) */ -export function numberToWeekStartsOn( - day: 0 | 1 | 2 | 3 | 4 | 5 | 6 -): - | 'sunday' - | 'monday' - | 'tuesday' - | 'wednesday' - | 'thursday' - | 'friday' - | 'saturday' { - const mapping = { - 0: 'sunday', - 1: 'monday', - 2: 'tuesday', - 3: 'wednesday', - 4: 'thursday', - 5: 'friday', - 6: 'saturday', - } as const; +export function numberToWeekStartsOn( day: DayNumber ): DayString { + const result = days[ day ]; + if ( result === undefined ) { + return DEFAULT_DAY_STRING; + } - return mapping[ day ]; + return result; } From 484f3fe502aec7f392d5b793fb2dbaaa049b81ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:57:30 +0100 Subject: [PATCH 12/16] Prioritize runtime safety and code maintanability over type hints --- packages/dataviews/src/types/field-api.ts | 17 +----- .../dataviews/src/utils/normalize-fields.ts | 55 +++++++------------ 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index e30cf9cb07039a..75b3df648b4655 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -335,7 +335,7 @@ type FormatDate = { | 'saturday'; }; -type NormalizedFieldBase< Item > = Omit< Field< Item >, 'Edit' > & { +export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { label: string; header: string | ReactElement; getValue: ( args: { item: Item } ) => any; @@ -349,22 +349,9 @@ type NormalizedFieldBase< Item > = Omit< Field< Item >, 'Edit' > & { enableSorting: boolean; filterBy: NormalizedFilterByConfig | false; readOnly: boolean; + format: {} | Required< FormatDate >; }; -export type NormalizedFieldDate< Item > = NormalizedFieldBase< Item > & { - type: 'date'; - format: Required< FormatDate >; -}; - -export type NormalizedFieldGeneric< Item > = NormalizedFieldBase< Item > & { - type?: Exclude< FieldType, 'date' >; - format: {}; -}; - -export type NormalizedField< Item > = - | NormalizedFieldDate< Item > - | NormalizedFieldGeneric< Item >; - /** * A collection of dataview fields for a data type. */ diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index 95d9c8e9a61305..bf853ada0d66e6 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -188,15 +188,25 @@ export default function normalizeFields< Item >( const filterBy = getFilterBy( field, fieldTypeDefinition ); - // TypeScript is unable to figure out that we're returning the proper types - // (either NormalizedFieldDate or NormalizedFieldGeneric) - // when the type is part of the object spread below. - // - // Hence, why we remove it and add back it later. - const { type, ...fieldWithoutType } = field; - - const baseField = { - ...fieldWithoutType, + const format = + field.type === 'date' + ? { + date: + field.format?.date !== undefined && + typeof field.format.date === 'string' + ? field.format.date + : getSettings().formats.date, + weekStartsOn: + field.format?.weekStartsOn !== undefined + ? field.format.weekStartsOn + : numberToWeekStartsOn( + getSettings().l10n.startOfWeek + ), + } + : {}; + + return { + ...field, label: field.label || field.id, header: field.header || field.label || field.id, getValue, @@ -213,32 +223,7 @@ export default function normalizeFields< Item >( true, filterBy, readOnly: field.readOnly ?? fieldTypeDefinition.readOnly ?? false, - }; - - if ( field.type === 'date' ) { - return { - ...baseField, - type: 'date' as const, - format: { - date: - field.format?.date !== undefined && - typeof field.format.date === 'string' - ? field.format.date - : getSettings().formats.date, - weekStartsOn: - field.format?.weekStartsOn !== undefined - ? field.format.weekStartsOn - : numberToWeekStartsOn( - getSettings().l10n.startOfWeek - ), - }, - }; - } - - return { - ...baseField, - type: field.type, - format: {}, + format, }; } ); } From af2e6182910c530f2d23e87cab38c23b4c754c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:01:19 +0100 Subject: [PATCH 13/16] Improve README --- packages/dataviews/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index fd6045cb410dee..7288869cdb8e44 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1682,8 +1682,8 @@ Display format configuration for fields. Currently supported for date fields. Th - Type: `object`. - Optional. - Properties: - - `date`: The format string using PHP date format (e.g., 'F j, Y' for 'March 10, 2023'). Optional, defaults to WordPress date format settings. - - `weekStartsOn`: Specifies the first day of the week for calendar controls. One of `'sunday'`, `'monday'`, `'tuesday'`, `'wednesday'`, `'thursday'`, `'friday'`, `'saturday'`. Optional, defaults to WordPress date format settings. + - `date`: The format string using PHP date format (e.g., 'F j, Y' for 'March 10, 2023'). Optional, defaults to WordPress "Date Format" setting. + - `weekStartsOn`: Specifies the first day of the week for calendar controls. One of `'sunday'`, `'monday'`, `'tuesday'`, `'wednesday'`, `'thursday'`, `'friday'`, `'saturday'`. Optional, defaults to WordPress "Week Starts On" setting. Example: From 14a756bee13ef6cff2f42674f436fd5722638911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:10:27 +0100 Subject: [PATCH 14/16] Improve readibility of format normalization --- packages/dataviews/src/types/field-api.ts | 18 +++++----- .../dataviews/src/utils/normalize-fields.ts | 34 +++++++++---------- .../dataviews/src/utils/week-starts-on.ts | 20 +++++------ 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index 75b3df648b4655..f9cc398c2ec9bc 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -325,15 +325,17 @@ export type Field< Item > = { */ type FormatDate = { date?: string; - weekStartsOn?: - | 'sunday' - | 'monday' - | 'tuesday' - | 'wednesday' - | 'thursday' - | 'friday' - | 'saturday'; + weekStartsOn?: DayString; }; +export type DayNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6; +export type DayString = + | 'sunday' + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday'; export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { label: string; diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index bf853ada0d66e6..5ed805bbf4c933 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -13,6 +13,7 @@ import { getSettings } from '@wordpress/date'; */ import getFieldTypeDefinition from '../field-types'; import type { + DayString, DataViewRenderFieldProps, Field, FieldTypeDefinition, @@ -26,7 +27,7 @@ import { SINGLE_SELECTION_OPERATORS, } from '../constants'; import hasElements from './has-elements'; -import { numberToWeekStartsOn } from './week-starts-on'; +import { numberToWeekStartsOn, DAYS_OF_WEEK } from './week-starts-on'; const getValueFromId = ( id: string ) => @@ -188,22 +189,21 @@ export default function normalizeFields< Item >( const filterBy = getFilterBy( field, fieldTypeDefinition ); - const format = - field.type === 'date' - ? { - date: - field.format?.date !== undefined && - typeof field.format.date === 'string' - ? field.format.date - : getSettings().formats.date, - weekStartsOn: - field.format?.weekStartsOn !== undefined - ? field.format.weekStartsOn - : numberToWeekStartsOn( - getSettings().l10n.startOfWeek - ), - } - : {}; + let format = {}; + if ( field.type === 'date' ) { + format = { + date: + field.format?.date !== undefined && + typeof field.format.date === 'string' + ? field.format.date + : getSettings().formats.date, + weekStartsOn: DAYS_OF_WEEK.includes( + field.format?.weekStartsOn as DayString + ) + ? field?.format?.weekStartsOn + : numberToWeekStartsOn( getSettings().l10n.startOfWeek ), + }; + } return { ...field, diff --git a/packages/dataviews/src/utils/week-starts-on.ts b/packages/dataviews/src/utils/week-starts-on.ts index f31c4569072e06..60651023751289 100644 --- a/packages/dataviews/src/utils/week-starts-on.ts +++ b/packages/dataviews/src/utils/week-starts-on.ts @@ -1,13 +1,9 @@ -type DayNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6; -type DayString = - | 'sunday' - | 'monday' - | 'tuesday' - | 'wednesday' - | 'thursday' - | 'friday' - | 'saturday'; -const days: DayString[] = [ +/** + * Internal dependencies + */ +import type { DayNumber, DayString } from '../types/field-api'; + +export const DAYS_OF_WEEK: DayString[] = [ 'sunday', 'monday', 'tuesday', @@ -26,7 +22,7 @@ const DEFAULT_DAY_NUMBER = 0; * @return The corresponding number (0 for Sunday, 1 for Monday, etc.) */ export function weekStartsOnToNumber( day: DayString ): DayNumber { - const index = days.indexOf( day ); + const index = DAYS_OF_WEEK.indexOf( day ); if ( index === -1 ) { return DEFAULT_DAY_NUMBER; } @@ -41,7 +37,7 @@ export function weekStartsOnToNumber( day: DayString ): DayNumber { * @return The corresponding day name ('sunday', 'monday', etc.) */ export function numberToWeekStartsOn( day: DayNumber ): DayString { - const result = days[ day ]; + const result = DAYS_OF_WEEK[ day ]; if ( result === undefined ) { return DEFAULT_DAY_STRING; } From 988a195e880a87f8081ad5fbdcbd382e08f8fb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:52:40 +0100 Subject: [PATCH 15/16] Better types --- packages/dataviews/src/types/field-api.ts | 17 +++++- .../dataviews/src/utils/normalize-fields.ts | 59 +++++++++++++------ 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index f9cc398c2ec9bc..6a0cc90bbde59f 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -337,7 +337,7 @@ export type DayString = | 'friday' | 'saturday'; -export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { +type NormalizedFieldBase< Item > = Omit< Field< Item >, 'Edit' > & { label: string; header: string | ReactElement; getValue: ( args: { item: Item } ) => any; @@ -351,9 +351,22 @@ export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & { enableSorting: boolean; filterBy: NormalizedFilterByConfig | false; readOnly: boolean; - format: {} | Required< FormatDate >; }; +type NormalizedFieldDate< Item > = NormalizedFieldBase< Item > & { + type: 'date'; + format: Required< FormatDate >; +}; + +type NormalizedFieldGeneric< Item > = NormalizedFieldBase< Item > & { + type?: Exclude< FieldType, 'date' >; + format: {}; +}; + +export type NormalizedField< Item > = + | NormalizedFieldGeneric< Item > + | NormalizedFieldDate< Item >; + /** * A collection of dataview fields for a data type. */ diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index 5ed805bbf4c933..77f5e958c824c1 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -189,24 +189,22 @@ export default function normalizeFields< Item >( const filterBy = getFilterBy( field, fieldTypeDefinition ); - let format = {}; - if ( field.type === 'date' ) { - format = { - date: - field.format?.date !== undefined && - typeof field.format.date === 'string' - ? field.format.date - : getSettings().formats.date, - weekStartsOn: DAYS_OF_WEEK.includes( - field.format?.weekStartsOn as DayString - ) - ? field?.format?.weekStartsOn - : numberToWeekStartsOn( getSettings().l10n.startOfWeek ), - }; - } - - return { - ...field, + /** + * NormalizedField is a discriminated union type: the shape of the format property + * depends on the type property. For example, for the 'date' type, the format + * contains date or weekStartsOn — which are not valid for other types. + * + * Being type and format interdependent, we need to write the code + * in a way that TypeScript is able to statically infer the types. + * That's why we have a return branch for every item in the union type. + * + * See a longer explanation with examples at + * https://github.com/WordPress/gutenberg/pull/72999#discussion_r2523145453 + */ + const { type, ...fieldWithoutType } = field; + + const baseField = { + ...fieldWithoutType, label: field.label || field.id, header: field.header || field.label || field.id, getValue, @@ -223,7 +221,30 @@ export default function normalizeFields< Item >( true, filterBy, readOnly: field.readOnly ?? fieldTypeDefinition.readOnly ?? false, - format, + format: {}, }; + + if ( field.type === 'date' ) { + const format = { + date: + field.format?.date !== undefined && + typeof field.format.date === 'string' + ? field.format.date + : getSettings().formats.date, + weekStartsOn: DAYS_OF_WEEK.includes( + field.format?.weekStartsOn as DayString + ) + ? field?.format?.weekStartsOn + : numberToWeekStartsOn( getSettings().l10n.startOfWeek ), + }; + + return { + ...baseField, + type: 'date', + format, + }; + } + + return { ...baseField, type: field.type, format: {} }; } ); } From ddae41b73e467424f8609cebdb153c379c081de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:19:22 +0100 Subject: [PATCH 16/16] Improve checks for weekStartsOn --- packages/dataviews/src/utils/normalize-fields.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts index 77f5e958c824c1..42fda2e86a2e01 100644 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ b/packages/dataviews/src/utils/normalize-fields.ts @@ -231,11 +231,15 @@ export default function normalizeFields< Item >( typeof field.format.date === 'string' ? field.format.date : getSettings().formats.date, - weekStartsOn: DAYS_OF_WEEK.includes( - field.format?.weekStartsOn as DayString - ) - ? field?.format?.weekStartsOn - : numberToWeekStartsOn( getSettings().l10n.startOfWeek ), + weekStartsOn: + field.format?.weekStartsOn !== undefined && + DAYS_OF_WEEK.includes( + field.format?.weekStartsOn as DayString + ) + ? field.format.weekStartsOn + : numberToWeekStartsOn( + getSettings().l10n.startOfWeek + ), }; return {