Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataViews: allow register/unregister fields #67175

Merged
merged 5 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
71 changes: 41 additions & 30 deletions packages/editor/src/components/post-fields/index.ts
Original file line number Diff line number Diff line change
@@ -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 >[];
Expand All @@ -28,29 +24,44 @@ interface Author {
}

function usePostFields(): UsePostFieldsReturn {
const postType = 'page'; // TODO: this could be page or post (experimental).
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have two post types that work with the same fields: the Pages page pulls data for the page post type, and the experimental post screen (enable in "Gutenberg > Experiments > Redesigned post dashboard") does the same with posts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While they have the same fields in dataviews, this is how we display them in the post editor:

Pages Posts
Screenshot 2024-11-20 at 18 34 47 Screenshot 2024-11-20 at 18 34 32

It sounds a follow-up to this PR should be implementing the format field for the post post type, and add the postType parameter to the usePostFields hook.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if ultimately these hooks should received kind/name just like the registration function. I think we'll have this use-case pretty quickly when we start working on the tags/categories dataviews.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, we'd have:

  • useFields( 'postType', 'pages' ) instead of usePostFields( 'page' )
  • useFields( 'taxonomy', 'category' ) instead of useTaxonomyFields( 'category' )

Under the hood and conceptually, fields and actions are already connected to core-data. Making the kind an argument could work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we would also use this for taxonomies would it make sense to move these actions/selectors to the core data store instead? Given this is more entity specific.
Or in general do we expect every place that uses DataViews/DataForms within WordPress to also import the @wordpress/editor package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversation is relevant: plan is to move them to @wordpress/fields package when it becomes stable.


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 {
Expand Down
41 changes: 41 additions & 0 deletions packages/editor/src/dataviews/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { store as editorStore } from '../store';

/**
* @typedef {import('@wordpress/dataviews').Action} Action
* @typedef {import('@wordpress/dataviews').Field} Field
*/

/**
Expand Down Expand Up @@ -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 );
}
}
89 changes: 85 additions & 4 deletions packages/editor/src/dataviews/store/private-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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';

Expand Down Expand Up @@ -53,11 +62,38 @@ export function unregisterEntityAction(
};
}

export function setIsReady( kind: string, name: string ) {
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, part: string ) {
return {
type: 'SET_IS_READY' as const,
kind,
name,
part,
};
}

Expand All @@ -66,15 +102,17 @@ export const registerPostTypeActions =
async ( { registry }: { registry: any } ) => {
const isReady = unlock( registry.select( editorStore ) ).isEntityReady(
'postType',
postType
postType,
'actions'
);
if ( isReady ) {
return;
}

unlock( registry.dispatch( editorStore ) ).setIsReady(
'postType',
postType
postType,
'actions'
);

const postTypeConfig = ( await registry
Expand Down Expand Up @@ -139,3 +177,46 @@ export const registerPostTypeActions =

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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we avoid this argument and just have a global registration function for entities that register both actions and fields?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed registerPostTypeActions to registerPostTypeSchema and consolidated everything there 3e6782e

);

const fields = [
featuredImageField,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need the post type config to add some fields conditionally? For example featured image is the post type supports it, etc..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! But I don't want to do it in this PR, I'd rather work on small steps that land fast :)

There's a few other things we need to do: for example, somewhere we need to keep a list of post types <=> fields (see related conversation), but also a bit of validation (return early if post type is unknown), etc..

Would that work for you?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tracked this follow-up in the overview issue #61084

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 );
};
13 changes: 11 additions & 2 deletions packages/editor/src/dataviews/store/private-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ export function getEntityActions( state: State, kind: string, name: string ) {
return state.actions[ kind ]?.[ name ] ?? EMPTY_ARRAY;
}

export function isEntityReady( state: State, kind: string, name: string ) {
return state.isReady[ kind ]?.[ name ];
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 ];
}
Loading
Loading