From 96f6338034682929ff4fc79ec5b1225e258678e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:23:54 +0100 Subject: [PATCH] DataViews: allow register/unregister fields (#67175) Co-authored-by: oandregal Co-authored-by: youknowriad Co-authored-by: ntsekouras Co-authored-by: louwie17 --- packages/editor/README.md | 24 +++++++ .../src/components/post-actions/actions.js | 6 +- .../src/components/post-fields/index.ts | 71 +++++++++++-------- packages/editor/src/dataviews/api.js | 41 +++++++++++ .../src/dataviews/store/private-actions.ts | 60 +++++++++++++++- .../src/dataviews/store/private-selectors.ts | 4 ++ .../editor/src/dataviews/store/reducer.ts | 39 +++++++++- .../editor/src/store/private-selectors.js | 5 ++ 8 files changed, 213 insertions(+), 37 deletions(-) diff --git a/packages/editor/README.md b/packages/editor/README.md index 07405d0d51c3d2..bc00e15c8fb892 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/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index e1c0ed1558193d..8dbe5b9dfcd5ad 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 3d675ab763d64c..41b61fe103a70f 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 { registerPostTypeSchema } = unlock( useDispatch( editorStore ) ); + useEffect( () => { + registerPostTypeSchema( postType ); + }, [ registerPostTypeSchema, 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 { diff --git a/packages/editor/src/dataviews/api.js b/packages/editor/src/dataviews/api.js index 130a69bba754c7..e03b9ef35ac758 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 ); + } +} diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 10f2b9ce872d5a..77ac131a8e2302 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, @@ -61,7 +96,7 @@ export function setIsReady( kind: string, name: string ) { }; } -export const registerPostTypeActions = +export const registerPostTypeSchema = ( postType: string ) => async ( { registry }: { registry: any } ) => { const isReady = unlock( registry.select( editorStore ) ).isEntityReady( @@ -124,6 +159,18 @@ export const registerPostTypeActions = permanentlyDeletePost, ]; + const fields = [ + featuredImageField, + titleField, + authorField, + statusField, + dateField, + slugField, + parentField, + commentStatusField, + passwordField, + ]; + registry.batch( () => { actions.forEach( ( action ) => { if ( ! action ) { @@ -135,7 +182,14 @@ export const registerPostTypeActions = action ); } ); + fields.forEach( ( field ) => { + unlock( registry.dispatch( editorStore ) ).registerEntityField( + 'postType', + postType, + field + ); + } ); } ); - doAction( 'core.registerPostTypeActions', 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 117c5b30966a39..e1daeb4032fc21 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 9124b74f02860a..94d7f2e6c4f190 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 9bc065436c10bb..9af0512c19dbdd 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.