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 (