diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 710b46241230d4..4a5b6b09c4be28 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -4,6 +4,7 @@ ### Enhancements +- Simplify field normalization and types. [#73387](https://github.com/WordPress/gutenberg/pull/73387) - DataViews table layout: make checkboxes permanently visible when bulk actions are available. [#73245](https://github.com/WordPress/gutenberg/pull/73245) - Documentation: surface better the `type` property in the documentation. [#73349](https://github.com/WordPress/gutenberg/pull/73349) - DataViews: Make sticky elements (table headers, footer, actions column) inherit background colors from parent container. This allows DataViews instances to seamlessly adapt to containers with custom background colors. [#73240](https://github.com/WordPress/gutenberg/pull/73240) diff --git a/packages/dataviews/src/components/dataform/index.tsx b/packages/dataviews/src/components/dataform/index.tsx index f9d9e3025e730e..321af6f9302669 100644 --- a/packages/dataviews/src/components/dataform/index.tsx +++ b/packages/dataviews/src/components/dataform/index.tsx @@ -8,7 +8,7 @@ import { useMemo } from '@wordpress/element'; */ import type { DataFormProps } from '../../types'; import { DataFormProvider } from '../dataform-context'; -import normalizeFields from '../../utils/normalize-fields'; +import normalizeFields from '../../field-types/utils/normalize-fields'; import { DataFormLayout } from '../../dataform-layouts/data-form-layout'; import normalizeForm from '../../dataform-layouts/normalize-form'; diff --git a/packages/dataviews/src/components/dataviews-filters/filter.tsx b/packages/dataviews/src/components/dataviews-filters/filter.tsx index bb21598b43b225..f41c50dd1e6ba1 100644 --- a/packages/dataviews/src/components/dataviews-filters/filter.tsx +++ b/packages/dataviews/src/components/dataviews-filters/filter.tsx @@ -21,9 +21,6 @@ import { useRef, createInterpolateElement } from '@wordpress/element'; import { closeSmall } from '@wordpress/icons'; import { dateI18n, getDate } from '@wordpress/date'; -const ENTER = 'Enter'; -const SPACE = ' '; - /** * Internal dependencies */ @@ -57,6 +54,7 @@ import { import type { Filter, NormalizedField, + NormalizedFieldDate, NormalizedFilter, Operator, Option, @@ -65,6 +63,9 @@ import type { import useElements from '../../hooks/use-elements'; import parseDateTime from '../../field-types/utils/parse-date-time'; +const ENTER = 'Enter'; +const SPACE = ' '; + interface FilterTextProps { activeElements: Option[]; filterInView?: Filter; @@ -503,7 +504,10 @@ export default function Filter( { try { const dateValue = parseDateTime( label ); if ( dateValue !== null ) { - label = dateI18n( field.format.date, getDate( label ) ); + label = dateI18n( + ( field as NormalizedFieldDate< any > ).format.date, + getDate( label ) + ); } } catch ( e ) { label = filterInView.value; diff --git a/packages/dataviews/src/components/dataviews-picker/index.tsx b/packages/dataviews/src/components/dataviews-picker/index.tsx index df831a79a9e506..a97bb40825978f 100644 --- a/packages/dataviews/src/components/dataviews-picker/index.tsx +++ b/packages/dataviews/src/components/dataviews-picker/index.tsx @@ -29,7 +29,7 @@ import DataViewsViewConfig, { DataviewsViewConfigDropdown, ViewTypeMenu, } from '../dataviews-view-config'; -import normalizeFields from '../../utils/normalize-fields'; +import normalizeFields from '../../field-types/utils/normalize-fields'; import type { ActionButton, Field, View, SupportedLayouts } from '../../types'; import type { SelectionOrUpdater } from '../../types/private'; type ItemWithId = { id: string }; diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index 9d5870174f9db7..07cda5d91bd9b7 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -30,7 +30,7 @@ import DataViewsViewConfig, { DataviewsViewConfigDropdown, ViewTypeMenu, } from '../dataviews-view-config'; -import normalizeFields from '../../utils/normalize-fields'; +import normalizeFields from '../../field-types/utils/normalize-fields'; import type { Action, Field, View, SupportedLayouts } from '../../types'; import type { SelectionOrUpdater } from '../../types/private'; type ItemWithId = { id: string }; diff --git a/packages/dataviews/src/dataform-controls/date.tsx b/packages/dataviews/src/dataform-controls/date.tsx index ae17fd3bd65869..64cf4139d7a1f0 100644 --- a/packages/dataviews/src/dataform-controls/date.tsx +++ b/packages/dataviews/src/dataform-controls/date.tsx @@ -48,10 +48,11 @@ import { unlock } from '../lock-unlock'; import type { DataFormControlProps, FieldValidity, + FormatDate, NormalizedField, } from '../types'; import getCustomValidity from './utils/get-custom-validity'; -import { weekStartsOnToNumber } from '../utils/week-starts-on'; +import { weekStartsOnToNumber } from '../field-types/utils/week-starts-on'; const { DateCalendar, DateRangeCalendar } = unlock( componentsPrivateApis ); @@ -271,9 +272,13 @@ function CalendarDateControl< Item >( { null ); - let weekStartsOn; + let weekStartsOn = getSettings().l10n.startOfWeek; if ( type === 'date' ) { - weekStartsOn = weekStartsOnToNumber( fieldFormat.weekStartsOn ); + // If the field type is date, we've already normalized the format, + // and so it's safe to tell TypeScript to trust us ("as Required"). + weekStartsOn = weekStartsOnToNumber( + ( fieldFormat as Required< FormatDate > ).weekStartsOn + ); } const fieldValue = getValue( { item: data } ); @@ -437,7 +442,11 @@ function CalendarDateRangeControl< Item >( { let weekStartsOn; if ( type === 'date' ) { - weekStartsOn = weekStartsOnToNumber( fieldFormat.weekStartsOn ); + // If the field type is date, we've already normalized the format, + // and so it's safe to tell TypeScript to trust us ("as Required"). + weekStartsOn = weekStartsOnToNumber( + ( fieldFormat as Required< FormatDate > ).weekStartsOn + ); } const onChangeCallback = useCallback( diff --git a/packages/dataviews/src/dataform-controls/index.tsx b/packages/dataviews/src/dataform-controls/index.tsx index 1f351d85d592b2..a2b97ee3a8016c 100644 --- a/packages/dataviews/src/dataform-controls/index.tsx +++ b/packages/dataviews/src/dataform-controls/index.tsx @@ -6,12 +6,7 @@ import type { ComponentType } from 'react'; /** * Internal dependencies */ -import type { - DataFormControlProps, - Field, - FieldTypeDefinition, - EditConfig, -} from '../types'; +import type { DataFormControlProps, Field, EditConfig } from '../types'; import checkbox from './checkbox'; import datetime from './datetime'; import date from './date'; @@ -29,7 +24,7 @@ import toggleGroup from './toggle-group'; import array from './array'; import color from './color'; import password from './password'; -import hasElements from '../utils/has-elements'; +import hasElements from '../field-types/utils/has-elements'; interface FormControls { [ key: string ]: ComponentType< DataFormControlProps< any > >; @@ -64,6 +59,9 @@ function isEditConfig( value: any ): value is EditConfig { function createConfiguredControl( config: EditConfig ) { const { control, ...controlConfig } = config; const BaseControlType = getControlByType( control ); + if ( BaseControlType === null ) { + return null; + } return function ConfiguredControl< Item >( props: DataFormControlProps< Item > @@ -74,8 +72,8 @@ function createConfiguredControl( config: EditConfig ) { export function getControl< Item >( field: Field< Item >, - fieldTypeDefinition: FieldTypeDefinition< Item > -) { + fallback: string | null +): ComponentType< DataFormControlProps< Item > > | null { if ( typeof field.Edit === 'function' ) { return field.Edit; } @@ -92,15 +90,11 @@ export function getControl< Item >( return getControlByType( 'select' ); } - if ( typeof fieldTypeDefinition.Edit === 'string' ) { - return getControlByType( fieldTypeDefinition.Edit ); - } - - if ( isEditConfig( fieldTypeDefinition.Edit ) ) { - return createConfiguredControl( fieldTypeDefinition.Edit ); + if ( fallback === null ) { + return null; } - return fieldTypeDefinition.Edit; + return getControlByType( fallback ); } export function getControlByType( type: string ) { @@ -108,5 +102,5 @@ export function getControlByType( type: string ) { return FORM_CONTROLS[ type ]; } - throw 'Control ' + type + ' not found'; + return null; } diff --git a/packages/dataviews/src/field-types/array.tsx b/packages/dataviews/src/field-types/array.tsx index a4e78871a34681..50fc08b519a645 100644 --- a/packages/dataviews/src/field-types/array.tsx +++ b/packages/dataviews/src/field-types/array.tsx @@ -8,9 +8,11 @@ import { __ } from '@wordpress/i18n'; */ import type { DataViewRenderFieldProps, - SortDirection, - FieldTypeDefinition, + Field, NormalizedField, + Operator, + Rules, + SortDirection, } from '../types'; import { OPERATOR_IS_ALL, @@ -18,35 +20,54 @@ import { OPERATOR_IS_NONE, OPERATOR_IS_NOT_ALL, } from '../constants'; - -// Sort arrays by length, then alphabetically by joined string -function sort( valueA: any, valueB: any, direction: SortDirection ) { - const arrA = Array.isArray( valueA ) ? valueA : []; - const arrB = Array.isArray( valueB ) ? valueB : []; - if ( arrA.length !== arrB.length ) { - return direction === 'asc' - ? arrA.length - arrB.length - : arrB.length - arrA.length; - } - - const joinedA = arrA.join( ',' ); - const joinedB = arrB.join( ',' ); - return direction === 'asc' - ? joinedA.localeCompare( joinedB ) - : joinedB.localeCompare( joinedA ); -} +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; function render( { item, field }: DataViewRenderFieldProps< any > ) { const value = field.getValue( { item } ) || []; return value.join( ', ' ); } -const arrayFieldType: FieldTypeDefinition< any > = { - sort, - isValid: { +const defaultOperators: Operator[] = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; +const validOperators: Operator[] = [ + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, +]; + +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + + const sort = ( a: any, b: any, direction: SortDirection ) => { + // Sort arrays by length, then alphabetically by joined string + const valueA = getValue( a ); + const valueB = getValue( b ); + const arrA = Array.isArray( valueA ) ? valueA : []; + const arrB = Array.isArray( valueB ) ? valueB : []; + if ( arrA.length !== arrB.length ) { + return direction === 'asc' + ? arrA.length - arrB.length + : arrB.length - arrA.length; + } + + const joinedA = arrA.join( ',' ); + const joinedB = arrB.join( ',' ); + return direction === 'asc' + ? joinedA.localeCompare( joinedB ) + : joinedB.localeCompare( joinedA ); + }; + + const isValid: Rules< Item > = { elements: true, - custom: ( item: any, field: NormalizedField< any > ) => { - const value = field.getValue( { item } ); + custom: ( item: any, normalizedField ) => { + const value = normalizedField.getValue( { item } ); if ( ! [ undefined, '', null ].includes( value ) && @@ -62,19 +83,33 @@ const arrayFieldType: FieldTypeDefinition< any > = { return null; }, - }, - Edit: 'array', // Use array control - render, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ], - validOperators: [ - OPERATOR_IS_ANY, - OPERATOR_IS_NONE, - OPERATOR_IS_ALL, - OPERATOR_IS_NOT_ALL, - ], - }, -}; + }; -export default arrayFieldType; + return { + id: field.id, + type: 'array', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'array' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/boolean.tsx b/packages/dataviews/src/field-types/boolean.tsx index 6e58c059f31e2b..3c1ab3059f4dd6 100644 --- a/packages/dataviews/src/field-types/boolean.tsx +++ b/packages/dataviews/src/field-types/boolean.tsx @@ -8,36 +8,65 @@ import { __ } from '@wordpress/i18n'; */ import type { DataViewRenderFieldProps, - SortDirection, - FieldTypeDefinition, + Field, NormalizedField, + Operator, + Rules, + SortDirection, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { OPERATOR_IS, OPERATOR_IS_NOT } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( a: any, b: any, direction: SortDirection ) { - const boolA = Boolean( a ); - const boolB = Boolean( b ); +function render( { item, field }: DataViewRenderFieldProps< any > ) { + if ( field.hasElements ) { + return ; + } - if ( boolA === boolB ) { - return 0; + if ( field.getValue( { item } ) === true ) { + return __( 'True' ); } - // In ascending order, false comes before true - if ( direction === 'asc' ) { - return boolA ? 1 : -1; + if ( field.getValue( { item } ) === false ) { + return __( 'False' ); } - // In descending order, true comes before false - return boolA ? -1 : 1; + return null; } -export default { - sort, - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + + const sort = ( a: any, b: any, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + const boolA = Boolean( valueA ); + const boolB = Boolean( valueB ); + + if ( boolA === boolB ) { + return 0; + } + + // In ascending order, false comes before true + if ( direction === 'asc' ) { + return boolA ? 1 : -1; + } + + // In descending order, true comes before false + return boolA ? -1 : 1; + }; + + const isValid: Rules< Item > = { elements: true, - custom: ( item: any, field: NormalizedField< any > ) => { - const value = field.getValue( { item } ); + custom: ( item: any, normalizedField ) => { + const value = normalizedField.getValue( { item } ); if ( ! [ undefined, '', null ].includes( value ) && @@ -48,26 +77,37 @@ export default { return null; }, - }, - Edit: 'checkbox', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - if ( field.hasElements ) { - return ; - } + }; - if ( field.getValue( { item } ) === true ) { - return __( 'True' ); - } + const defaultOperators: Operator[] = [ OPERATOR_IS, OPERATOR_IS_NOT ]; - if ( field.getValue( { item } ) === false ) { - return __( 'False' ); - } + const validOperators: Operator[] = [ OPERATOR_IS, OPERATOR_IS_NOT ]; - return null; - }, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS, OPERATOR_IS_NOT ], - validOperators: [ OPERATOR_IS, OPERATOR_IS_NOT ], - }, -} satisfies FieldTypeDefinition< any >; + return { + id: field.id, + type: 'boolean', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'checkbox' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/color.tsx b/packages/dataviews/src/field-types/color.tsx index a293e77f3a6280..907c62f8ee0620 100644 --- a/packages/dataviews/src/field-types/color.tsx +++ b/packages/dataviews/src/field-types/color.tsx @@ -13,9 +13,11 @@ import { __ } from '@wordpress/i18n'; */ import type { DataViewRenderFieldProps, - SortDirection, + Field, NormalizedField, - FieldTypeDefinition, + Operator, + Rules, + SortDirection, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { @@ -24,41 +26,79 @@ import { OPERATOR_IS_NONE, OPERATOR_IS_NOT, } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( valueA: any, valueB: any, direction: SortDirection ) { - // Convert colors to HSL for better sorting - const colorA = colord( valueA ); - const colorB = colord( valueB ); - - if ( ! colorA.isValid() && ! colorB.isValid() ) { - return 0; - } - if ( ! colorA.isValid() ) { - return direction === 'asc' ? 1 : -1; - } - if ( ! colorB.isValid() ) { - return direction === 'asc' ? -1 : 1; +function render( { item, field }: DataViewRenderFieldProps< any > ) { + if ( field.hasElements ) { + return ; } - // Sort by hue, then saturation, then lightness - const hslA = colorA.toHsl(); - const hslB = colorB.toHsl(); + const value = field.getValue( { item } ); - if ( hslA.h !== hslB.h ) { - return direction === 'asc' ? hslA.h - hslB.h : hslB.h - hslA.h; - } - if ( hslA.s !== hslB.s ) { - return direction === 'asc' ? hslA.s - hslB.s : hslB.s - hslA.s; + if ( ! value || ! colord( value ).isValid() ) { + return value; } - return direction === 'asc' ? hslA.l - hslB.l : hslB.l - hslA.l; + + // Render color with visual preview + return ( +
+
+ { value } +
+ ); } -export default { - sort, - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + + const sort = ( valueA: any, valueB: any, direction: SortDirection ) => { + // Convert colors to HSL for better sorting + const colorA = colord( valueA ); + const colorB = colord( valueB ); + + if ( ! colorA.isValid() && ! colorB.isValid() ) { + return 0; + } + if ( ! colorA.isValid() ) { + return direction === 'asc' ? 1 : -1; + } + if ( ! colorB.isValid() ) { + return direction === 'asc' ? -1 : 1; + } + + // Sort by hue, then saturation, then lightness + const hslA = colorA.toHsl(); + const hslB = colorB.toHsl(); + + if ( hslA.h !== hslB.h ) { + return direction === 'asc' ? hslA.h - hslB.h : hslB.h - hslA.h; + } + if ( hslA.s !== hslB.s ) { + return direction === 'asc' ? hslA.s - hslB.s : hslB.s - hslA.s; + } + return direction === 'asc' ? hslA.l - hslB.l : hslB.l - hslA.l; + }; + + const isValid: Rules< Item > = { elements: true, - custom: ( item: any, field: NormalizedField< any > ) => { - const value = field.getValue( { item } ); + custom: ( item: any, normalizedField ) => { + const value = normalizedField.getValue( { item } ); if ( ! [ undefined, '', null ].includes( value ) && @@ -69,41 +109,42 @@ export default { return null; }, - }, - Edit: 'color', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - if ( field.hasElements ) { - return ; - } + }; - const value = field.getValue( { item } ); + const defaultOperators: Operator[] = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; - if ( ! value || ! colord( value ).isValid() ) { - return value; - } + const validOperators: Operator[] = [ + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + ]; - // Render color with visual preview - return ( -
-
- { value } -
- ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ], - validOperators: [ OPERATOR_IS, OPERATOR_IS_NOT ], - }, -} satisfies FieldTypeDefinition< any >; + return { + id: field.id, + type: 'color', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'color' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/date.tsx b/packages/dataviews/src/field-types/date.tsx index fa23f660c4b464..c6cb5ba3ad59d4 100644 --- a/packages/dataviews/src/field-types/date.tsx +++ b/packages/dataviews/src/field-types/date.tsx @@ -1,15 +1,20 @@ /** * WordPress dependencies */ -import { dateI18n, getDate } from '@wordpress/date'; +import { dateI18n, getDate, getSettings } from '@wordpress/date'; /** * Internal dependencies */ import type { DataViewRenderFieldProps, + DayString, + Field, + FormatDate, + NormalizedField, + Operator, + Rules, SortDirection, - FieldTypeDefinition, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { @@ -23,65 +28,121 @@ import { OPERATOR_OVER, OPERATOR_BETWEEN, } from '../constants'; +import { DAYS_OF_WEEK, numberToWeekStartsOn } from './utils/week-starts-on'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( a: any, b: any, direction: SortDirection ) { - const timeA = new Date( a ).getTime(); - const timeB = new Date( b ).getTime(); +function getFormat( field: Field< any > ): Required< FormatDate > { + return { + date: + field.format?.date !== undefined && + typeof field.format.date === 'string' + ? field.format.date + : getSettings().formats.date, + weekStartsOn: + field.format?.weekStartsOn !== undefined && + DAYS_OF_WEEK.includes( field.format?.weekStartsOn as DayString ) + ? field.format.weekStartsOn + : numberToWeekStartsOn( getSettings().l10n.startOfWeek ), + }; +} + +function render( { item, field }: DataViewRenderFieldProps< any > ) { + if ( field.hasElements ) { + return ; + } + + const value = field.getValue( { item } ); + if ( ! value ) { + return ''; + } + + // If the field type is date, we've already normalized the format, + // and so it's safe to tell TypeScript to trust us ("as Required"). + // + // There're no runtime paths where this render function is called with a non-date field, + // but TypeScript is unable to infer this, hence the type assertion. + let format: Required< FormatDate >; + if ( field.type !== 'date' ) { + format = getFormat( field as Field< any > ); + } else { + format = field.format as Required< FormatDate >; + } - return direction === 'asc' ? timeA - timeB : timeB - timeA; + return dateI18n( format.weekStartsOn, getDate( value ) ); } -export default { - sort, - Edit: 'date', - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, custom: () => null, - }, - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - if ( field.hasElements ) { - return ; - } + }; - const value = field.getValue( { item } ); - if ( ! value ) { - return ''; - } + const sort = ( a: Item, b: Item, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + const timeA = new Date( valueA ).getTime(); + const timeB = new Date( valueB ).getTime(); - // 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. - // There's an opportunity here to improve the type system. - if ( field.type !== 'date' ) { - return ''; - } + return direction === 'asc' ? timeA - timeB : timeB - timeA; + }; - return dateI18n( field.format.date, getDate( value ) ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ - OPERATOR_ON, - OPERATOR_NOT_ON, - OPERATOR_BEFORE, - OPERATOR_AFTER, - OPERATOR_BEFORE_INC, - OPERATOR_AFTER_INC, - OPERATOR_IN_THE_PAST, - OPERATOR_OVER, - OPERATOR_BETWEEN, - ], - validOperators: [ - OPERATOR_ON, - OPERATOR_NOT_ON, - OPERATOR_BEFORE, - OPERATOR_AFTER, - OPERATOR_BEFORE_INC, - OPERATOR_AFTER_INC, - OPERATOR_IN_THE_PAST, - OPERATOR_OVER, - OPERATOR_BETWEEN, - ], - }, -} satisfies FieldTypeDefinition< any >; + const defaultOperators: Operator[] = [ + OPERATOR_ON, + OPERATOR_NOT_ON, + OPERATOR_BEFORE, + OPERATOR_AFTER, + OPERATOR_BEFORE_INC, + OPERATOR_AFTER_INC, + OPERATOR_IN_THE_PAST, + OPERATOR_OVER, + OPERATOR_BETWEEN, + ]; + + const validOperators: Operator[] = [ + OPERATOR_ON, + OPERATOR_NOT_ON, + OPERATOR_BEFORE, + OPERATOR_AFTER, + OPERATOR_BEFORE_INC, + OPERATOR_AFTER_INC, + OPERATOR_IN_THE_PAST, + OPERATOR_OVER, + OPERATOR_BETWEEN, + ]; + + return { + id: field.id, + type: 'date', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'date' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: getFormat( field ), + }; +} diff --git a/packages/dataviews/src/field-types/datetime.tsx b/packages/dataviews/src/field-types/datetime.tsx index c857c38c9389d3..bdf424c1ec57a3 100644 --- a/packages/dataviews/src/field-types/datetime.tsx +++ b/packages/dataviews/src/field-types/datetime.tsx @@ -3,8 +3,11 @@ */ import type { DataViewRenderFieldProps, + Field, + NormalizedField, + Operator, + Rules, SortDirection, - FieldTypeDefinition, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import parseDateTime from './utils/parse-date-time'; @@ -18,59 +21,96 @@ import { OPERATOR_IN_THE_PAST, OPERATOR_OVER, } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( a: any, b: any, direction: SortDirection ) { - const timeA = new Date( a ).getTime(); - const timeB = new Date( b ).getTime(); +function render( { item, field }: DataViewRenderFieldProps< any > ) { + if ( field.elements ) { + return ; + } - return direction === 'asc' ? timeA - timeB : timeB - timeA; + const value = field.getValue( { item } ); + if ( [ '', undefined, null ].includes( value ) ) { + return null; + } + + try { + const dateValue = parseDateTime( value ); + return dateValue?.toLocaleString(); + } catch ( error ) { + return null; + } } -export default { - sort, - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, custom: () => null, - }, - Edit: 'datetime', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - if ( field.elements ) { - return ; - } + }; + + const sort = ( a: Item, b: Item, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + const timeA = new Date( valueA ).getTime(); + const timeB = new Date( valueB ).getTime(); + + return direction === 'asc' ? timeA - timeB : timeB - timeA; + }; - const value = field.getValue( { item } ); - if ( [ '', undefined, null ].includes( value ) ) { - return null; - } + const defaultOperators: Operator[] = [ + OPERATOR_ON, + OPERATOR_NOT_ON, + OPERATOR_BEFORE, + OPERATOR_AFTER, + OPERATOR_BEFORE_INC, + OPERATOR_AFTER_INC, + OPERATOR_IN_THE_PAST, + OPERATOR_OVER, + ]; - try { - const dateValue = parseDateTime( value ); - return dateValue?.toLocaleString(); - } catch ( error ) { - return null; - } - }, - enableSorting: true, - filterBy: { - defaultOperators: [ - OPERATOR_ON, - OPERATOR_NOT_ON, - OPERATOR_BEFORE, - OPERATOR_AFTER, - OPERATOR_BEFORE_INC, - OPERATOR_AFTER_INC, - OPERATOR_IN_THE_PAST, - OPERATOR_OVER, - ], - validOperators: [ - OPERATOR_ON, - OPERATOR_NOT_ON, - OPERATOR_BEFORE, - OPERATOR_AFTER, - OPERATOR_BEFORE_INC, - OPERATOR_AFTER_INC, - OPERATOR_IN_THE_PAST, - OPERATOR_OVER, - ], - }, -} satisfies FieldTypeDefinition< any >; + const validOperators: Operator[] = [ + OPERATOR_ON, + OPERATOR_NOT_ON, + OPERATOR_BEFORE, + OPERATOR_AFTER, + OPERATOR_BEFORE_INC, + OPERATOR_AFTER_INC, + OPERATOR_IN_THE_PAST, + OPERATOR_OVER, + ]; + + return { + id: field.id, + type: 'datetime', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'datetime' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/email.tsx b/packages/dataviews/src/field-types/email.tsx index 8af0d443a9a24f..ad4e03fa6b0f49 100644 --- a/packages/dataviews/src/field-types/email.tsx +++ b/packages/dataviews/src/field-types/email.tsx @@ -8,9 +8,11 @@ import { __ } from '@wordpress/i18n'; */ import type { DataViewRenderFieldProps, - SortDirection, + Field, NormalizedField, - FieldTypeDefinition, + Operator, + Rules, + SortDirection, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { @@ -24,24 +26,43 @@ import { OPERATOR_NOT_CONTAINS, OPERATOR_STARTS_WITH, } from '../constants'; - -function sort( valueA: any, valueB: any, direction: SortDirection ) { - return direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); -} +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; // Email validation regex based on HTML5 spec // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address const emailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; -export default { - sort, - isValid: { +function render( { item, field }: DataViewRenderFieldProps< any > ) { + return field.hasElements ? ( + + ) : ( + field.getValue( { item } ) + ); +} + +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + + const sort = ( a: any, b: any, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + return direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + }; + + const isValid: Rules< Item > = { elements: true, - custom: ( item: any, field: NormalizedField< any > ) => { - const value = field.getValue( { item } ); + custom: ( item: any, normalizedField ) => { + const value = normalizedField.getValue( { item } ); if ( ! [ undefined, '', null ].includes( value ) && @@ -52,29 +73,48 @@ export default { return null; }, - }, - Edit: 'email', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - return field.hasElements ? ( - - ) : ( - field.getValue( { item } ) - ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ], - validOperators: [ - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_CONTAINS, - OPERATOR_NOT_CONTAINS, - OPERATOR_STARTS_WITH, - // Multiple selection - OPERATOR_IS_ANY, - OPERATOR_IS_NONE, - OPERATOR_IS_ALL, - OPERATOR_IS_NOT_ALL, - ], - }, -} satisfies FieldTypeDefinition< any >; + }; + + const defaultOperators: Operator[] = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; + + const validOperators: Operator[] = [ + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_CONTAINS, + OPERATOR_NOT_CONTAINS, + OPERATOR_STARTS_WITH, + // Multiple selection + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, + ]; + + return { + id: field.id, + type: 'email', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'email' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/index.tsx b/packages/dataviews/src/field-types/index.tsx index 8db2da452da65a..4534a2ff3e6d6f 100644 --- a/packages/dataviews/src/field-types/index.tsx +++ b/packages/dataviews/src/field-types/index.tsx @@ -3,8 +3,10 @@ */ import type { DataViewRenderFieldProps, + Field, FieldType, - FieldTypeDefinition, + NormalizedField, + Operator, SortDirection, } from '../types'; import { default as email } from './email'; @@ -22,6 +24,78 @@ import { default as color } from './color'; import { default as url } from './url'; import RenderFromElements from './utils/render-from-elements'; import { ALL_OPERATORS, OPERATOR_IS, OPERATOR_IS_NOT } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; + +function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + + const isValid = { + elements: true, + custom: () => null, + }; + + const sort = ( a: any, b: any, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + + if ( typeof valueA === 'number' && typeof valueB === 'number' ) { + return direction === 'asc' ? valueA - valueB : valueB - valueA; + } + + return direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + }; + + const render = ( { + item, + field: normalizedField, + }: DataViewRenderFieldProps< Item > ) => { + return normalizedField.hasElements ? ( + + ) : ( + normalizedField.getValue( { item } ) + ); + }; + + const defaultOperators: Operator[] = [ OPERATOR_IS, OPERATOR_IS_NOT ]; + const validOperators: Operator[] = ALL_OPERATORS; + + return { + id: field.id, + // type — it does not have a type + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, null ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} /** * @@ -29,9 +103,9 @@ import { ALL_OPERATORS, OPERATOR_IS, OPERATOR_IS_NOT } from '../constants'; * * @return A field type definition. */ -export default function getFieldTypeDefinition< Item >( +export default function getNormalizeFieldFunction< Item >( type?: FieldType -): FieldTypeDefinition< Item > { +): ( field: Field< Item > ) => NormalizedField< Item > { if ( 'email' === type ) { return email; } @@ -86,32 +160,5 @@ export default function getFieldTypeDefinition< Item >( // This is a fallback for fields that don't provide a type. // It can be removed when the field.type is mandatory. - return { - sort: ( a: any, b: any, direction: SortDirection ) => { - if ( typeof a === 'number' && typeof b === 'number' ) { - return direction === 'asc' ? a - b : b - a; - } - - return direction === 'asc' - ? a.localeCompare( b ) - : b.localeCompare( a ); - }, - isValid: { - elements: true, - custom: () => null, - }, - Edit: null, - render: ( { item, field }: DataViewRenderFieldProps< Item > ) => { - return field.hasElements ? ( - - ) : ( - field.getValue( { item } ) - ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS, OPERATOR_IS_NOT ], - validOperators: ALL_OPERATORS, - }, - }; + return normalizeField; } diff --git a/packages/dataviews/src/field-types/integer.tsx b/packages/dataviews/src/field-types/integer.tsx index 7674cdb9d3329c..19864a1fab7ae8 100644 --- a/packages/dataviews/src/field-types/integer.tsx +++ b/packages/dataviews/src/field-types/integer.tsx @@ -8,9 +8,11 @@ import { __ } from '@wordpress/i18n'; */ import type { DataViewRenderFieldProps, - SortDirection, + Field, NormalizedField, - FieldTypeDefinition, + Operator, + Rules, + SortDirection, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { @@ -26,17 +28,29 @@ import { OPERATOR_IS_NOT_ALL, OPERATOR_BETWEEN, } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( a: any, b: any, direction: SortDirection ) { - return direction === 'asc' ? a - b : b - a; +function render( { item, field }: DataViewRenderFieldProps< any > ) { + return field.hasElements ? ( + + ) : ( + field.getValue( { item } ) + ); } -export default { - sort, - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, - custom: ( item: any, field: NormalizedField< any > ) => { - const value = field.getValue( { item } ); + custom: ( item: any, normalizedField ) => { + const value = normalizedField.getValue( { item } ); if ( ! [ undefined, '', null ].includes( value ) && ! Number.isInteger( value ) @@ -46,40 +60,65 @@ export default { return null; }, - }, - Edit: 'integer', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - return field.hasElements ? ( - - ) : ( - field.getValue( { item } ) - ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_LESS_THAN, - OPERATOR_GREATER_THAN, - OPERATOR_LESS_THAN_OR_EQUAL, - OPERATOR_GREATER_THAN_OR_EQUAL, - OPERATOR_BETWEEN, - ], - validOperators: [ - // Single-selection - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_LESS_THAN, - OPERATOR_GREATER_THAN, - OPERATOR_LESS_THAN_OR_EQUAL, - OPERATOR_GREATER_THAN_OR_EQUAL, - OPERATOR_BETWEEN, - // Multiple-selection - OPERATOR_IS_ANY, - OPERATOR_IS_NONE, - OPERATOR_IS_ALL, - OPERATOR_IS_NOT_ALL, - ], - }, -} satisfies FieldTypeDefinition< any >; + }; + + const sort = ( a: Item, b: Item, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + return direction === 'asc' ? valueA - valueB : valueB - valueA; + }; + + const defaultOperators: Operator[] = [ + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_LESS_THAN, + OPERATOR_GREATER_THAN, + OPERATOR_LESS_THAN_OR_EQUAL, + OPERATOR_GREATER_THAN_OR_EQUAL, + OPERATOR_BETWEEN, + ]; + + const validOperators: Operator[] = [ + // Single-selection + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_LESS_THAN, + OPERATOR_GREATER_THAN, + OPERATOR_LESS_THAN_OR_EQUAL, + OPERATOR_GREATER_THAN_OR_EQUAL, + OPERATOR_BETWEEN, + // Multiple-selection + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, + ]; + + return { + id: field.id, + type: 'integer', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'integer' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/media.tsx b/packages/dataviews/src/field-types/media.tsx index 62fd76458a0ffa..6e2ba665110189 100644 --- a/packages/dataviews/src/field-types/media.tsx +++ b/packages/dataviews/src/field-types/media.tsx @@ -1,20 +1,55 @@ /** * Internal dependencies */ -import type { FieldTypeDefinition } from '../types'; +import type { Field, NormalizedField, Rules } from '../types'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; function sort() { return 0; } -export default { - sort, - isValid: { +function render() { + return null; +} + +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, custom: () => null, - }, - Edit: null, - render: () => null, - enableSorting: false, - filterBy: false, -} satisfies FieldTypeDefinition< any >; + }; + + return { + id: field.id, + type: 'media', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, null ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? false, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: false, + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/number.tsx b/packages/dataviews/src/field-types/number.tsx index d968c6bc66a519..ca6d54fbeb352e 100644 --- a/packages/dataviews/src/field-types/number.tsx +++ b/packages/dataviews/src/field-types/number.tsx @@ -8,9 +8,11 @@ import { __ } from '@wordpress/i18n'; */ import type { DataViewRenderFieldProps, - SortDirection, + Field, NormalizedField, - FieldTypeDefinition, + Operator, + Rules, + SortDirection, } from '../types'; import { OPERATOR_IS, @@ -26,21 +28,38 @@ import { OPERATOR_BETWEEN, } from '../constants'; import RenderFromElements from './utils/render-from-elements'; - -function sort( a: any, b: any, direction: SortDirection ) { - return direction === 'asc' ? a - b : b - a; -} +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; function isEmpty( value: unknown ): value is '' | undefined | null { return value === '' || value === undefined || value === null; } -export default { - sort, - isValid: { +function render( { item, field }: DataViewRenderFieldProps< any > ) { + if ( field.hasElements ) { + return ; + } + + const value = field.getValue( { item } ); + if ( ! [ null, undefined ].includes( value ) ) { + return Number( value ).toFixed( 2 ); + } + + return null; +} + +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, - custom: ( item: any, field: NormalizedField< any > ) => { - const value = field.getValue( { item } ); + custom: ( item: any, normalizedField ) => { + const value = normalizedField.getValue( { item } ); if ( ! isEmpty( value ) && ! Number.isFinite( value ) ) { return __( 'Value must be a number.' ); @@ -48,45 +67,65 @@ export default { return null; }, - }, - Edit: 'number', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - if ( field.hasElements ) { - ; - } + }; + + const sort = ( a: Item, b: Item, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + return direction === 'asc' ? valueA - valueB : valueB - valueA; + }; - const value = field.getValue( { item } ); - if ( ! [ null, undefined ].includes( value ) ) { - return Number( value ).toFixed( 2 ); - } + const defaultOperators: Operator[] = [ + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_LESS_THAN, + OPERATOR_GREATER_THAN, + OPERATOR_LESS_THAN_OR_EQUAL, + OPERATOR_GREATER_THAN_OR_EQUAL, + OPERATOR_BETWEEN, + ]; - return null; - }, - enableSorting: true, - filterBy: { - defaultOperators: [ - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_LESS_THAN, - OPERATOR_GREATER_THAN, - OPERATOR_LESS_THAN_OR_EQUAL, - OPERATOR_GREATER_THAN_OR_EQUAL, - OPERATOR_BETWEEN, - ], - validOperators: [ - // Single-selection - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_LESS_THAN, - OPERATOR_GREATER_THAN, - OPERATOR_LESS_THAN_OR_EQUAL, - OPERATOR_GREATER_THAN_OR_EQUAL, - OPERATOR_BETWEEN, - // Multiple-selection - OPERATOR_IS_ANY, - OPERATOR_IS_NONE, - OPERATOR_IS_ALL, - OPERATOR_IS_NOT_ALL, - ], - }, -} satisfies FieldTypeDefinition< any >; + const validOperators: Operator[] = [ + // Single-selection + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_LESS_THAN, + OPERATOR_GREATER_THAN, + OPERATOR_LESS_THAN_OR_EQUAL, + OPERATOR_GREATER_THAN_OR_EQUAL, + OPERATOR_BETWEEN, + // Multiple-selection + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, + ]; + + return { + id: field.id, + type: 'number', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'number' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/password.tsx b/packages/dataviews/src/field-types/password.tsx index 6a9d5b95273a75..ec8c5ea8dc5758 100644 --- a/packages/dataviews/src/field-types/password.tsx +++ b/packages/dataviews/src/field-types/password.tsx @@ -3,31 +3,66 @@ */ import type { DataViewRenderFieldProps, + Field, + NormalizedField, + Rules, SortDirection, - FieldTypeDefinition, } from '../types'; import RenderFromElements from './utils/render-from-elements'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function sort( valueA: any, valueB: any, direction: SortDirection ) { +function sort( _valueA: any, _valueB: any, _direction: SortDirection ) { // Passwords should not be sortable for security reasons return 0; } -export default { - sort, - isValid: { +function render( { item, field }: DataViewRenderFieldProps< any > ) { + return field.hasElements ? ( + + ) : ( + '••••••••' + ); +} + +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, custom: () => null, - }, - Edit: 'password', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - return field.hasElements ? ( - - ) : ( - '••••••••' - ); - }, - enableSorting: false, - filterBy: false, -} satisfies FieldTypeDefinition< any >; + }; + + return { + id: field.id, + type: 'password', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'password' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? false, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: false, + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/telephone.tsx b/packages/dataviews/src/field-types/telephone.tsx index 7fe98248f0ba59..19d2040c820de0 100644 --- a/packages/dataviews/src/field-types/telephone.tsx +++ b/packages/dataviews/src/field-types/telephone.tsx @@ -3,8 +3,11 @@ */ import type { DataViewRenderFieldProps, + Field, + NormalizedField, + Operator, + Rules, SortDirection, - FieldTypeDefinition, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { @@ -18,41 +21,78 @@ import { OPERATOR_NOT_CONTAINS, OPERATOR_STARTS_WITH, } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( valueA: any, valueB: any, direction: SortDirection ) { - return direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); +function render( { item, field }: DataViewRenderFieldProps< any > ) { + return field.hasElements ? ( + + ) : ( + field.getValue( { item } ) + ); } -export default { - sort, - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, custom: () => null, - }, - Edit: 'telephone', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - return field.hasElements ? ( - - ) : ( - field.getValue( { item } ) - ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ], - validOperators: [ - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_CONTAINS, - OPERATOR_NOT_CONTAINS, - OPERATOR_STARTS_WITH, - // Multiple selection - OPERATOR_IS_ANY, - OPERATOR_IS_NONE, - OPERATOR_IS_ALL, - OPERATOR_IS_NOT_ALL, - ], - }, -} satisfies FieldTypeDefinition< any >; + }; + + const sort = ( a: any, b: any, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + return direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + }; + + const defaultOperators: Operator[] = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; + + const validOperators: Operator[] = [ + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_CONTAINS, + OPERATOR_NOT_CONTAINS, + OPERATOR_STARTS_WITH, + // Multiple selection + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, + ]; + + return { + id: field.id, + type: 'telephone', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'telephone' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/text.tsx b/packages/dataviews/src/field-types/text.tsx index 5cb5944248d0c3..036863ff8ad058 100644 --- a/packages/dataviews/src/field-types/text.tsx +++ b/packages/dataviews/src/field-types/text.tsx @@ -3,8 +3,11 @@ */ import type { DataViewRenderFieldProps, + Field, + NormalizedField, + Operator, + Rules, SortDirection, - FieldTypeDefinition, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { @@ -18,42 +21,79 @@ import { OPERATOR_NOT_CONTAINS, OPERATOR_STARTS_WITH, } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( valueA: any, valueB: any, direction: SortDirection ) { - return direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); +function render( { item, field }: DataViewRenderFieldProps< any > ) { + return field.hasElements ? ( + + ) : ( + field.getValue( { item } ) + ); } -export default { - sort, - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, custom: () => null, - }, - Edit: 'text', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - return field.hasElements ? ( - - ) : ( - field.getValue( { item } ) - ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ], - validOperators: [ - // Single selection - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_CONTAINS, - OPERATOR_NOT_CONTAINS, - OPERATOR_STARTS_WITH, - // Multiple selection - OPERATOR_IS_ANY, - OPERATOR_IS_NONE, - OPERATOR_IS_ALL, - OPERATOR_IS_NOT_ALL, - ], - }, -} satisfies FieldTypeDefinition< any >; + }; + + const sort = ( a: Item, b: Item, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + return direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + }; + + const defaultOperators: Operator[] = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; + + const validOperators: Operator[] = [ + // Single selection + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_CONTAINS, + OPERATOR_NOT_CONTAINS, + OPERATOR_STARTS_WITH, + // Multiple selection + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, + ]; + + return { + id: field.id, + type: 'text', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'text' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/url.tsx b/packages/dataviews/src/field-types/url.tsx index 8d24ff96fc1f04..d43dcfd885e57f 100644 --- a/packages/dataviews/src/field-types/url.tsx +++ b/packages/dataviews/src/field-types/url.tsx @@ -3,8 +3,11 @@ */ import type { DataViewRenderFieldProps, + Field, + NormalizedField, + Operator, + Rules, SortDirection, - FieldTypeDefinition, } from '../types'; import RenderFromElements from './utils/render-from-elements'; import { @@ -18,41 +21,78 @@ import { OPERATOR_NOT_CONTAINS, OPERATOR_STARTS_WITH, } from '../constants'; +import { getControl } from '../dataform-controls'; +import hasElements from './utils/has-elements'; +import getValueFromId from './utils/get-value-from-id'; +import setValueFromId from './utils/set-value-from-id'; +import getFilterBy from './utils/get-filter-by'; -function sort( valueA: any, valueB: any, direction: SortDirection ) { - return direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); +function render( { item, field }: DataViewRenderFieldProps< any > ) { + return field.hasElements ? ( + + ) : ( + field.getValue( { item } ) + ); } -export default { - sort, - isValid: { +export default function normalizeField< Item >( + field: Field< Item > +): NormalizedField< Item > { + const getValue = field.getValue || getValueFromId( field.id ); + const setValue = field.setValue || setValueFromId( field.id ); + const isValid: Rules< Item > = { elements: true, custom: () => null, - }, - Edit: 'url', - render: ( { item, field }: DataViewRenderFieldProps< any > ) => { - return field.hasElements ? ( - - ) : ( - field.getValue( { item } ) - ); - }, - enableSorting: true, - filterBy: { - defaultOperators: [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ], - validOperators: [ - OPERATOR_IS, - OPERATOR_IS_NOT, - OPERATOR_CONTAINS, - OPERATOR_NOT_CONTAINS, - OPERATOR_STARTS_WITH, - // Multiple selection - OPERATOR_IS_ANY, - OPERATOR_IS_NONE, - OPERATOR_IS_ALL, - OPERATOR_IS_NOT_ALL, - ], - }, -} satisfies FieldTypeDefinition< any >; + }; + + const sort = ( a: any, b: any, direction: SortDirection ) => { + const valueA = getValue( { item: a } ); + const valueB = getValue( { item: b } ); + return direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + }; + + const defaultOperators: Operator[] = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; + + const validOperators: Operator[] = [ + OPERATOR_IS, + OPERATOR_IS_NOT, + OPERATOR_CONTAINS, + OPERATOR_NOT_CONTAINS, + OPERATOR_STARTS_WITH, + // Multiple selection + OPERATOR_IS_ANY, + OPERATOR_IS_NONE, + OPERATOR_IS_ALL, + OPERATOR_IS_NOT_ALL, + ]; + + return { + id: field.id, + type: 'url', + label: field.label || field.id, + header: field.header || field.label || field.id, + description: field.description, + placeholder: field.placeholder, + getValue, + setValue, + elements: field.elements, + getElements: field.getElements, + hasElements: hasElements( field ), + render: field.render ?? render, + Edit: getControl( field, 'url' ), + sort: field.sort ?? sort, + isValid: { + ...isValid, + ...field.isValid, + }, + isVisible: field.isVisible, + enableSorting: field.enableSorting ?? true, + enableGlobalSearch: field.enableGlobalSearch ?? false, + enableHiding: field.enableHiding ?? true, + readOnly: field.readOnly ?? false, + filterBy: getFilterBy( field, defaultOperators, validOperators ), + format: {}, + }; +} diff --git a/packages/dataviews/src/field-types/utils/get-filter-by.ts b/packages/dataviews/src/field-types/utils/get-filter-by.ts new file mode 100644 index 00000000000000..333ed12b959365 --- /dev/null +++ b/packages/dataviews/src/field-types/utils/get-filter-by.ts @@ -0,0 +1,50 @@ +/** + * Internal dependencies + */ +import type { Field, FilterByConfig, Operator } from '../../types'; + +function getFilterBy< Item >( + field: Field< Item >, + defaultOperators: Operator[], + validOperators: Operator[] +): Required< FilterByConfig > | false { + if ( field.filterBy === false ) { + return false; + } + + if ( typeof field.filterBy === 'object' ) { + let operators = field.filterBy.operators; + + // Assign default values if no operator was provided. + if ( ! operators || ! Array.isArray( operators ) ) { + operators = defaultOperators; + } + + // Make sure only valid operators are included. + operators = operators.filter( ( operator ) => + validOperators.includes( operator ) + ); + + // If no operators are left at this point, + // the filters should be disabled. + if ( operators.length === 0 ) { + return false; + } + + return { + isPrimary: !! field.filterBy.isPrimary, + operators, + }; + } + + if ( defaultOperators.length === 0 ) { + return false; + } + + return { + isPrimary: false, + operators: defaultOperators, + }; +} + +export default getFilterBy; diff --git a/packages/dataviews/src/field-types/utils/get-value-from-id.ts b/packages/dataviews/src/field-types/utils/get-value-from-id.ts new file mode 100644 index 00000000000000..558f42af6065e7 --- /dev/null +++ b/packages/dataviews/src/field-types/utils/get-value-from-id.ts @@ -0,0 +1,17 @@ +const getValueFromId = + ( id: string ) => + ( { item }: { item: any } ) => { + const path = id.split( '.' ); + let value = item; + for ( const segment of path ) { + if ( value.hasOwnProperty( segment ) ) { + value = value[ segment ]; + } else { + value = undefined; + } + } + + return value; + }; + +export default getValueFromId; diff --git a/packages/dataviews/src/utils/has-elements.ts b/packages/dataviews/src/field-types/utils/has-elements.ts similarity index 82% rename from packages/dataviews/src/utils/has-elements.ts rename to packages/dataviews/src/field-types/utils/has-elements.ts index 9fcee088f35a69..ae1414f649ccb8 100644 --- a/packages/dataviews/src/utils/has-elements.ts +++ b/packages/dataviews/src/field-types/utils/has-elements.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import type { Field } from '../types/field-api'; +import type { Field } from '../../types/field-api'; export default function hasElements< Item >( field: Field< Item > ): boolean { return ( diff --git a/packages/dataviews/src/field-types/utils/normalize-fields.ts b/packages/dataviews/src/field-types/utils/normalize-fields.ts new file mode 100644 index 00000000000000..0abb1f55f7332c --- /dev/null +++ b/packages/dataviews/src/field-types/utils/normalize-fields.ts @@ -0,0 +1,25 @@ +/** + * External dependencies + */ + +/** + * Internal dependencies + */ +import getNormalizeFieldFunction from '..'; +import type { Field, NormalizedField } from '../../types'; + +/** + * Apply default values and normalize the fields config. + * + * @param fields Fields config. + * @return Normalized fields config. + */ +export default function normalizeFields< Item >( + fields: Field< Item >[] +): NormalizedField< Item >[] { + return fields.map( ( field ) => { + const normalize = getNormalizeFieldFunction< Item >( field.type ); + + return normalize( field ); + } ); +} diff --git a/packages/dataviews/src/field-types/utils/set-value-from-id.ts b/packages/dataviews/src/field-types/utils/set-value-from-id.ts new file mode 100644 index 00000000000000..8419d0b0ea3cc7 --- /dev/null +++ b/packages/dataviews/src/field-types/utils/set-value-from-id.ts @@ -0,0 +1,17 @@ +const setValueFromId = + ( id: string ) => + ( { value }: { value: any } ) => { + const path = id.split( '.' ); + const result: any = {}; + let current = result; + + for ( const segment of path.slice( 0, -1 ) ) { + current[ segment ] = {}; + current = current[ segment ]; + } + + current[ path.at( -1 )! ] = value; + return result; + }; + +export default setValueFromId; diff --git a/packages/dataviews/src/utils/week-starts-on.ts b/packages/dataviews/src/field-types/utils/week-starts-on.ts similarity index 93% rename from packages/dataviews/src/utils/week-starts-on.ts rename to packages/dataviews/src/field-types/utils/week-starts-on.ts index 60651023751289..eb8576756dab88 100644 --- a/packages/dataviews/src/utils/week-starts-on.ts +++ b/packages/dataviews/src/field-types/utils/week-starts-on.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import type { DayNumber, DayString } from '../types/field-api'; +import type { DayNumber, DayString } from '../../types/field-api'; export const DAYS_OF_WEEK: DayString[] = [ 'sunday', diff --git a/packages/dataviews/src/hooks/use-form-validity.ts b/packages/dataviews/src/hooks/use-form-validity.ts index 1167c9af763644..3c7d2a3d6fb02a 100644 --- a/packages/dataviews/src/hooks/use-form-validity.ts +++ b/packages/dataviews/src/hooks/use-form-validity.ts @@ -13,7 +13,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import normalizeFields from '../utils/normalize-fields'; +import normalizeFields from '../field-types/utils/normalize-fields'; import normalizeForm from '../dataform-layouts/normalize-form'; import type { Field, diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts index 946abeff64cf82..7b1296e00c8305 100644 --- a/packages/dataviews/src/test/normalize-fields.ts +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import normalizeFields from '../utils/normalize-fields'; +import normalizeFields from '../field-types/utils/normalize-fields'; import type { Field } from '../types'; describe( 'normalizeFields: default getValue', () => { @@ -165,7 +165,10 @@ describe( 'normalizeFields: default getValue', () => { ]; const normalizedFields = normalizeFields( fields ); const result = normalizedFields[ 0 ].filterBy; - expect( result ).toStrictEqual( { operators: [ 'is', 'isNot' ] } ); + expect( result ).toStrictEqual( { + isPrimary: false, + operators: [ 'is', 'isNot' ], + } ); } ); it( 'returns the default field type definition if undefined for untyped field (for primary filters)', () => { const fields: Field< {} >[] = [ @@ -194,6 +197,7 @@ describe( 'normalizeFields: default getValue', () => { const normalizedFields = normalizeFields( fields ); const result = normalizedFields[ 0 ].filterBy; expect( result ).toStrictEqual( { + isPrimary: false, operators: [ 'is', 'isNot', @@ -216,6 +220,7 @@ describe( 'normalizeFields: default getValue', () => { const normalizedFields = normalizeFields( fields ); const result = normalizedFields[ 0 ].filterBy; expect( result ).toStrictEqual( { + isPrimary: false, operators: [ 'is', 'isNot', diff --git a/packages/dataviews/src/types/field-api.ts b/packages/dataviews/src/types/field-api.ts index 6a0cc90bbde59f..31f1011eeaa454 100644 --- a/packages/dataviews/src/types/field-api.ts +++ b/packages/dataviews/src/types/field-api.ts @@ -37,33 +37,6 @@ export interface FilterByConfig { isPrimary?: boolean; } -export interface NormalizedFilterByConfig { - /** - * The list of operators supported by the field. - */ - operators: Operator[]; - - /** - * Whether it is a primary filter. - * - * A primary filter is always visible and is not listed in the "Add filter" component, - * except for the list layout where it behaves like a secondary filter. - */ - isPrimary?: boolean; -} - -interface FilterConfigForType { - /** - * What operators are used by default. - */ - defaultOperators: Operator[]; - - /** - * What operators are supported by the field. - */ - validOperators: Operator[]; -} - export type Operator = | 'is' | 'isNot' @@ -103,51 +76,6 @@ export type FieldType = | 'url' | 'array'; -/** - * An abstract interface for Field based on the field type. - */ -export type FieldTypeDefinition< Item > = { - /** - * Callback used to sort the field. - */ - sort: ( a: Item, b: Item, direction: SortDirection ) => number; - - /** - * Callback used to validate the field. - */ - isValid: Rules< Item >; - - /** - * Callback used to render an edit control for the field or control name. - */ - Edit: - | ComponentType< DataFormControlProps< Item > > - | string - | EditConfig - | null; - - /** - * Callback used to render the field. - */ - render: ComponentType< DataViewRenderFieldProps< Item > >; - - /** - * The filter config for the field. - */ - filterBy: FilterConfigForType | false; - - /** - * Whether the field is readOnly. - * If `true`, the value will be rendered using the `render` callback. - */ - readOnly?: boolean; - - /** - * Whether the field is sortable. - */ - enableSorting: boolean; -}; - export type Rules< Item > = { required?: boolean; elements?: boolean; @@ -315,6 +243,8 @@ export type Field< Item > = { format?: FormatDate; }; +export type NormalizedFormat = Required< FormatDate > | {}; + /** * Format for date fields: * @@ -323,7 +253,7 @@ export type Field< Item > = { * * If not provided, defaults to WordPress date format settings. */ -type FormatDate = { +export type FormatDate = { date?: string; weekStartsOn?: DayString; }; @@ -349,22 +279,18 @@ type NormalizedFieldBase< Item > = Omit< Field< Item >, 'Edit' > & { isValid: Rules< Item >; enableHiding: boolean; enableSorting: boolean; - filterBy: NormalizedFilterByConfig | false; + filterBy: Required< FilterByConfig > | false; readOnly: boolean; + format: {}; }; -type NormalizedFieldDate< Item > = NormalizedFieldBase< Item > & { +export 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 > + | NormalizedFieldBase< Item > | NormalizedFieldDate< Item >; /** diff --git a/packages/dataviews/src/utils/filter-sort-and-paginate.ts b/packages/dataviews/src/utils/filter-sort-and-paginate.ts index 5738fa331fc689..362d80ab68a4bc 100644 --- a/packages/dataviews/src/utils/filter-sort-and-paginate.ts +++ b/packages/dataviews/src/utils/filter-sort-and-paginate.ts @@ -36,7 +36,7 @@ import { OPERATOR_IN_THE_PAST, OPERATOR_OVER, } from '../constants'; -import normalizeFields from './normalize-fields'; +import normalizeFields from '../field-types/utils/normalize-fields'; import type { Field, View } from '../types'; function normalizeSearchInput( input = '' ) { diff --git a/packages/dataviews/src/utils/normalize-fields.ts b/packages/dataviews/src/utils/normalize-fields.ts deleted file mode 100644 index 42fda2e86a2e01..00000000000000 --- a/packages/dataviews/src/utils/normalize-fields.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * External dependencies - */ -import type { FunctionComponent } from 'react'; - -/** - * WordPress dependencies - */ -import { getSettings } from '@wordpress/date'; - -/** - * Internal dependencies - */ -import getFieldTypeDefinition from '../field-types'; -import type { - DayString, - DataViewRenderFieldProps, - Field, - FieldTypeDefinition, - NormalizedFilterByConfig, - NormalizedField, -} from '../types'; -import { getControl } from '../dataform-controls'; -import { - ALL_OPERATORS, - OPERATOR_BETWEEN, - SINGLE_SELECTION_OPERATORS, -} from '../constants'; -import hasElements from './has-elements'; -import { numberToWeekStartsOn, DAYS_OF_WEEK } from './week-starts-on'; - -const getValueFromId = - ( id: string ) => - ( { item }: { item: any } ) => { - const path = id.split( '.' ); - let value = item; - for ( const segment of path ) { - if ( value.hasOwnProperty( segment ) ) { - value = value[ segment ]; - } else { - value = undefined; - } - } - - return value; - }; - -const setValueFromId = - ( id: string ) => - ( { value }: { value: any } ) => { - const path = id.split( '.' ); - const result: any = {}; - let current = result; - - for ( const segment of path.slice( 0, -1 ) ) { - current[ segment ] = {}; - current = current[ segment ]; - } - - current[ path.at( -1 )! ] = value; - return result; - }; - -function getFilterBy< Item >( - field: Field< Item >, - fieldTypeDefinition: FieldTypeDefinition< Item > -): NormalizedFilterByConfig | false { - if ( field.filterBy === false ) { - return false; - } - - if ( typeof field.filterBy === 'object' ) { - let operators = field.filterBy.operators; - - // Assign default values if no operator was provided. - if ( ! operators || ! Array.isArray( operators ) ) { - operators = !! fieldTypeDefinition.filterBy - ? fieldTypeDefinition.filterBy.defaultOperators - : []; - } - - // Make sure only valid operators are included. - let validOperators = ALL_OPERATORS; - if ( typeof fieldTypeDefinition.filterBy === 'object' ) { - validOperators = fieldTypeDefinition.filterBy.validOperators; - } - operators = operators.filter( ( operator ) => - validOperators.includes( operator ) - ); - - // The `between` operator is not supported when elements are provided. - if ( hasElements( field ) && operators.includes( OPERATOR_BETWEEN ) ) { - operators = operators.filter( - ( operator ) => operator !== OPERATOR_BETWEEN - ); - } - - // Do not allow mixing single & multiselection operators. - // Remove multiselection operators if any of the single selection ones is present. - const hasSingleSelectionOperator = operators.some( ( operator ) => - SINGLE_SELECTION_OPERATORS.includes( operator ) - ); - if ( hasSingleSelectionOperator ) { - operators = operators.filter( ( operator ) => - // The 'Between' operator is unique as it can be combined with single selection operators. - [ ...SINGLE_SELECTION_OPERATORS, OPERATOR_BETWEEN ].includes( - operator - ) - ); - } - - // If no operators are left at this point, - // the filters should be disabled. - if ( operators.length === 0 ) { - return false; - } - - return { - isPrimary: !! field.filterBy.isPrimary, - operators, - }; - } - - if ( fieldTypeDefinition.filterBy === false ) { - return false; - } - - let defaultOperators = fieldTypeDefinition.filterBy.defaultOperators; - // The `between` operator is not supported when elements are provided. - if ( - hasElements( field ) && - defaultOperators.includes( OPERATOR_BETWEEN ) - ) { - defaultOperators = defaultOperators.filter( - ( operator ) => operator !== OPERATOR_BETWEEN - ); - } - - return { - operators: defaultOperators, - }; -} - -/** - * Apply default values and normalize the fields config. - * - * @param fields Fields config. - * @return Normalized fields config. - */ -export default function normalizeFields< Item >( - fields: Field< Item >[] -): NormalizedField< Item >[] { - return fields.map( ( field ) => { - const fieldTypeDefinition = getFieldTypeDefinition< Item >( - field.type - ); - const getValue = field.getValue || getValueFromId( field.id ); - const setValue = field.setValue || setValueFromId( field.id ); - - const sort = - field.sort ?? - function sort( a, b, direction ) { - return fieldTypeDefinition.sort( - getValue( { item: a } ), - getValue( { item: b } ), - direction - ); - }; - - const isValid = { - ...fieldTypeDefinition.isValid, - ...field.isValid, - }; - - const Edit = getControl( field, fieldTypeDefinition ); - - const render = - field.render ?? - function render( { - item, - field: renderedField, - }: DataViewRenderFieldProps< Item > ) { - return ( - fieldTypeDefinition.render as FunctionComponent< - DataViewRenderFieldProps< Item > - > - )( { item, field: renderedField } ); - }; - - const filterBy = getFilterBy( field, fieldTypeDefinition ); - - /** - * 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, - setValue, - render, - sort, - isValid, - Edit, - hasElements: hasElements( field ), - enableHiding: field.enableHiding ?? true, - enableSorting: - field.enableSorting ?? - fieldTypeDefinition.enableSorting ?? - true, - filterBy, - readOnly: field.readOnly ?? fieldTypeDefinition.readOnly ?? false, - format: {}, - }; - - if ( field.type === 'date' ) { - const format = { - date: - field.format?.date !== undefined && - typeof field.format.date === 'string' - ? field.format.date - : getSettings().formats.date, - weekStartsOn: - field.format?.weekStartsOn !== undefined && - 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: {} }; - } ); -}