From 3012632c74da9c0cc79599fbe11d549021855246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:07:52 +0100 Subject: [PATCH 1/5] Implement store: reducer, actions, selectors --- .../src/dataviews/store/private-actions.ts | 68 ++++++++++++++++++- .../src/dataviews/store/private-selectors.ts | 4 ++ .../editor/src/dataviews/store/reducer.ts | 39 ++++++++++- .../editor/src/store/private-selectors.js | 5 ++ 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 10f2b9ce872d5..0db70c8a0e6e7 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -2,7 +2,7 @@ * WordPress dependencies */ import { store as coreStore } from '@wordpress/core-data'; -import type { Action } from '@wordpress/dataviews'; +import type { Action, Field } from '@wordpress/dataviews'; import { doAction } from '@wordpress/hooks'; /** @@ -24,6 +24,15 @@ import { renamePost, resetPost, deletePost, + featuredImageField, + dateField, + parentField, + passwordField, + commentStatusField, + slugField, + statusField, + authorField, + titleField, } from '@wordpress/fields'; import duplicateTemplatePart from '../actions/duplicate-template-part'; @@ -53,6 +62,32 @@ export function unregisterEntityAction( }; } +export function registerEntityField< Item >( + kind: string, + name: string, + config: Field< Item > +) { + return { + type: 'REGISTER_ENTITY_FIELD' as const, + kind, + name, + config, + }; +} + +export function unregisterEntityField( + kind: string, + name: string, + fieldId: string +) { + return { + type: 'UNREGISTER_ENTITY_FIELD' as const, + kind, + name, + fieldId, + }; +} + export function setIsReady( kind: string, name: string ) { return { type: 'SET_IS_READY' as const, @@ -139,3 +174,34 @@ export const registerPostTypeActions = doAction( 'core.registerPostTypeActions', postType ); }; + +export const registerPostTypeFields = + ( postType: string ) => + async ( { registry }: { registry: any } ) => { + // TODO: do not register fields if there were already registered. + // Consider the existing isReady state. + + const fields = [ + featuredImageField, + titleField, + authorField, + statusField, + dateField, + slugField, + parentField, + commentStatusField, + passwordField, + ]; + + registry.batch( () => { + fields.forEach( ( field ) => { + unlock( registry.dispatch( editorStore ) ).registerEntityField( + 'postType', + postType, + field + ); + } ); + } ); + + doAction( 'core.registerPostTypeFields', postType ); + }; diff --git a/packages/editor/src/dataviews/store/private-selectors.ts b/packages/editor/src/dataviews/store/private-selectors.ts index 117c5b30966a3..e1daeb4032fc2 100644 --- a/packages/editor/src/dataviews/store/private-selectors.ts +++ b/packages/editor/src/dataviews/store/private-selectors.ts @@ -9,6 +9,10 @@ export function getEntityActions( state: State, kind: string, name: string ) { return state.actions[ kind ]?.[ name ] ?? EMPTY_ARRAY; } +export function getEntityFields( state: State, kind: string, name: string ) { + return state.fields[ kind ]?.[ name ] ?? EMPTY_ARRAY; +} + export function isEntityReady( state: State, kind: string, name: string ) { return state.isReady[ kind ]?.[ name ]; } diff --git a/packages/editor/src/dataviews/store/reducer.ts b/packages/editor/src/dataviews/store/reducer.ts index 9124b74f02860..94d7f2e6c4f19 100644 --- a/packages/editor/src/dataviews/store/reducer.ts +++ b/packages/editor/src/dataviews/store/reducer.ts @@ -2,17 +2,21 @@ * WordPress dependencies */ import { combineReducers } from '@wordpress/data'; -import type { Action } from '@wordpress/dataviews'; +import type { Action, Field } from '@wordpress/dataviews'; type ReduxAction = | ReturnType< typeof import('./private-actions').registerEntityAction > | ReturnType< typeof import('./private-actions').unregisterEntityAction > + | ReturnType< typeof import('./private-actions').registerEntityField > + | ReturnType< typeof import('./private-actions').unregisterEntityField > | ReturnType< typeof import('./private-actions').setIsReady >; export type ActionState = Record< string, Record< string, Action< any >[] > >; +export type FieldsState = Record< string, Record< string, Field< any >[] > >; export type ReadyState = Record< string, Record< string, boolean > >; export type State = { actions: ActionState; + fields: FieldsState; isReady: ReadyState; }; @@ -64,7 +68,40 @@ function actions( state: ActionState = {}, action: ReduxAction ) { return state; } +function fields( state: FieldsState = {}, action: ReduxAction ) { + switch ( action.type ) { + case 'REGISTER_ENTITY_FIELD': + return { + ...state, + [ action.kind ]: { + ...state[ action.kind ], + [ action.name ]: [ + ...( + state[ action.kind ]?.[ action.name ] ?? [] + ).filter( + ( _field ) => _field.id !== action.config.id + ), + action.config, + ], + }, + }; + case 'UNREGISTER_ENTITY_FIELD': + return { + ...state, + [ action.kind ]: { + ...state[ action.kind ], + [ action.name ]: ( + state[ action.kind ]?.[ action.name ] ?? [] + ).filter( ( _field ) => _field.id !== action.fieldId ), + }, + }; + } + + return state; +} + export default combineReducers( { actions, + fields, isReady, } ); diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index 9bc065436c10b..9af0512c19dbd 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -27,6 +27,7 @@ import { } from './selectors'; import { getEntityActions as _getEntityActions, + getEntityFields as _getEntityFields, isEntityReady as _isEntityReady, } from '../dataviews/store/private-selectors'; @@ -171,6 +172,10 @@ export function isEntityReady( state, ...args ) { return _isEntityReady( state.dataviews, ...args ); } +export function getEntityFields( state, ...args ) { + return _getEntityFields( state.dataviews, ...args ); +} + /** * Similar to getBlocksByName in @wordpress/block-editor, but only returns the top-most * blocks that aren't descendants of the query block. From 74436150810ae1d1adb73a41cd27780722d0aadc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:13:15 +0100 Subject: [PATCH 2/5] Implement API --- packages/editor/README.md | 24 ++++++++++++++++ packages/editor/src/dataviews/api.js | 41 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/editor/README.md b/packages/editor/README.md index 07405d0d51c3d..bc00e15c8fb89 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -1602,6 +1602,18 @@ _Parameters_ - _name_ `string`: Entity name. - _config_ `Action`: Action configuration. +### registerEntityField + +Registers a new DataViews field. + +This is an experimental API and is subject to change. it's only available in the Gutenberg plugin for now. + +_Parameters_ + +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _config_ `Field`: Field configuration. + ### RichText > **Deprecated** since 5.3, use `wp.blockEditor.RichText` instead. @@ -1697,6 +1709,18 @@ _Parameters_ - _name_ `string`: Entity name. - _actionId_ `string`: Action ID. +### unregisterEntityField + +Unregisters a DataViews field. + +This is an experimental API and is subject to change. it's only available in the Gutenberg plugin for now. + +_Parameters_ + +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _fieldId_ `string`: Field ID. + ### UnsavedChangesWarning Warns the user if there are unsaved changes before leaving the editor. Compatible with Post Editor and Site Editor. diff --git a/packages/editor/src/dataviews/api.js b/packages/editor/src/dataviews/api.js index 130a69bba754c..e03b9ef35ac75 100644 --- a/packages/editor/src/dataviews/api.js +++ b/packages/editor/src/dataviews/api.js @@ -11,6 +11,7 @@ import { store as editorStore } from '../store'; /** * @typedef {import('@wordpress/dataviews').Action} Action + * @typedef {import('@wordpress/dataviews').Field} Field */ /** @@ -53,3 +54,43 @@ export function unregisterEntityAction( kind, name, actionId ) { _unregisterEntityAction( kind, name, actionId ); } } + +/** + * Registers a new DataViews field. + * + * This is an experimental API and is subject to change. + * it's only available in the Gutenberg plugin for now. + * + * @param {string} kind Entity kind. + * @param {string} name Entity name. + * @param {Field} config Field configuration. + */ +export function registerEntityField( kind, name, config ) { + const { registerEntityField: _registerEntityField } = unlock( + dispatch( editorStore ) + ); + + if ( globalThis.IS_GUTENBERG_PLUGIN ) { + _registerEntityField( kind, name, config ); + } +} + +/** + * Unregisters a DataViews field. + * + * This is an experimental API and is subject to change. + * it's only available in the Gutenberg plugin for now. + * + * @param {string} kind Entity kind. + * @param {string} name Entity name. + * @param {string} fieldId Field ID. + */ +export function unregisterEntityField( kind, name, fieldId ) { + const { unregisterEntityField: _unregisterEntityField } = unlock( + dispatch( editorStore ) + ); + + if ( globalThis.IS_GUTENBERG_PLUGIN ) { + _unregisterEntityField( kind, name, fieldId ); + } +} From 8649f93b3596c5efe49a5d41f375217d35e2ec28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:53:56 +0100 Subject: [PATCH 3/5] usePostFields: take them from the registry --- .../src/components/post-fields/index.ts | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/packages/editor/src/components/post-fields/index.ts b/packages/editor/src/components/post-fields/index.ts index 3d675ab763d64..e93f167f37e03 100644 --- a/packages/editor/src/components/post-fields/index.ts +++ b/packages/editor/src/components/post-fields/index.ts @@ -1,22 +1,18 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; +import { useEffect, useMemo } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; +import { useDispatch, useSelect } from '@wordpress/data'; import type { Field } from '@wordpress/dataviews'; -import { - featuredImageField, - slugField, - parentField, - passwordField, - statusField, - commentStatusField, - titleField, - dateField, - authorField, -} from '@wordpress/fields'; import type { BasePostWithEmbeddedAuthor } from '@wordpress/fields'; +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; + interface UsePostFieldsReturn { isLoading: boolean; fields: Field< BasePostWithEmbeddedAuthor >[]; @@ -28,29 +24,44 @@ interface Author { } function usePostFields(): UsePostFieldsReturn { + const postType = 'page'; // TODO: this could be page or post (experimental). + + const { registerPostTypeFields } = unlock( useDispatch( editorStore ) ); + useEffect( () => { + registerPostTypeFields( postType ); + }, [ registerPostTypeFields, postType ] ); + + const { defaultFields } = useSelect( + ( select ) => { + const { getEntityFields } = unlock( select( editorStore ) ); + return { + defaultFields: getEntityFields( 'postType', postType ), + }; + }, + [ postType ] + ); + const { records: authors, isResolving: isLoadingAuthors } = useEntityRecords< Author >( 'root', 'user', { per_page: -1 } ); const fields = useMemo( () => - [ - featuredImageField, - titleField, - { - ...authorField, - elements: authors?.map( ( { id, name } ) => ( { - value: id, - label: name, - } ) ), - }, - statusField, - dateField, - slugField, - parentField, - commentStatusField, - passwordField, - ] as Field< BasePostWithEmbeddedAuthor >[], - [ authors ] + defaultFields.map( + ( field: Field< BasePostWithEmbeddedAuthor > ) => { + if ( field.id === 'author' ) { + return { + ...field, + elements: authors?.map( ( { id, name } ) => ( { + value: id, + label: name, + } ) ), + }; + } + + return field; + } + ) as Field< BasePostWithEmbeddedAuthor >[], + [ authors, defaultFields ] ); return { From df078fd48f0290a66593d18ac2234b7ea320824a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:12:46 +0100 Subject: [PATCH 4/5] isReady: consider actions or fields --- .../src/dataviews/store/private-actions.ts | 25 +++++++++++++++---- .../src/dataviews/store/private-selectors.ts | 9 +++++-- .../editor/src/dataviews/store/reducer.ts | 10 ++++++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 0db70c8a0e6e7..682a0c29b28e0 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -88,11 +88,12 @@ export function unregisterEntityField( }; } -export function setIsReady( kind: string, name: string ) { +export function setIsReady( kind: string, name: string, part: string ) { return { type: 'SET_IS_READY' as const, kind, name, + part, }; } @@ -101,7 +102,8 @@ export const registerPostTypeActions = async ( { registry }: { registry: any } ) => { const isReady = unlock( registry.select( editorStore ) ).isEntityReady( 'postType', - postType + postType, + 'actions' ); if ( isReady ) { return; @@ -109,7 +111,8 @@ export const registerPostTypeActions = unlock( registry.dispatch( editorStore ) ).setIsReady( 'postType', - postType + postType, + 'actions' ); const postTypeConfig = ( await registry @@ -178,8 +181,20 @@ export const registerPostTypeActions = export const registerPostTypeFields = ( postType: string ) => async ( { registry }: { registry: any } ) => { - // TODO: do not register fields if there were already registered. - // Consider the existing isReady state. + const isReady = unlock( registry.select( editorStore ) ).isEntityReady( + 'postType', + postType, + 'fields' + ); + if ( isReady ) { + return; + } + + unlock( registry.dispatch( editorStore ) ).setIsReady( + 'postType', + postType, + 'fields' + ); const fields = [ featuredImageField, diff --git a/packages/editor/src/dataviews/store/private-selectors.ts b/packages/editor/src/dataviews/store/private-selectors.ts index e1daeb4032fc2..a683f1fbcc56a 100644 --- a/packages/editor/src/dataviews/store/private-selectors.ts +++ b/packages/editor/src/dataviews/store/private-selectors.ts @@ -13,6 +13,11 @@ export function getEntityFields( state: State, kind: string, name: string ) { return state.fields[ kind ]?.[ name ] ?? EMPTY_ARRAY; } -export function isEntityReady( state: State, kind: string, name: string ) { - return state.isReady[ kind ]?.[ name ]; +export function isEntityReady( + state: State, + kind: string, + name: string, + part: string +) { + return state.isReady[ kind ]?.[ name ]?.[ part ]; } diff --git a/packages/editor/src/dataviews/store/reducer.ts b/packages/editor/src/dataviews/store/reducer.ts index 94d7f2e6c4f19..74a0bd9156439 100644 --- a/packages/editor/src/dataviews/store/reducer.ts +++ b/packages/editor/src/dataviews/store/reducer.ts @@ -13,7 +13,10 @@ type ReduxAction = export type ActionState = Record< string, Record< string, Action< any >[] > >; export type FieldsState = Record< string, Record< string, Field< any >[] > >; -export type ReadyState = Record< string, Record< string, boolean > >; +export type ReadyState = Record< + string, + Record< string, Record< string, boolean > > +>; export type State = { actions: ActionState; fields: FieldsState; @@ -27,7 +30,10 @@ function isReady( state: ReadyState = {}, action: ReduxAction ) { ...state, [ action.kind ]: { ...state[ action.kind ], - [ action.name ]: true, + [ action.name ]: { + ...( state[ action.kind ]?.[ action.name ] ?? {} ), + [ action.part ]: true, + }, }, }; } From 3e6782ea6e469534767e8e5da641fd41698cae2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:23:03 +0100 Subject: [PATCH 5/5] Consolidate registerPostType{Actions|Fields} into registerPostTypeSchema --- .../src/components/post-actions/actions.js | 6 +- .../src/components/post-fields/index.ts | 6 +- .../src/dataviews/store/private-actions.ts | 57 +++++-------------- .../src/dataviews/store/private-selectors.ts | 9 +-- .../editor/src/dataviews/store/reducer.ts | 10 +--- 5 files changed, 25 insertions(+), 63 deletions(-) diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index e1c0ed1558193..8dbe5b9dfcd5a 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -21,10 +21,10 @@ export function usePostActions( { postType, onActionPerformed, context } ) { [ postType ] ); - const { registerPostTypeActions } = unlock( useDispatch( editorStore ) ); + const { registerPostTypeSchema } = unlock( useDispatch( editorStore ) ); useEffect( () => { - registerPostTypeActions( postType ); - }, [ registerPostTypeActions, postType ] ); + registerPostTypeSchema( postType ); + }, [ registerPostTypeSchema, postType ] ); return useMemo( () => { // Filter actions based on provided context. If not provided diff --git a/packages/editor/src/components/post-fields/index.ts b/packages/editor/src/components/post-fields/index.ts index e93f167f37e03..41b61fe103a70 100644 --- a/packages/editor/src/components/post-fields/index.ts +++ b/packages/editor/src/components/post-fields/index.ts @@ -26,10 +26,10 @@ interface Author { function usePostFields(): UsePostFieldsReturn { const postType = 'page'; // TODO: this could be page or post (experimental). - const { registerPostTypeFields } = unlock( useDispatch( editorStore ) ); + const { registerPostTypeSchema } = unlock( useDispatch( editorStore ) ); useEffect( () => { - registerPostTypeFields( postType ); - }, [ registerPostTypeFields, postType ] ); + registerPostTypeSchema( postType ); + }, [ registerPostTypeSchema, postType ] ); const { defaultFields } = useSelect( ( select ) => { diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 682a0c29b28e0..77ac131a8e230 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -88,22 +88,20 @@ export function unregisterEntityField( }; } -export function setIsReady( kind: string, name: string, part: string ) { +export function setIsReady( kind: string, name: string ) { return { type: 'SET_IS_READY' as const, kind, name, - part, }; } -export const registerPostTypeActions = +export const registerPostTypeSchema = ( postType: string ) => async ( { registry }: { registry: any } ) => { const isReady = unlock( registry.select( editorStore ) ).isEntityReady( 'postType', - postType, - 'actions' + postType ); if ( isReady ) { return; @@ -111,8 +109,7 @@ export const registerPostTypeActions = unlock( registry.dispatch( editorStore ) ).setIsReady( 'postType', - postType, - 'actions' + postType ); const postTypeConfig = ( await registry @@ -162,40 +159,6 @@ export const registerPostTypeActions = permanentlyDeletePost, ]; - registry.batch( () => { - actions.forEach( ( action ) => { - if ( ! action ) { - return; - } - unlock( registry.dispatch( editorStore ) ).registerEntityAction( - 'postType', - postType, - action - ); - } ); - } ); - - doAction( 'core.registerPostTypeActions', postType ); - }; - -export const registerPostTypeFields = - ( postType: string ) => - async ( { registry }: { registry: any } ) => { - const isReady = unlock( registry.select( editorStore ) ).isEntityReady( - 'postType', - postType, - 'fields' - ); - if ( isReady ) { - return; - } - - unlock( registry.dispatch( editorStore ) ).setIsReady( - 'postType', - postType, - 'fields' - ); - const fields = [ featuredImageField, titleField, @@ -209,6 +172,16 @@ export const registerPostTypeFields = ]; registry.batch( () => { + actions.forEach( ( action ) => { + if ( ! action ) { + return; + } + unlock( registry.dispatch( editorStore ) ).registerEntityAction( + 'postType', + postType, + action + ); + } ); fields.forEach( ( field ) => { unlock( registry.dispatch( editorStore ) ).registerEntityField( 'postType', @@ -218,5 +191,5 @@ export const registerPostTypeFields = } ); } ); - doAction( 'core.registerPostTypeFields', postType ); + doAction( 'core.registerPostTypeSchema', postType ); }; diff --git a/packages/editor/src/dataviews/store/private-selectors.ts b/packages/editor/src/dataviews/store/private-selectors.ts index a683f1fbcc56a..e1daeb4032fc2 100644 --- a/packages/editor/src/dataviews/store/private-selectors.ts +++ b/packages/editor/src/dataviews/store/private-selectors.ts @@ -13,11 +13,6 @@ export function getEntityFields( state: State, kind: string, name: string ) { return state.fields[ kind ]?.[ name ] ?? EMPTY_ARRAY; } -export function isEntityReady( - state: State, - kind: string, - name: string, - part: string -) { - return state.isReady[ kind ]?.[ name ]?.[ part ]; +export function isEntityReady( state: State, kind: string, name: string ) { + return state.isReady[ kind ]?.[ name ]; } diff --git a/packages/editor/src/dataviews/store/reducer.ts b/packages/editor/src/dataviews/store/reducer.ts index 74a0bd9156439..94d7f2e6c4f19 100644 --- a/packages/editor/src/dataviews/store/reducer.ts +++ b/packages/editor/src/dataviews/store/reducer.ts @@ -13,10 +13,7 @@ type ReduxAction = export type ActionState = Record< string, Record< string, Action< any >[] > >; export type FieldsState = Record< string, Record< string, Field< any >[] > >; -export type ReadyState = Record< - string, - Record< string, Record< string, boolean > > ->; +export type ReadyState = Record< string, Record< string, boolean > >; export type State = { actions: ActionState; fields: FieldsState; @@ -30,10 +27,7 @@ function isReady( state: ReadyState = {}, action: ReduxAction ) { ...state, [ action.kind ]: { ...state[ action.kind ], - [ action.name ]: { - ...( state[ action.kind ]?.[ action.name ] ?? {} ), - [ action.part ]: true, - }, + [ action.name ]: true, }, }; }