diff --git a/packages/core-data/src/dynamic-entities.ts b/packages/core-data/src/dynamic-entities.ts new file mode 100644 index 0000000000000..51b579cb0cfcf --- /dev/null +++ b/packages/core-data/src/dynamic-entities.ts @@ -0,0 +1,111 @@ +/** + * Internal dependencies + */ +import type { GetRecordsHttpQuery, State } from './selectors'; +import type * as ET from './entity-types'; + +export type WPEntityTypes< C extends ET.Context = 'edit' > = { + Comment: ET.Comment< C >; + GlobalStyles: ET.GlobalStylesRevision< C >; + Media: ET.Attachment< C >; + Menu: ET.NavMenu< C >; + MenuItem: ET.NavMenuItem< C >; + MenuLocation: ET.MenuLocation< C >; + Plugin: ET.Plugin< C >; + PostType: ET.Type< C >; + Revision: ET.PostRevision< C >; + Sidebar: ET.Sidebar< C >; + Site: ET.Settings< C >; + Status: ET.PostStatusObject< C >; + Taxonomy: ET.Taxonomy< C >; + Theme: ET.Theme< C >; + UnstableBase: ET.UnstableBase< C >; + User: ET.User< C >; + Widget: ET.Widget< C >; + WidgetType: ET.WidgetType< C >; +}; + +/** + * A simple utility that pluralizes a string. + * Converts: + * - "post" to "posts" + * - "taxonomy" to "taxonomies" + * - "media" to "mediaItems" + * - "status" to "statuses" + * + * It does not pluralize "GlobalStyles" due to lack of clarity about it at time of writing. + */ +type PluralizeEntity< T extends string > = T extends 'GlobalStyles' + ? never + : T extends 'Media' + ? 'MediaItems' + : T extends 'Status' + ? 'Statuses' + : T extends `${ infer U }y` + ? `${ U }ies` + : `${ T }s`; + +/** + * A simple utility that singularizes a string. + * + * Converts: + * - "posts" to "post" + * - "taxonomies" to "taxonomy" + * - "mediaItems" to "media" + * - "statuses" to "status" + */ +type SingularizeEntity< T extends string > = T extends 'MediaItems' + ? 'Media' + : T extends 'Statuses' + ? 'Status' + : T extends `${ infer U }ies` + ? `${ U }y` + : T extends `${ infer U }s` + ? U + : T; + +export type SingularGetters = { + [ Key in `get${ keyof WPEntityTypes }` ]: ( + state: State, + id: number | string, + query?: GetRecordsHttpQuery + ) => WPEntityTypes[ Key extends `get${ infer E }` ? E : never ] | undefined; +}; + +export type PluralGetters = { + [ Key in `get${ PluralizeEntity< keyof WPEntityTypes > }` ]: ( + state: State, + query?: GetRecordsHttpQuery + ) => Array< + WPEntityTypes[ Key extends `get${ infer E }` + ? SingularizeEntity< E > + : never ] + > | null; +}; + +type ActionOptions = { + throwOnError?: boolean; +}; + +type DeleteRecordsHttpQuery = Record< string, any >; + +export type SaveActions = { + [ Key in `save${ keyof WPEntityTypes }` ]: ( + data: Partial< + WPEntityTypes[ Key extends `save${ infer E }` ? E : never ] + >, + options?: ActionOptions + ) => Promise< void >; +}; + +export type DeleteActions = { + [ Key in `delete${ keyof WPEntityTypes }` ]: ( + id: number | string, + query?: DeleteRecordsHttpQuery, + options?: ActionOptions + ) => Promise< void >; +}; + +export let dynamicActions: SaveActions & DeleteActions; + +export let dynamicSelectors: SingularGetters & PluralGetters; diff --git a/packages/core-data/src/entity-types/base.ts b/packages/core-data/src/entity-types/base.ts new file mode 100644 index 0000000000000..79a3039ad140d --- /dev/null +++ b/packages/core-data/src/entity-types/base.ts @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +import type { Context, OmitNevers } from './helpers'; +import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; + +export type TemplatePartArea = { + area: string; + label: string; + icon: string; + description: string; +}; + +export type TemplateType = { + title: string; + description: string; + slug: string; +}; + +declare module './base-entity-records' { + export namespace BaseEntityRecords { + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + export interface Base< C extends Context > { + /** + * Site description. + */ + description: string; + + /** + * GMT offset for the site. + */ + gmt_offset: string; + + /** + * Home URL. + */ + home: string; + + /** + * Site title + */ + name: string; + + /** + * Site icon ID. + */ + site_icon?: number; + + /** + * Site icon URL. + */ + site_icon_url: string; + + /** + * Site logo ID. + */ + site_logo?: number; + + /** + * Site timezone string. + */ + timezone_string: string; + + /** + * Site URL. + */ + url: string; + + /** + * Default template part areas. + */ + default_template_part_areas?: Array< TemplatePartArea >; + + /** + * Default template types + */ + default_template_types?: Array< TemplateType >; + } + } +} + +export type Base< C extends Context = 'edit' > = OmitNevers< + _BaseEntityRecords.Base< C > +>; diff --git a/packages/core-data/src/entity-types/index.ts b/packages/core-data/src/entity-types/index.ts index 0e601137cbcb6..68087a74005b2 100644 --- a/packages/core-data/src/entity-types/index.ts +++ b/packages/core-data/src/entity-types/index.ts @@ -3,6 +3,7 @@ */ import type { Context, Updatable } from './helpers'; import type { Attachment } from './attachment'; +import type { Base, TemplatePartArea, TemplateType } from './base'; import type { Comment } from './comment'; import type { GlobalStylesRevision } from './global-styles-revision'; import type { MenuLocation } from './menu-location'; @@ -11,6 +12,7 @@ import type { NavMenuItem } from './nav-menu-item'; import type { Page } from './page'; import type { Plugin } from './plugin'; import type { Post } from './post'; +import type { PostStatusObject } from './post-status'; import type { PostRevision } from './post-revision'; import type { Settings } from './settings'; import type { Sidebar } from './sidebar'; @@ -27,6 +29,7 @@ export type { BaseEntityRecords } from './base-entity-records'; export type { Attachment, + Base as UnstableBase, Comment, Context, GlobalStylesRevision, @@ -37,13 +40,16 @@ export type { Plugin, Post, PostRevision, + PostStatusObject, Settings, Sidebar, Taxonomy, + TemplatePartArea, + TemplateType, Theme, + Type, Updatable, User, - Type, Widget, WidgetType, WpTemplate, @@ -84,6 +90,7 @@ export type { */ export interface PerPackageEntityRecords< C extends Context > { core: + | Base< C > | Attachment< C > | Comment< C > | GlobalStylesRevision< C > @@ -93,6 +100,7 @@ export interface PerPackageEntityRecords< C extends Context > { | Page< C > | Plugin< C > | Post< C > + | PostStatusObject< C > | PostRevision< C > | Settings< C > | Sidebar< C > diff --git a/packages/core-data/src/entity-types/post-status.ts b/packages/core-data/src/entity-types/post-status.ts new file mode 100644 index 0000000000000..92360dfdc17a6 --- /dev/null +++ b/packages/core-data/src/entity-types/post-status.ts @@ -0,0 +1,56 @@ +/** + * Internal dependencies + */ +import type { Context, OmitNevers } from './helpers'; +import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; + +declare module './base-entity-records' { + export namespace BaseEntityRecords { + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + export interface PostStatusObject< C extends Context > { + /** + * The title for the status. + */ + name: string; + + /** + * Whether posts with this status should be private. + */ + private: boolean; + + /** + * Whether posts with this status should be protected. + */ + protected: boolean; + + /** + * Whether posts of this status should be shown in the front end of the site. + */ + public: boolean; + + /** + * Whether posts with this status should be publicly-queryable. + */ + queryable: boolean; + + /** + * Whether to include posts in the edit listing for their post type. + */ + show_in_list: boolean; + + /** + * An alphanumeric identifier for the status. + */ + slug: string; + + /** + * Whether posts of this status may have floating published dates. + */ + date_floating: boolean; + } + } +} + +export type PostStatusObject< C extends Context = 'edit' > = OmitNevers< + _BaseEntityRecords.Type< C > +>; diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index 99507a914f377..db0fc85496133 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -20,6 +20,7 @@ import { } from './entities'; import { STORE_NAME } from './name'; import { unlock } from './lock-unlock'; +import { dynamicActions, dynamicSelectors } from './dynamic-entities'; // The entity selectors/resolvers and actions are shortcuts to their generic equivalents // (getEntityRecord, getEntityRecords, updateEntityRecord, updateEntityRecords) @@ -68,8 +69,17 @@ const entityActions = entitiesConfig.reduce( ( result, entity ) => { const storeConfig = () => ( { reducer, - actions: { ...actions, ...entityActions, ...createLocksActions() }, - selectors: { ...selectors, ...entitySelectors }, + actions: { + ...dynamicActions, + ...actions, + ...entityActions, + ...createLocksActions(), + }, + selectors: { + ...dynamicSelectors, + ...selectors, + ...entitySelectors, + }, resolvers: { ...resolvers, ...entityResolvers }, } ); diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 7f4b0d3884646..c31ebc0425464 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -113,7 +113,7 @@ type Optional< T > = T | undefined; /** * HTTP Query parameters sent with the API request to fetch the entity records. */ -type GetRecordsHttpQuery = Record< string, any >; +export type GetRecordsHttpQuery = Record< string, any >; /** * Arguments for EntityRecord selectors. diff --git a/packages/fields/src/components/create-template-part-modal/index.tsx b/packages/fields/src/components/create-template-part-modal/index.tsx index 8728f2681a493..927192eee17fc 100644 --- a/packages/fields/src/components/create-template-part-modal/index.tsx +++ b/packages/fields/src/components/create-template-part-modal/index.tsx @@ -11,6 +11,7 @@ import { __experimentalVStack as VStack, } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; +import type { TemplatePartArea } from '@wordpress/core-data'; import { store as coreStore } from '@wordpress/core-data'; import { useDispatch, useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; @@ -52,13 +53,6 @@ type CreateTemplatePartModalContentsProps = { defaultTitle?: string; }; -type TemplatePartArea = { - area: string; - label: string; - icon: string; - description: string; -}; - /** * A React component that renders a modal for creating a template part. The modal displays a title and the contents for creating the template part. * This component should not live in this package, it should be moved to a dedicated package responsible for managing template. @@ -73,7 +67,6 @@ export default function CreateTemplatePartModal( { } & CreateTemplatePartModalContentsProps ) { const defaultModalTitle = useSelect( ( select ) => - // @ts-expect-error getPostType is not typed with 'wp_template_part' as argument. select( coreStore ).getPostType( 'wp_template_part' )?.labels ?.add_new_item, [] @@ -135,7 +128,6 @@ export function CreateTemplatePartModalContents( { const defaultTemplatePartAreas = useSelect( ( select ) => - // @ts-expect-error getEntityRecord is not typed with unstableBase as argument. select( coreStore ).getEntityRecord< { default_template_part_areas: Array< TemplatePartArea >; } >( 'root', '__unstableBase' )?.default_template_part_areas, diff --git a/packages/fields/src/fields/parent/parent-edit.tsx b/packages/fields/src/fields/parent/parent-edit.tsx index 21cdbee7a365a..60830b02c4ade 100644 --- a/packages/fields/src/fields/parent/parent-edit.tsx +++ b/packages/fields/src/fields/parent/parent-edit.tsx @@ -122,7 +122,6 @@ export function PageAttributesParent( { const { parentPostTitle, pageItems, isHierarchical } = useSelect( ( select ) => { - // @ts-expect-error getPostType is not typed const { getEntityRecord, getEntityRecords, getPostType } = select( coreStore ); @@ -289,7 +288,6 @@ export const ParentEdit = ( { const { id } = field; const homeUrl = useSelect( ( select ) => { - // @ts-expect-error getEntityRecord is not typed with unstableBase as argument. return select( coreStore ).getEntityRecord< { home: string; } >( 'root', '__unstableBase' )?.home as string;