Skip to content

Commit

Permalink
DataForm: migrate order action modal and introduce form validation (#…
Browse files Browse the repository at this point in the history
…63895)

Co-authored-by: oandregal <oandregal@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
  • Loading branch information
3 people authored Jul 26, 2024
1 parent 9f7e26f commit 654fef6
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 27 deletions.
38 changes: 35 additions & 3 deletions packages/dataviews/src/components/dataform/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 > = {
Expand Down Expand Up @@ -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 (
<NumberControl
label={ label }
help={ description }
value={ value }
onChange={ onChangeControl }
__next40pxDefaultSize
/>
);
}

const controls: {
[ key: string ]: < Item >(
[ key in FieldType ]: < Item >(
props: DataFormControlProps< Item >
) => JSX.Element;
} = {
text: DataFormTextControl,
integer: DataFormNumberControl,
};

function getControlForField< Item >( field: NormalizedField< Item > ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions packages/dataviews/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
63 changes: 63 additions & 0 deletions packages/dataviews/src/test/validation.ts
Original file line number Diff line number Diff line change
@@ -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 );
} );
} );
7 changes: 6 additions & 1 deletion packages/dataviews/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -65,6 +65,11 @@ export type Field< Item > = {
*/
label?: string;

/**
* A description of the field.
*/
description?: string;

/**
* Placeholder for the field.
*/
Expand Down
33 changes: 33 additions & 0 deletions packages/dataviews/src/validation.ts
Original file line number Diff line number Diff line change
@@ -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;
} );
}
49 changes: 27 additions & 22 deletions packages/editor/src/components/post-actions/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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.
*
Expand Down Expand Up @@ -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,
Expand All @@ -673,9 +681,7 @@ function ReorderModal( { items, closeModal, onActionPerformed } ) {
} );
}
}
const saveIsDisabled =
! Number.isInteger( Number( orderInput ) ) ||
orderInput?.trim?.() === '';
const isSaveDisabled = ! isItemValid( item, fields, formOrderAction );
return (
<form onSubmit={ onOrder }>
<VStack spacing="5">
Expand All @@ -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.'
) }
</div>
<NumberControl
__next40pxDefaultSize
label={ __( 'Order' ) }
help={ __( 'Set the page order.' ) }
value={ orderInput }
onChange={ setOrderInput }
<DataForm
data={ item }
fields={ fields }
form={ formOrderAction }
onChange={ setItem }
/>
<HStack justify="right">
<Button
Expand All @@ -706,7 +711,7 @@ function ReorderModal( { items, closeModal, onActionPerformed } ) {
variant="primary"
type="submit"
accessibleWhenDisabled
disabled={ saveIsDisabled }
disabled={ isSaveDisabled }
__experimentalIsFocusable
>
{ __( 'Save' ) }
Expand Down Expand Up @@ -873,7 +878,7 @@ const useDuplicatePostAction = ( postType ) => {
<DataForm
data={ item }
fields={ fields }
form={ form }
form={ formDuplicateAction }
onChange={ setItem }
/>
<HStack spacing={ 2 } justify="end">
Expand Down

0 comments on commit 654fef6

Please sign in to comment.