From 3cb97b5592c8f1c834ef227ed975f52c48b56d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:36:13 +0200 Subject: [PATCH] DataForm: implement first prototype using duplicate page action (#63032) Co-authored-by: oandregal Co-authored-by: fabiankaegy Co-authored-by: youknowriad --- package-lock.json | 2 + packages/dataviews/CHANGELOG.md | 4 + packages/dataviews/src/dataform.tsx | 106 ++++++++++++++++++ packages/dataviews/src/index.ts | 1 + packages/dataviews/src/types.ts | 19 ++++ packages/editor/package.json | 1 + .../src/components/post-actions/actions.js | 51 ++++++--- packages/editor/src/private-apis.native.js | 2 - 8 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 packages/dataviews/src/dataform.tsx diff --git a/package-lock.json b/package-lock.json index 80317039b1f09f..c93815597de63c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54370,6 +54370,7 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", @@ -69493,6 +69494,7 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index a26c854fad8a75..be0b434483599a 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New features + +- Added a new `DataForm` component to render controls from a given configuration (fields, form), and data. + ## 2.2.0 (2024-06-26) ## 2.1.0 (2024-06-15) diff --git a/packages/dataviews/src/dataform.tsx b/packages/dataviews/src/dataform.tsx new file mode 100644 index 00000000000000..e96e0e13dc0517 --- /dev/null +++ b/packages/dataviews/src/dataform.tsx @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import type { Dispatch, SetStateAction } from 'react'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { TextControl } from '@wordpress/components'; +import { useCallback, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { Form, Field, NormalizedField } from './types'; +import { normalizeFields } from './normalize-fields'; + +type DataFormProps< Item > = { + data: Item; + fields: Field< Item >[]; + form: Form; + onChange: Dispatch< SetStateAction< Item > >; +}; + +type DataFormControlProps< Item > = { + data: Item; + field: NormalizedField< Item >; + onChange: Dispatch< SetStateAction< Item > >; +}; + +function DataFormTextControl< Item >( { + data, + field, + onChange, +}: DataFormControlProps< Item > ) { + const { id, header, placeholder } = field; + const value = field.getValue( { item: data } ); + + const onChangeControl = useCallback( + ( newValue: string ) => + onChange( ( prevItem: Item ) => ( { + ...prevItem, + [ id ]: newValue, + } ) ), + [ id, onChange ] + ); + + return ( + + ); +} + +const controls: { + [ key: string ]: < Item >( + props: DataFormControlProps< Item > + ) => JSX.Element; +} = { + text: DataFormTextControl, +}; + +function getControlForField< Item >( field: NormalizedField< Item > ) { + if ( ! field.type ) { + return null; + } + + if ( ! Object.keys( controls ).includes( field.type ) ) { + return null; + } + + return controls[ field.type ]; +} + +export default function DataForm< Item >( { + data, + fields, + form, + onChange, +}: DataFormProps< Item > ) { + const visibleFields = useMemo( + () => + normalizeFields( + fields.filter( + ( { id } ) => !! form.visibleFields?.includes( id ) + ) + ), + [ fields, form.visibleFields ] + ); + + return visibleFields.map( ( field ) => { + const DataFormControl = getControlForField( field ); + return DataFormControl ? ( + + ) : null; + } ); +} diff --git a/packages/dataviews/src/index.ts b/packages/dataviews/src/index.ts index 31f44e5ed97502..f4750bbbd2cd5d 100644 --- a/packages/dataviews/src/index.ts +++ b/packages/dataviews/src/index.ts @@ -2,3 +2,4 @@ export { default as DataViews } from './dataviews'; export { VIEW_LAYOUTS } from './layouts'; export { filterSortAndPaginate } from './filter-and-sort-data-view'; export type * from './types'; +export { default as DataForm } from './dataform'; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 964523c72f8a68..9e140f686d65f8 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -44,10 +44,17 @@ export type Operator = export type ItemRecord = Record< string, unknown >; +export type FieldType = 'text'; + /** * A dataview field for a specific property of a data type. */ export type Field< Item > = { + /** + * Type of the fields. + */ + type?: FieldType; + /** * The unique identifier of the field. */ @@ -58,6 +65,11 @@ export type Field< Item > = { */ header?: string; + /** + * Placeholder for the field. + */ + placeholder?: string; + /** * Callback used to render the field. Defaults to `field.getValue`. */ @@ -131,6 +143,13 @@ export type Fields< Item > = Field< Item >[]; export type Data< Item > = Item[]; +/** + * The form configuration. + */ +export type Form = { + visibleFields?: string[]; +}; + /** * The filters applied to the dataset. */ diff --git a/packages/editor/package.json b/packages/editor/package.json index 932be1a1799fc1..bdda258d1453fd 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -42,6 +42,7 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 535e474aafff1e..e38c64ddb3d7e6 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -11,7 +11,7 @@ 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 { Button, TextControl, @@ -39,6 +39,21 @@ import { CreateTemplatePartModalContents } from '../create-template-part-modal'; const { PATTERN_TYPES, CreatePatternModalContents, useDuplicatePatternProps } = unlock( patternsPrivateApis ); +// TODO: this should be shared with other components (page-pages). +const fields = [ + { + type: 'text', + header: __( 'Title' ), + id: 'title', + placeholder: __( 'No title' ), + getValue: ( { item } ) => item.title, + }, +]; + +const form = { + visibleFields: [ 'title' ], +}; + /** * Check if a template is removable. * @@ -649,16 +664,17 @@ const useDuplicatePostAction = ( postType ) => { return status !== 'trash'; }, RenderModal: ( { items, closeModal, onActionPerformed } ) => { - const [ item ] = items; + const [ item, setItem ] = useState( { + ...items[ 0 ], + title: sprintf( + /* translators: %s: Existing template title */ + __( '%s (Copy)' ), + getItemTitle( items[ 0 ] ) + ), + } ); + const [ isCreatingPage, setIsCreatingPage ] = useState( false ); - const [ title, setTitle ] = useState( - sprintf( - /* translators: %s: Existing item title */ - __( '%s (Copy)' ), - getItemTitle( item ) - ) - ); const { saveEntityRecord } = useDispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = @@ -673,8 +689,8 @@ const useDuplicatePostAction = ( postType ) => { const newItemOject = { status: 'draft', - title, - slug: title || __( 'No title' ), + title: item.title, + slug: item.title || __( 'No title' ), comment_status: item.comment_status, content: typeof item.content === 'string' @@ -725,7 +741,7 @@ const useDuplicatePostAction = ( postType ) => { // translators: %s: Title of the created template e.g: "Category". __( '"%s" successfully created.' ), decodeEntities( - newItem.title?.rendered || title + newItem.title?.rendered || item.title ) ), { @@ -753,14 +769,15 @@ const useDuplicatePostAction = ( postType ) => { closeModal(); } } + return (
-