diff --git a/packages/dataviews/src/components/dataform/index.tsx b/packages/dataviews/src/components/dataform/index.tsx index 64196c685a9784..42a6766813975e 100644 --- a/packages/dataviews/src/components/dataform/index.tsx +++ b/packages/dataviews/src/components/dataform/index.tsx @@ -6,13 +6,16 @@ import type { Dispatch, SetStateAction } from 'react'; /** * WordPress dependencies */ -import { TextControl } from '@wordpress/components'; +import { + TextControl, + __experimentalNumberControl as NumberControl, +} from '@wordpress/components'; import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import type { Form, Field, NormalizedField } from '../../types'; +import type { Form, Field, NormalizedField, FieldType } from '../../types'; import { normalizeFields } from '../../normalize-fields'; type DataFormProps< Item > = { @@ -56,12 +59,41 @@ function DataFormTextControl< Item >( { ); } +function DataFormNumberControl< Item >( { + data, + field, + onChange, +}: DataFormControlProps< Item > ) { + const { id, label, description } = field; + const value = field.getValue( { item: data } ); + + const onChangeControl = useCallback( + ( newValue: string | undefined ) => + onChange( ( prevItem: Item ) => ( { + ...prevItem, + [ id ]: newValue, + } ) ), + [ id, onChange ] + ); + + return ( + + ); +} + const controls: { - [ key: string ]: < Item >( + [ key in FieldType ]: < Item >( props: DataFormControlProps< Item > ) => JSX.Element; } = { text: DataFormTextControl, + integer: DataFormNumberControl, }; function getControlForField< Item >( field: NormalizedField< Item > ) { diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 2e288c8e11d41c..a67eaa6b76f042 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -20,15 +20,21 @@ const fields = [ label: 'Title', type: 'text' as const, }, + { + id: 'order', + label: 'Order', + type: 'integer' as const, + }, ]; export const Default = () => { const [ post, setPost ] = useState( { title: 'Hello, World!', + order: 2, } ); const form = { - visibleFields: [ 'title' ], + visibleFields: [ 'title', 'order' ], }; return ( diff --git a/packages/dataviews/src/index.ts b/packages/dataviews/src/index.ts index 8b6e53e1ff7293..95a8ab4c2e5e8d 100644 --- a/packages/dataviews/src/index.ts +++ b/packages/dataviews/src/index.ts @@ -3,3 +3,4 @@ export { default as DataForm } from './components/dataform'; export { VIEW_LAYOUTS } from './layouts'; export { filterSortAndPaginate } from './filter-and-sort-data-view'; export type * from './types'; +export { isItemValid } from './validation'; diff --git a/packages/dataviews/src/test/validation.ts b/packages/dataviews/src/test/validation.ts new file mode 100644 index 00000000000000..d90d4744ac3272 --- /dev/null +++ b/packages/dataviews/src/test/validation.ts @@ -0,0 +1,63 @@ +/** + * Internal dependencies + */ +import { isItemValid } from '../validation'; +import type { Field } from '../types'; + +describe( 'validation', () => { + it( 'fields not visible in form are not validated', () => { + const item = { id: 1, valid_order: 2, invalid_order: 'd' }; + const fields: Field< {} >[] = [ + { + id: 'valid_order', + type: 'integer', + }, + { + id: 'invalid_order', + type: 'integer', + }, + ]; + const form = { visibleFields: [ 'valid_order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'integer field is valid if value is integer', () => { + const item = { id: 1, order: 2, title: 'hi' }; + const fields: Field< {} >[] = [ + { + type: 'integer', + id: 'order', + }, + ]; + const form = { visibleFields: [ 'order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( true ); + } ); + + it( 'integer field is invalid if value is not integer', () => { + const item = { id: 1, order: 'd' }; + const fields: Field< {} >[] = [ + { + id: 'order', + type: 'integer', + }, + ]; + const form = { visibleFields: [ 'order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); + + it( 'integer field is invalid if value is empty', () => { + const item = { id: 1, order: '' }; + const fields: Field< {} >[] = [ + { + id: 'order', + type: 'integer', + }, + ]; + const form = { visibleFields: [ 'order' ] }; + const result = isItemValid( item, fields, form ); + expect( result ).toBe( false ); + } ); +} ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 0b43740efc3f2d..37c3efbde5cfb0 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -44,7 +44,7 @@ export type Operator = export type ItemRecord = Record< string, unknown >; -export type FieldType = 'text'; +export type FieldType = 'text' | 'integer'; /** * A dataview field for a specific property of a data type. @@ -65,6 +65,11 @@ export type Field< Item > = { */ label?: string; + /** + * A description of the field. + */ + description?: string; + /** * Placeholder for the field. */ diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts new file mode 100644 index 00000000000000..5b20d094a41861 --- /dev/null +++ b/packages/dataviews/src/validation.ts @@ -0,0 +1,33 @@ +/** + * Internal dependencies + */ +import { normalizeFields } from './normalize-fields'; +import type { Field, Form } from './types'; + +export function isItemValid< Item >( + item: Item, + fields: Field< Item >[], + form: Form +): boolean { + const _fields = normalizeFields( + fields.filter( ( { id } ) => !! form.visibleFields?.includes( id ) ) + ); + return _fields.every( ( field ) => { + const value = field.getValue( { item } ); + + // TODO: this implicitely means the value is required. + if ( field.type === 'integer' && value === '' ) { + return false; + } + + if ( + field.type === 'integer' && + ! Number.isInteger( Number( value ) ) + ) { + return false; + } + + // Nothing to validate. + return true; + } ); +} diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 2a97e688eeedbe..190b8ea6ca32f5 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -11,14 +11,13 @@ import { store as noticesStore } from '@wordpress/notices'; import { useMemo, useState } from '@wordpress/element'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; import { parse } from '@wordpress/blocks'; -import { DataForm } from '@wordpress/dataviews'; +import { DataForm, isItemValid } from '@wordpress/dataviews'; import { Button, TextControl, __experimentalText as Text, __experimentalHStack as HStack, __experimentalVStack as VStack, - __experimentalNumberControl as NumberControl, } from '@wordpress/components'; /** @@ -39,21 +38,31 @@ import { getItemTitle } from '../../dataviews/actions/utils'; const { PATTERN_TYPES, CreatePatternModalContents, useDuplicatePatternProps } = unlock( patternsPrivateApis ); -// TODO: this should be shared with other components (page-pages). +// TODO: this should be shared with other components (see post-fields in edit-site). const fields = [ { type: 'text', - header: __( 'Title' ), id: 'title', + label: __( 'Title' ), placeholder: __( 'No title' ), getValue: ( { item } ) => item.title, }, + { + type: 'integer', + id: 'menu_order', + label: __( 'Order' ), + description: __( 'Determines the order of pages.' ), + }, ]; -const form = { +const formDuplicateAction = { visibleFields: [ 'title' ], }; +const formOrderAction = { + visibleFields: [ 'menu_order' ], +}; + /** * Check if a template is removable. * @@ -635,21 +644,20 @@ function useRenamePostAction( postType ) { } function ReorderModal( { items, closeModal, onActionPerformed } ) { - const [ item ] = items; + const [ item, setItem ] = useState( items[ 0 ] ); + const orderInput = item.menu_order; const { editEntityRecord, saveEditedEntityRecord } = useDispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); - const [ orderInput, setOrderInput ] = useState( item.menu_order ); async function onOrder( event ) { event.preventDefault(); - if ( - ! Number.isInteger( Number( orderInput ) ) || - orderInput?.trim?.() === '' - ) { + + if ( ! isItemValid( item, fields, formOrderAction ) ) { return; } + try { await editEntityRecord( 'postType', item.type, item.id, { menu_order: orderInput, @@ -673,9 +681,7 @@ function ReorderModal( { items, closeModal, onActionPerformed } ) { } ); } } - const saveIsDisabled = - ! Number.isInteger( Number( orderInput ) ) || - orderInput?.trim?.() === ''; + const isSaveDisabled = ! isItemValid( item, fields, formOrderAction ); return (
@@ -684,12 +690,11 @@ function ReorderModal( { items, closeModal, onActionPerformed } ) { 'Determines the order of pages. Pages with the same order value are sorted alphabetically. Negative order values are supported.' ) } -