diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js deleted file mode 100644 index e0332a27b562ab..00000000000000 --- a/packages/core-data/src/entities.js +++ /dev/null @@ -1,331 +0,0 @@ -/** - * External dependencies - */ -import { upperFirst, camelCase, map, find, get, startCase } from 'lodash'; - -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { addEntities } from './actions'; - -export const DEFAULT_ENTITY_KEY = 'id'; - -const POST_RAW_ATTRIBUTES = [ 'title', 'excerpt', 'content' ]; - -export const rootEntitiesConfig = [ - { - label: __( 'Base' ), - name: '__unstableBase', - kind: 'root', - baseURL: '/', - baseURLParams: { - _fields: [ - 'description', - 'gmt_offset', - 'home', - 'name', - 'site_icon', - 'site_icon_url', - 'site_logo', - 'timezone_string', - 'url', - ].join( ',' ), - }, - }, - { - label: __( 'Site' ), - name: 'site', - kind: 'root', - baseURL: '/wp/v2/settings', - getTitle: ( record ) => { - return get( record, [ 'title' ], __( 'Site Title' ) ); - }, - }, - { - label: __( 'Post Type' ), - name: 'postType', - kind: 'root', - key: 'slug', - baseURL: '/wp/v2/types', - baseURLParams: { context: 'edit' }, - rawAttributes: POST_RAW_ATTRIBUTES, - }, - { - name: 'media', - kind: 'root', - baseURL: '/wp/v2/media', - baseURLParams: { context: 'edit' }, - plural: 'mediaItems', - label: __( 'Media' ), - }, - { - name: 'taxonomy', - kind: 'root', - key: 'slug', - baseURL: '/wp/v2/taxonomies', - baseURLParams: { context: 'edit' }, - plural: 'taxonomies', - label: __( 'Taxonomy' ), - }, - { - name: 'sidebar', - kind: 'root', - baseURL: '/wp/v2/sidebars', - plural: 'sidebars', - transientEdits: { blocks: true }, - label: __( 'Widget areas' ), - }, - { - name: 'widget', - kind: 'root', - baseURL: '/wp/v2/widgets', - baseURLParams: { context: 'edit' }, - plural: 'widgets', - transientEdits: { blocks: true }, - label: __( 'Widgets' ), - }, - { - name: 'widgetType', - kind: 'root', - baseURL: '/wp/v2/widget-types', - baseURLParams: { context: 'edit' }, - plural: 'widgetTypes', - label: __( 'Widget types' ), - }, - { - label: __( 'User' ), - name: 'user', - kind: 'root', - baseURL: '/wp/v2/users', - baseURLParams: { context: 'edit' }, - plural: 'users', - }, - { - name: 'comment', - kind: 'root', - baseURL: '/wp/v2/comments', - baseURLParams: { context: 'edit' }, - plural: 'comments', - label: __( 'Comment' ), - }, - { - name: 'menu', - kind: 'root', - baseURL: '/wp/v2/menus', - baseURLParams: { context: 'edit' }, - plural: 'menus', - label: __( 'Menu' ), - }, - { - name: 'menuItem', - kind: 'root', - baseURL: '/wp/v2/menu-items', - baseURLParams: { context: 'edit' }, - plural: 'menuItems', - label: __( 'Menu Item' ), - rawAttributes: [ 'title', 'content' ], - }, - { - name: 'menuLocation', - kind: 'root', - baseURL: '/wp/v2/menu-locations', - baseURLParams: { context: 'edit' }, - plural: 'menuLocations', - label: __( 'Menu Location' ), - key: 'name', - }, - { - name: 'navigationArea', - kind: 'root', - baseURL: '/wp/v2/block-navigation-areas', - baseURLParams: { context: 'edit' }, - plural: 'navigationAreas', - label: __( 'Navigation Area' ), - key: 'name', - getTitle: ( record ) => record?.description, - }, - { - label: __( 'Global Styles' ), - name: 'globalStyles', - kind: 'root', - baseURL: '/wp/v2/global-styles', - baseURLParams: { context: 'edit' }, - plural: 'globalStylesVariations', // Should be different than name. - getTitle: ( record ) => record?.title?.rendered || record?.title, - }, - { - label: __( 'Themes' ), - name: 'theme', - kind: 'root', - baseURL: '/wp/v2/themes', - baseURLParams: { context: 'edit' }, - key: 'stylesheet', - }, - { - label: __( 'Plugins' ), - name: 'plugin', - kind: 'root', - baseURL: '/wp/v2/plugins', - baseURLParams: { context: 'edit' }, - key: 'plugin', - }, -]; - -export const additionalEntityConfigLoaders = [ - { kind: 'postType', loadEntities: loadPostTypeEntities }, - { kind: 'taxonomy', loadEntities: loadTaxonomyEntities }, -]; - -/** - * Returns a function to be used to retrieve extra edits to apply before persisting a post type. - * - * @param {Object} persistedRecord Already persisted Post - * @param {Object} edits Edits. - * @return {Object} Updated edits. - */ -export const prePersistPostType = ( persistedRecord, edits ) => { - const newEdits = {}; - - if ( persistedRecord?.status === 'auto-draft' ) { - // Saving an auto-draft should create a draft by default. - if ( ! edits.status && ! newEdits.status ) { - newEdits.status = 'draft'; - } - - // Fix the auto-draft default title. - if ( - ( ! edits.title || edits.title === 'Auto Draft' ) && - ! newEdits.title && - ( ! persistedRecord?.title || - persistedRecord?.title === 'Auto Draft' ) - ) { - newEdits.title = ''; - } - } - - return newEdits; -}; - -/** - * Returns the list of post type entities. - * - * @return {Promise} Entities promise - */ -async function loadPostTypeEntities() { - const postTypes = await apiFetch( { path: '/wp/v2/types?context=view' } ); - return map( postTypes, ( postType, name ) => { - const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( - name - ); - const namespace = postType?.rest_namespace ?? 'wp/v2'; - return { - kind: 'postType', - baseURL: `/${ namespace }/${ postType.rest_base }`, - baseURLParams: { context: 'edit' }, - name, - label: postType.name, - transientEdits: { - blocks: true, - selection: true, - }, - mergedEdits: { meta: true }, - rawAttributes: POST_RAW_ATTRIBUTES, - getTitle: ( record ) => - record?.title?.rendered || - record?.title || - ( isTemplate ? startCase( record.slug ) : String( record.id ) ), - __unstablePrePersist: isTemplate ? undefined : prePersistPostType, - __unstable_rest_base: postType.rest_base, - }; - } ); -} - -/** - * Returns the list of the taxonomies entities. - * - * @return {Promise} Entities promise - */ -async function loadTaxonomyEntities() { - const taxonomies = await apiFetch( { - path: '/wp/v2/taxonomies?context=view', - } ); - return map( taxonomies, ( taxonomy, name ) => { - const namespace = taxonomy?.rest_namespace ?? 'wp/v2'; - return { - kind: 'taxonomy', - baseURL: `/${ namespace }/${ taxonomy.rest_base }`, - baseURLParams: { context: 'edit' }, - name, - label: taxonomy.name, - }; - } ); -} - -/** - * Returns the entity's getter method name given its kind and name. - * - * @example - * ```js - * const nameSingular = getMethodName( 'root', 'theme', 'get' ); - * // nameSingular is getRootTheme - * - * const namePlural = getMethodName( 'root', 'theme', 'set' ); - * // namePlural is setRootThemes - * ``` - * - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {string} prefix Function prefix. - * @param {boolean} usePlural Whether to use the plural form or not. - * - * @return {string} Method name - */ -export const getMethodName = ( - kind, - name, - prefix = 'get', - usePlural = false -) => { - const entityConfig = find( rootEntitiesConfig, { kind, name } ); - const kindPrefix = kind === 'root' ? '' : upperFirst( camelCase( kind ) ); - const nameSuffix = - upperFirst( camelCase( name ) ) + ( usePlural ? 's' : '' ); - const suffix = - usePlural && entityConfig.plural - ? upperFirst( camelCase( entityConfig.plural ) ) - : nameSuffix; - return `${ prefix }${ kindPrefix }${ suffix }`; -}; - -/** - * Loads the kind entities into the store. - * - * @param {string} kind Kind - * - * @return {Array} Entities - */ -export const getOrLoadEntitiesConfig = ( kind ) => async ( { - select, - dispatch, -} ) => { - let configs = select.getEntitiesConfig( kind ); - if ( configs && configs.length !== 0 ) { - return configs; - } - - const loader = find( additionalEntityConfigLoaders, { kind } ); - if ( ! loader ) { - return []; - } - - configs = await loader.loadEntities(); - dispatch( addEntities( configs ) ); - - return configs; -}; diff --git a/packages/core-data/src/entities.ts b/packages/core-data/src/entities.ts new file mode 100644 index 00000000000000..289b94f1eb4144 --- /dev/null +++ b/packages/core-data/src/entities.ts @@ -0,0 +1,471 @@ +/** + * External dependencies + */ +import { upperFirst, camelCase, map, find, get, startCase } from 'lodash'; + +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { addEntities } from './actions'; +import type * as Records from './entity-types'; +import type { Context } from './entity-types'; +import type { EntityConfigTypeFromConst } from './entity-types/entities'; + +export const DEFAULT_ENTITY_KEY = 'id'; + +const POST_RAW_ATTRIBUTES = [ 'title', 'excerpt', 'content' ]; + +const attachmentConfig = { + name: 'media', + kind: 'root', + baseURL: '/wp/v2/media', + baseURLParams: { context: 'edit' }, + plural: 'mediaItems', + label: __( 'Media' ), +} as const; + +type AttachmentConfig< C extends Context > = EntityConfigTypeFromConst< + typeof attachmentConfig, + Records.Attachment< C > +>; + +const siteConfig = { + label: __( 'Site' ), + name: 'site', + kind: 'root', + baseURL: '/wp/v2/settings', + getTitle: ( record: Records.Settings< 'edit' > ) => { + return get( record, [ 'title' ], __( 'Site Title' ) ); + }, +} as const; + +type SiteConfig< C extends Context > = EntityConfigTypeFromConst< + typeof siteConfig, + Records.Settings< C > +>; + +const postTypeConfig = { + label: __( 'Post Type' ), + name: 'postType', + kind: 'root', + key: 'slug', + baseURL: '/wp/v2/types', + baseURLParams: { context: 'edit' }, + rawAttributes: POST_RAW_ATTRIBUTES, +} as const; + +type TypeConfig< C extends Context > = EntityConfigTypeFromConst< + typeof postTypeConfig, + Records.Type< C > +>; + +const taxonomyConfig = { + name: 'taxonomy', + kind: 'root', + key: 'slug', + baseURL: '/wp/v2/taxonomies', + baseURLParams: { context: 'edit' }, + plural: 'taxonomies', + label: __( 'Taxonomy' ), +} as const; + +type TaxonomyConfig< C extends Context > = EntityConfigTypeFromConst< + typeof taxonomyConfig, + Records.Taxonomy< C > +>; + +const sidebarConfig = { + name: 'sidebar', + kind: 'root', + baseURL: '/wp/v2/sidebars', + plural: 'sidebars', + transientEdits: { blocks: true }, + label: __( 'Widget areas' ), +} as const; + +type SidebarConfig< C extends Context > = EntityConfigTypeFromConst< + typeof sidebarConfig, + Records.Sidebar< C > +>; + +const widgetConfig = { + name: 'widget', + kind: 'root', + baseURL: '/wp/v2/widgets', + baseURLParams: { context: 'edit' }, + plural: 'widgets', + transientEdits: { blocks: true }, + label: __( 'Widgets' ), +} as const; + +type WidgetConfig< C extends Context > = EntityConfigTypeFromConst< + typeof widgetConfig, + Records.Widget< C > +>; + +const widgetTypeConfig = { + name: 'widgetType', + kind: 'root', + baseURL: '/wp/v2/widget-types', + baseURLParams: { context: 'edit' }, + plural: 'widgetTypes', + label: __( 'Widget types' ), +} as const; + +type WidgetTypeConfig< C extends Context > = EntityConfigTypeFromConst< + typeof widgetTypeConfig, + Records.WidgetType< C > +>; + +const userConfig = { + label: __( 'User' ), + name: 'user', + kind: 'root', + baseURL: '/wp/v2/users', + baseURLParams: { context: 'edit' }, + plural: 'users', +} as const; + +type UserConfig< C extends Context > = EntityConfigTypeFromConst< + typeof userConfig, + Records.User< C > +>; + +const commentConfig = { + name: 'comment', + kind: 'root', + baseURL: '/wp/v2/comments', + baseURLParams: { context: 'edit' }, + plural: 'comments', + label: __( 'Comment' ), +} as const; + +type CommentConfig< C extends Context > = EntityConfigTypeFromConst< + typeof commentConfig, + Records.Comment< C > +>; + +const menuConfig = { + name: 'menu', + kind: 'root', + baseURL: '/wp/v2/menus', + baseURLParams: { context: 'edit' }, + plural: 'menus', + label: __( 'Menu' ), +} as const; + +type NavMenuConfig< C extends Context > = EntityConfigTypeFromConst< + typeof menuConfig, + Records.NavMenu< C > +>; + +const menuItemConfig = { + name: 'menuItem', + kind: 'root', + baseURL: '/wp/v2/menu-items', + baseURLParams: { context: 'edit' }, + plural: 'menuItems', + label: __( 'Menu Item' ), + rawAttributes: [ 'title', 'content' ], +} as const; + +type NavMenuItemConfig< C extends Context > = EntityConfigTypeFromConst< + typeof menuItemConfig, + Records.NavMenu< C > +>; + +const menuLocationConfig = { + name: 'menuLocation', + kind: 'root', + baseURL: '/wp/v2/menu-locations', + baseURLParams: { context: 'edit' }, + plural: 'menuLocations', + label: __( 'Menu Location' ), + key: 'name', +} as const; + +type MenuLocationConfig< C extends Context > = EntityConfigTypeFromConst< + typeof menuLocationConfig, + Records.MenuLocation< C > +>; + +const navigationAreaConfig = { + name: 'navigationArea', + kind: 'root', + baseURL: '/wp/v2/block-navigation-areas', + baseURLParams: { context: 'edit' }, + plural: 'navigationAreas', + label: __( 'Navigation Area' ), + key: 'name', + getTitle: ( record: Records.NavigationArea< 'edit' > | null ) => + record?.description, +} as const; + +type NavigationAreaConfig< C extends Context > = EntityConfigTypeFromConst< + typeof navigationAreaConfig, + Records.NavigationArea< C > +>; + +const globalStyleConfig = { + label: __( 'Global Styles' ), + name: 'globalStyles', + kind: 'root', + baseURL: '/wp/v2/global-styles', + baseURLParams: { context: 'edit' }, + plural: 'globalStylesVariations', // should be different than name + getTitle: ( record: any ) => record?.title?.rendered || record?.title, +} as const; + +const themeConfig = { + label: __( 'Themes' ), + name: 'theme', + kind: 'root', + baseURL: '/wp/v2/themes', + baseURLParams: { context: 'edit' }, + key: 'stylesheet', +} as const; + +type ThemeConfig< C extends Context > = EntityConfigTypeFromConst< + typeof themeConfig, + Records.Theme< C > +>; + +const pluginConfig = { + label: __( 'Plugins' ), + name: 'plugin', + kind: 'root', + baseURL: '/wp/v2/plugins', + baseURLParams: { context: 'edit' }, + key: 'plugin', +} as const; + +type PluginConfig< C extends Context > = EntityConfigTypeFromConst< + typeof pluginConfig, + Records.Plugin< C > +>; + +export const rootEntitiesConfig = [ + { + label: __( 'Base' ), + kind: 'root', + name: '__unstableBase', + baseURL: '/', + }, + siteConfig, + postTypeConfig, + attachmentConfig, + taxonomyConfig, + sidebarConfig, + widgetConfig, + widgetTypeConfig, + userConfig, + commentConfig, + menuConfig, + menuItemConfig, + menuLocationConfig, + globalStyleConfig, + themeConfig, + pluginConfig, +]; + +type PostTypeConfig = { + kind: 'postType'; + key: 'id'; + defaultContext: 'edit'; +}; + +type PostConfig< C extends Context > = PostTypeConfig & { + name: 'post'; + recordType: Records.Post< C >; +}; +type PageConfig< C extends Context > = PostTypeConfig & { + name: 'page'; + recordType: Records.Page< C >; +}; +type WpTemplateConfig< C extends Context > = PostTypeConfig & { + name: 'wp_template'; + recordType: Records.WpTemplate< C >; +}; +type WpTemplatePartConfig< C extends Context > = PostTypeConfig & { + name: 'wp_template_part'; + recordType: Records.WpTemplatePart< C >; +}; + +export type CoreEntityConfig< C extends Context > = + | SiteConfig< C > + | TypeConfig< C > + | AttachmentConfig< C > + | TaxonomyConfig< C > + | SidebarConfig< C > + | WidgetConfig< C > + | WidgetTypeConfig< C > + | UserConfig< C > + | CommentConfig< C > + | NavMenuConfig< C > + | NavMenuItemConfig< C > + | NavigationAreaConfig< C > + | MenuLocationConfig< C > + | ThemeConfig< C > + | PluginConfig< C > + | PostConfig< C > + | PageConfig< C > + | WpTemplateConfig< C > + | WpTemplatePartConfig< C >; + +export const additionalEntityConfigLoaders = [ + { kind: 'postType', loadEntities: loadPostTypeEntities }, + { kind: 'taxonomy', loadEntities: loadTaxonomyEntities }, +]; + +/** + * Returns a function to be used to retrieve extra edits to apply before persisting a post type. + * + * @param {Object} persistedRecord Already persisted Post + * @param {Object} edits Edits. + * @return {Object} Updated edits. + */ +export const prePersistPostType = ( persistedRecord, edits ) => { + const newEdits = {} as any; + + if ( persistedRecord?.status === 'auto-draft' ) { + // Saving an auto-draft should create a draft by default. + if ( ! edits.status && ! newEdits.status ) { + newEdits.status = 'draft'; + } + + // Fix the auto-draft default title. + if ( + ( ! edits.title || edits.title === 'Auto Draft' ) && + ! newEdits.title && + ( ! persistedRecord?.title || + persistedRecord?.title === 'Auto Draft' ) + ) { + newEdits.title = ''; + } + } + + return newEdits; +}; + +/** + * Returns the list of post type entities. + * + * @return {Promise} Entities promise + */ +async function loadPostTypeEntities() { + const postTypes = ( await apiFetch( { + path: '/wp/v2/types?context=view', + } ) ) as Record< string, Records.Type< 'view' > >; + return map( postTypes, ( _postType, name ) => { + const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( + name + ); + const namespace = _postType?.rest_namespace ?? 'wp/v2'; + return { + kind: 'postType', + baseURL: `/${ namespace }/${ _postType.rest_base }`, + baseURLParams: { context: 'edit' }, + name, + label: _postType.name, + transientEdits: { + blocks: true, + selection: true, + }, + mergedEdits: { meta: true }, + rawAttributes: POST_RAW_ATTRIBUTES, + getTitle: ( record ) => + record?.title?.rendered || + record?.title || + ( isTemplate ? startCase( record.slug ) : String( record.id ) ), + __unstablePrePersist: isTemplate ? undefined : prePersistPostType, + __unstable_rest_base: _postType.rest_base, + }; + } ); +} + +/** + * Returns the list of the taxonomies entities. + * + * @return {Promise} Entities promise + */ +async function loadTaxonomyEntities() { + const taxonomies = ( await apiFetch( { + path: '/wp/v2/taxonomies?context=view', + } ) ) as Array< Records.Taxonomy< 'view' > >; + return map( taxonomies, ( _taxonomy, name ) => { + const namespace = _taxonomy?.rest_namespace ?? 'wp/v2'; + return { + kind: 'taxonomy', + baseURL: `/${ namespace }/${ _taxonomy.rest_base }`, + baseURLParams: { context: 'edit' }, + name, + label: _taxonomy.name, + }; + } ); +} + +/** + * Returns the entity's getter method name given its kind and name. + * + * @example + * ```js + * const nameSingular = getMethodName( 'root', 'theme', 'get' ); + * // nameSingular is getRootTheme + * + * const namePlural = getMethodName( 'root', 'theme', 'set' ); + * // namePlural is setRootThemes + * ``` + * + * @param {string} kind Entity kind. + * @param {string} name Entity name. + * @param {string} prefix Function prefix. + * @param {boolean} usePlural Whether to use the plural form or not. + * + * @return {string} Method name + */ +export const getMethodName = ( + kind, + name, + prefix = 'get', + usePlural = false +) => { + const entityConfig = find( rootEntitiesConfig, { kind, name } ) as any; + const kindPrefix = kind === 'root' ? '' : upperFirst( camelCase( kind ) ); + const nameSuffix = + upperFirst( camelCase( name ) ) + ( usePlural ? 's' : '' ); + const suffix = + usePlural && entityConfig.plural + ? upperFirst( camelCase( entityConfig.plural ) ) + : nameSuffix; + return `${ prefix }${ kindPrefix }${ suffix }`; +}; + +/** + * Loads the kind entities into the store. + * + * @param {string} kind Kind + * + * @return {Array} Entities + */ +const getOrLoadEntitiesConfig = ( kind ) => async ( { select, dispatch } ) => { + let configs = select.getEntitiesConfig( kind ); + if ( configs && configs.length !== 0 ) { + return configs; + } + + const loader = find( additionalEntityConfigLoaders, { kind } ); + if ( ! loader ) { + return []; + } + + configs = await loader.loadEntities(); + dispatch( addEntities( configs ) ); + + return configs; +}; diff --git a/packages/core-data/src/entity-types/attachment.ts b/packages/core-data/src/entity-types/attachment.ts index 22aceb5a00b869..df698031bcef5b 100644 --- a/packages/core-data/src/entity-types/attachment.ts +++ b/packages/core-data/src/entity-types/attachment.ts @@ -13,6 +13,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -141,6 +142,6 @@ declare module './base-entity-records' { } } -export type Attachment< C extends Context > = OmitNevers< - _BaseEntityRecords.Attachment< C > ->; +export type Attachment< + C extends Context = DefaultContextOf< 'root', 'media' > +> = OmitNevers< _BaseEntityRecords.Attachment< C > >; diff --git a/packages/core-data/src/entity-types/comment.ts b/packages/core-data/src/entity-types/comment.ts index 70a8fab8bfd610..977659122d7311 100644 --- a/packages/core-data/src/entity-types/comment.ts +++ b/packages/core-data/src/entity-types/comment.ts @@ -9,6 +9,7 @@ import type { RenderedText, } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; export type CommentStatus = 'hold' | 'approve' | 'spam' | 'trash' | '1' | '0'; @@ -91,6 +92,6 @@ declare module './base-entity-records' { } } -export type Comment< C extends Context > = OmitNevers< - _BaseEntityRecords.Comment< C > ->; +export type Comment< + C extends Context = DefaultContextOf< 'root', 'comment' > +> = OmitNevers< _BaseEntityRecords.Comment< C > >; diff --git a/packages/core-data/src/entity-types/entities.ts b/packages/core-data/src/entity-types/entities.ts new file mode 100644 index 00000000000000..7ad1b3e7b5bb4a --- /dev/null +++ b/packages/core-data/src/entity-types/entities.ts @@ -0,0 +1,162 @@ +/** + * Internal dependencies + */ +import type { Context } from './helpers'; +import type { CoreEntityConfig } from '../entities'; + +/** + * HTTP Query parameters sent with the API request to fetch the entity records. + */ +export type EntityQuery< + C extends Context, + Fields extends string[] | undefined = undefined +> = Record< string, any > & { + context?: C; + /** + * The requested fields. If specified, the REST API will remove from the response + * any fields not on that list. + */ + _fields?: Fields; +}; + +/** + * Helper type that transforms "raw" entity configuration from entities.ts + * into a format that makes searching by root and kind easy and extensible. + * + * This is the foundation of return type inference in calls such as: + * `getEntityRecord( "root", "comment", 15 )`. + * + * @see EntityRecordOf + * @see getEntityRecord + */ +export type EntityConfigTypeFromConst< + E extends { + kind: string; + name: string; + baseURLParams?: EntityQuery< any >; + key?: string; + }, + R +> = { + kind: E[ 'kind' ]; + name: E[ 'name' ]; + recordType: R; + key: E[ 'key' ] extends string ? E[ 'key' ] : 'id'; + defaultContext: E[ 'baseURLParams' ] extends { + context: infer C; + } + ? C + : 'view'; +}; + +/** + * An interface that may be extended to add types for new entities. Each entry + * must be a union of entity definitions adhering to the EntityInterface type. + * + * Example: + * + * ```ts + * import type { Context } from '@wordpress/core-data'; + * // ... + * + * interface Order { + * id: number; + * clientId: number; + * // ... + * } + * + * type OrderEntity = { + * kind: 'myPlugin'; + * name: 'order'; + * recordType: Order; + * } + * + * declare module '@wordpress/core-data' { + * export interface PerPackageEntities< C extends Context > { + * myPlugin: OrderEntity | ClientEntity + * } + * } + * + * const c = getEntityRecord( 'myPlugin', 'order', 15 ); + * // c is of the type Order + * ``` + */ +export interface PerPackageEntityConfig< C extends Context > { + core: CoreEntityConfig< C >; +} + +/** + * A union of all the registered entities. + */ +type EntityConfig< + C extends Context = any +> = PerPackageEntityConfig< C >[ keyof PerPackageEntityConfig< C > ]; + +/** + * A union of all known record types. + */ +export type EntityRecord< + C extends Context = any +> = EntityConfig< C >[ 'recordType' ]; + +/** + * An entity corresponding to a specified record type. + */ +export type EntityConfigOf< + RecordOrKind extends EntityRecord | Kind, + N extends Name = undefined +> = RecordOrKind extends EntityRecord + ? Extract< EntityConfig, { recordType: RecordOrKind } > + : Extract< EntityConfig, { kind: RecordOrKind; name: N } >; + +/** + * Name of the requested entity. + */ +export type NameOf< R extends EntityRecord > = EntityConfigOf< R >[ 'name' ]; + +/** + * Kind of the requested entity. + */ +export type KindOf< R extends EntityRecord > = EntityConfigOf< R >[ 'kind' ]; + +/** + * Primary key type of the requested entity, sourced from PerPackageEntities. + * + * For core entities, the key type is computed using the entity configuration in entities.js. + */ +export type KeyOf< + RecordOrKind extends EntityRecord | Kind, + N extends Name = undefined, + E extends EntityConfig = EntityConfigOf< RecordOrKind, N > +> = E[ 'key' ] extends keyof E[ 'recordType' ] + ? E[ 'recordType' ][ E[ 'key' ] ] + : never; + +/** + * Default context of the requested entity, sourced from PerPackageEntities. + * + * For core entities, the default context is extracted from the entity configuration + * in entities.js. + */ +export type DefaultContextOf< + RecordOrKind extends EntityRecord | Kind, + N extends Name = undefined +> = EntityConfigOf< RecordOrKind, N >[ 'defaultContext' ]; + +/** + * An entity record type associated with specified kind and name, sourced from PerPackageEntities. + */ +export type EntityRecordOf< + K extends Kind, + N extends Name, + C extends Context = DefaultContextOf< K, N > +> = Extract< EntityConfig< C >, { kind: K; name: N } >[ 'recordType' ]; + +/** + * A union of all known entity kinds. + */ +export type Kind = EntityConfig[ 'kind' ]; +/** + * A union of all known entity names. + */ +export type Name = EntityConfig[ 'name' ]; diff --git a/packages/core-data/src/entity-types/helpers.ts b/packages/core-data/src/entity-types/helpers.ts index 59ed25a72bc8b1..b1826e83f6ccaa 100644 --- a/packages/core-data/src/entity-types/helpers.ts +++ b/packages/core-data/src/entity-types/helpers.ts @@ -148,6 +148,6 @@ export interface RenderedText< C extends Context > { * // updatablePost.title is a string * ``` */ -export type Updatable< T extends EntityRecord< 'edit' > > = { +export type Updatable< T extends EntityRecord > = { [ K in keyof T ]: T[ K ] extends RenderedText< any > ? string : T[ K ]; }; diff --git a/packages/core-data/src/entity-types/index.ts b/packages/core-data/src/entity-types/index.ts index 19416f7b61fa4e..f58f34be24bc2e 100644 --- a/packages/core-data/src/entity-types/index.ts +++ b/packages/core-data/src/entity-types/index.ts @@ -21,6 +21,7 @@ import type { Widget } from './widget'; import type { WidgetType } from './widget-type'; import type { WpTemplate } from './wp-template'; import type { WpTemplatePart } from './wp-template-part'; +import type { Context, Updatable } from './helpers'; export type { BaseEntityRecords } from './base-entity-records'; @@ -46,25 +47,10 @@ export type { WpTemplatePart, }; -export type EntityRecord< C extends Context > = - | Attachment< C > - | Comment< C > - | MenuLocation< C > - | NavMenu< C > - | NavMenuItem< C > - | NavigationArea< C > - | Page< C > - | Plugin< C > - | Post< C > - | Settings< C > - | Sidebar< C > - | Taxonomy< C > - | Theme< C > - | Type< C > - | User< C > - | Widget< C > - | WidgetType< C > - | WpTemplate< C > - | WpTemplatePart< C >; +export type { PerPackageEntityConfig, EntityRecord } from './entities'; -export type UpdatableEntityRecord = Updatable< EntityRecord< 'edit' > >; +export type { + getEntityRecord, + getEditedEntityRecord, + getRawEntityRecord, +} from './selectors'; diff --git a/packages/core-data/src/entity-types/menu-location.ts b/packages/core-data/src/entity-types/menu-location.ts index a7d16067aa7a92..e2173a3f56784a 100644 --- a/packages/core-data/src/entity-types/menu-location.ts +++ b/packages/core-data/src/entity-types/menu-location.ts @@ -4,6 +4,7 @@ import type { Context, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -24,6 +25,6 @@ declare module './base-entity-records' { } } -export type MenuLocation< C extends Context > = OmitNevers< - _BaseEntityRecords.MenuLocation< C > ->; +export type MenuLocation< + C extends Context = DefaultContextOf< 'root', 'menuLocation' > +> = OmitNevers< _BaseEntityRecords.MenuLocation< C > >; diff --git a/packages/core-data/src/entity-types/nav-menu-item.ts b/packages/core-data/src/entity-types/nav-menu-item.ts index 901eeb78ba4b46..e922708e0824a1 100644 --- a/packages/core-data/src/entity-types/nav-menu-item.ts +++ b/packages/core-data/src/entity-types/nav-menu-item.ts @@ -9,6 +9,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; export type NavMenuItemType = | 'taxonomy' @@ -106,6 +107,6 @@ declare module './base-entity-records' { } } -export type NavMenuItem< C extends Context > = OmitNevers< - _BaseEntityRecords.NavMenuItem< C > ->; +export type NavMenuItem< + C extends Context = DefaultContextOf< 'root', 'menuItem' > +> = OmitNevers< _BaseEntityRecords.NavMenuItem< C > >; diff --git a/packages/core-data/src/entity-types/nav-menu.ts b/packages/core-data/src/entity-types/nav-menu.ts index 4bdf84c9cc90fd..3b78831c3ce251 100644 --- a/packages/core-data/src/entity-types/nav-menu.ts +++ b/packages/core-data/src/entity-types/nav-menu.ts @@ -4,6 +4,7 @@ import type { Context, ContextualField, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -48,6 +49,6 @@ declare module './base-entity-records' { } } -export type NavMenu< C extends Context > = OmitNevers< - _BaseEntityRecords.NavMenu< C > ->; +export type NavMenu< + C extends Context = DefaultContextOf< 'root', 'menu' > +> = OmitNevers< _BaseEntityRecords.NavMenu< C > >; diff --git a/packages/core-data/src/entity-types/navigation-area.ts b/packages/core-data/src/entity-types/navigation-area.ts index 06250433f46387..622be13b8642b0 100644 --- a/packages/core-data/src/entity-types/navigation-area.ts +++ b/packages/core-data/src/entity-types/navigation-area.ts @@ -4,6 +4,7 @@ import type { Context, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -24,6 +25,6 @@ declare module './base-entity-records' { } } -export type NavigationArea< C extends Context > = OmitNevers< - _BaseEntityRecords.NavigationArea< C > ->; +export type NavigationArea< + C extends Context = DefaultContextOf< 'root', 'navigationArea' > +> = OmitNevers< _BaseEntityRecords.NavigationArea< C > >; diff --git a/packages/core-data/src/entity-types/page.ts b/packages/core-data/src/entity-types/page.ts index 3240da1ffcda88..fe9a469df2f6a4 100644 --- a/packages/core-data/src/entity-types/page.ts +++ b/packages/core-data/src/entity-types/page.ts @@ -12,6 +12,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -139,6 +140,6 @@ declare module './base-entity-records' { } } -export type Page< C extends Context > = OmitNevers< - _BaseEntityRecords.Page< C > ->; +export type Page< + C extends Context = DefaultContextOf< 'postType', 'page' > +> = OmitNevers< _BaseEntityRecords.Page< C > >; diff --git a/packages/core-data/src/entity-types/plugin.ts b/packages/core-data/src/entity-types/plugin.ts index 4d5d92c2b2fe6e..b34de57baef542 100644 --- a/packages/core-data/src/entity-types/plugin.ts +++ b/packages/core-data/src/entity-types/plugin.ts @@ -9,6 +9,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -74,6 +75,6 @@ declare module './base-entity-records' { } export type PluginStatus = 'active' | 'inactive'; -export type Plugin< C extends Context > = OmitNevers< - _BaseEntityRecords.Plugin< C > ->; +export type Plugin< + C extends Context = DefaultContextOf< 'root', 'plugin' > +> = OmitNevers< _BaseEntityRecords.Plugin< C > >; diff --git a/packages/core-data/src/entity-types/post.ts b/packages/core-data/src/entity-types/post.ts index 2308b76cb92644..f2d877534b31fe 100644 --- a/packages/core-data/src/entity-types/post.ts +++ b/packages/core-data/src/entity-types/post.ts @@ -13,6 +13,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -148,6 +149,6 @@ declare module './base-entity-records' { } } -export type Post< C extends Context > = OmitNevers< - _BaseEntityRecords.Post< C > ->; +export type Post< + C extends Context = DefaultContextOf< 'postType', 'post' > +> = OmitNevers< _BaseEntityRecords.Post< C > >; diff --git a/packages/core-data/src/entity-types/selectors.ts b/packages/core-data/src/entity-types/selectors.ts new file mode 100644 index 00000000000000..6fd6fa6a4de8b8 --- /dev/null +++ b/packages/core-data/src/entity-types/selectors.ts @@ -0,0 +1,237 @@ +/** + * Internal dependencies + */ +import type { + Context, + Theme, + Updatable, + User, + WpTemplate, + Comment, +} from './index'; +import type { + DefaultContextOf, + EntityDeclaration, + EntityConfigOf, + EntityQuery, + KeyOf, + Kind, + KindOf, + Name, + NameOf, + EntityRecordOf, +} from './entities'; + +type State = any; + +/** + * Types for all the selectors in core-data. + * + * Occasionally the return type is any – this reflects the historical JSdoc annotations. + * Ideally, all instances of `any` will turn into appropriate types over time. + */ +export type isRequestingEmbedPreview = ( state: State, url: string ) => boolean; +export type getAuthors = ( + state: State, + query: Record< string, unknown > +) => User[]; + +export type getCurrentUser = ( state: State ) => User; +export type getUserQueryResults = ( state: State, queryID: string ) => User[]; +export type getEntitiesByKind = ( + state: State, + queryID: string +) => EntityDeclaration[]; + +export type getEntity = ( + state: State, + kind: Kind, + name: Name +) => EntityDeclaration | undefined; + +export type getEntityRecord = < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R >, + /** + * The requested fields. If specified, the REST API will remove from the response + * any fields not on that list. + */ + Fields extends string[] | undefined = undefined +>( + state: State, + kind: K, + name: N, + key: KeyOf< R >, + query?: EntityQuery< C, Fields > +) => + | ( Fields extends undefined + ? EntityRecordOf< K, N, C > + : Partial< EntityRecordOf< K, N, C > > ) + | null + | undefined; + +export type __experimentalGetEntityRecordNoResolver = getEntityRecord; + +export type getRawEntityRecord = getEditedEntityRecord; + +export type hasEntityRecords = < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > +>( + state: State, + kind: K, + name: N, + query?: { + context?: C; + } +) => boolean; + +export type getEntityRecords = < + R extends EntityRecordOf< K, N >, + C extends Context = DefaultContextOf< R >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R >, + /** + * The requested fields. If specified, the REST API will remove from the response + * any fields not on that list. + */ + Fields extends string[] | undefined = undefined +>( + state: State, + kind: K, + name: N, + query?: EntityQuery< C, Fields > +) => + | Array< + Fields extends undefined + ? EntityRecordOf< K, N, C > + : Partial< EntityRecordOf< K, N, C > > + > + | null + | undefined; + +export type __experimentalGetDirtyEntityRecords = < + K extends Kind, + N extends Name +>( + state: State +) => { title: string; key: KeyOf< K, N >; name: N; kind: K }[]; + +export type __experimentalGetEntitiesBeingSaved = __experimentalGetDirtyEntityRecords; + +export type getEntityRecordEdits = < + R extends EntityRecordOf< K, N >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > +>( + state: State, + kind: K, + name: N, + key: KeyOf< R > +) => Partial< EntityRecordOf< K, N > >[] | undefined; + +export type getEntityRecordNonTransientEdits = getEntityRecordEdits; + +type recordToBoolean = < + R extends EntityRecordOf< K, N >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > +>( + state: State, + kind: K, + name: N, + key: KeyOf< R > +) => boolean; + +export type hasEditsForEntityRecord = recordToBoolean; + +export type getEditedEntityRecord = < + R extends EntityRecordOf< K, N >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > +>( + state: State, + kind: K, + name: N, + recordId: KeyOf< R > +) => Updatable< EntityRecordOf< K, N > > | null | undefined; + +export type isAutosavingEntityRecord = recordToBoolean; +export type isSavingEntityRecord = recordToBoolean; +export type isDeletingEntityRecord = recordToBoolean; + +export type getLastEntitySaveError = < + R extends EntityRecordOf< K, N >, + K extends Kind = KindOf< R >, + N extends Name = NameOf< R > +>( + state: State, + kind: K, + name: N, + key: KeyOf< R > +) => any; + +export type getLastEntityDeleteError = getLastEntitySaveError; + +export type getCurrentUndoOffset = ( state: State ) => number; +export type getUndoEdit = ( state: State ) => any; +export type getRedoEdit = ( state: State ) => any; +export type hasUndo = ( state: State ) => boolean; +export type hasRedo = ( state: State ) => boolean; +export type getCurrentTheme = ( state: State ) => Theme; +export type __experimentalGetCurrentGlobalStylesId = ( state: State ) => string; +export type getThemeSupports = ( state: State ) => any; +export type getEmbedPreview = ( state: State, url: string ) => any; +export type isPreviewEmbedFallback = ( state: State, url: string ) => boolean; +export type canUser = ( + state: State, + action: string, + resource: string, + id?: string +) => boolean | undefined; +export type canUserEditEntityRecord = recordToBoolean; + +export type getAutosaves = < + N extends EntityConfigOf< 'postType', any >[ 'name' ] +>( + state: State, + postType: N, + postId: number +) => Partial< EntityRecordOf< 'postType', N > >[] | undefined; + +export type getAutosave = < + N extends EntityConfigOf< 'postType', any >[ 'name' ] +>( + state: State, + postType: N, + postId: number, + authorId: number +) => Partial< EntityRecordOf< 'postType', N > > | undefined; + +export type hasFetchedAutosaves = < + N extends EntityConfigOf< 'postType', any >[ 'name' ] +>( + state: State, + postType: N, + postId: number +) => boolean; + +export type getReferenceByDistinctEdits = ( state: State ) => any; +export type __experimentalGetTemplateForLink = ( + state: State, + link: string +) => WpTemplate | null; + +export type __experimentalGetCurrentThemeBaseGlobalStyles = ( + state: State, + link: string +) => any | null; + +export type __experimentalGetCurrentThemeGlobalStylesVariations = ( + state: State, + link: string +) => any | null; diff --git a/packages/core-data/src/entity-types/settings.ts b/packages/core-data/src/entity-types/settings.ts index ee48198a20803c..07c56b1540bba7 100644 --- a/packages/core-data/src/entity-types/settings.ts +++ b/packages/core-data/src/entity-types/settings.ts @@ -9,6 +9,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -93,6 +94,6 @@ declare module './base-entity-records' { } } -export type Settings< C extends Context > = OmitNevers< - _BaseEntityRecords.Settings< C > ->; +export type Settings< + C extends Context = DefaultContextOf< 'root', 'site' > +> = OmitNevers< _BaseEntityRecords.Settings< C > >; diff --git a/packages/core-data/src/entity-types/sidebar.ts b/packages/core-data/src/entity-types/sidebar.ts index cbfceefd901db7..300dfeecc2a18d 100644 --- a/packages/core-data/src/entity-types/sidebar.ts +++ b/packages/core-data/src/entity-types/sidebar.ts @@ -1,10 +1,10 @@ /** * Internal dependencies */ -import type { Widget } from './widget'; import type { Context, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -55,6 +55,6 @@ declare module './base-entity-records' { type SidebarStatus = 'active' | 'inactive'; -export type Sidebar< C extends Context > = OmitNevers< - _BaseEntityRecords.Sidebar< C > ->; +export type Sidebar< + C extends Context = DefaultContextOf< 'root', 'sidebar' > +> = OmitNevers< _BaseEntityRecords.Sidebar< C > >; diff --git a/packages/core-data/src/entity-types/taxonomy.ts b/packages/core-data/src/entity-types/taxonomy.ts index b2587d0f8deb5a..a4293461544e85 100644 --- a/packages/core-data/src/entity-types/taxonomy.ts +++ b/packages/core-data/src/entity-types/taxonomy.ts @@ -4,6 +4,7 @@ import type { Context, ContextualField, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -87,6 +88,6 @@ declare module './base-entity-records' { } } -export type Taxonomy< C extends Context > = OmitNevers< - _BaseEntityRecords.Taxonomy< C > ->; +export type Taxonomy< + C extends Context = DefaultContextOf< 'root', 'taxonomy' > +> = OmitNevers< _BaseEntityRecords.Taxonomy< C > >; diff --git a/packages/core-data/src/entity-types/theme.ts b/packages/core-data/src/entity-types/theme.ts index 3ff8bdb800999f..caddb6cba65a57 100644 --- a/packages/core-data/src/entity-types/theme.ts +++ b/packages/core-data/src/entity-types/theme.ts @@ -4,6 +4,7 @@ import type { Context, PostFormat, RenderedText, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -217,6 +218,6 @@ declare module './base-entity-records' { } } -export type Theme< C extends Context > = OmitNevers< - _BaseEntityRecords.Theme< C > ->; +export type Theme< + C extends Context = DefaultContextOf< 'root', 'theme' > +> = OmitNevers< _BaseEntityRecords.Theme< C > >; diff --git a/packages/core-data/src/entity-types/type.ts b/packages/core-data/src/entity-types/type.ts index c97c5821f2e4be..3e777f92fa3c3a 100644 --- a/packages/core-data/src/entity-types/type.ts +++ b/packages/core-data/src/entity-types/type.ts @@ -4,6 +4,7 @@ import type { Context, ContextualField, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -75,6 +76,6 @@ declare module './base-entity-records' { } } -export type Type< C extends Context > = OmitNevers< - _BaseEntityRecords.Type< C > ->; +export type Type< + C extends Context = DefaultContextOf< 'root', 'postType' > +> = OmitNevers< _BaseEntityRecords.Type< C > >; diff --git a/packages/core-data/src/entity-types/user.ts b/packages/core-data/src/entity-types/user.ts index ddc483fc934a73..425e0bd336a9a4 100644 --- a/packages/core-data/src/entity-types/user.ts +++ b/packages/core-data/src/entity-types/user.ts @@ -9,6 +9,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -109,6 +110,6 @@ declare module './base-entity-records' { } } -export type User< C extends Context > = OmitNevers< - _BaseEntityRecords.User< C > ->; +export type User< + C extends Context = DefaultContextOf< 'root', 'user' > +> = OmitNevers< _BaseEntityRecords.User< C > >; diff --git a/packages/core-data/src/entity-types/widget-type.ts b/packages/core-data/src/entity-types/widget-type.ts index 7f513adab2819f..9b6bd05536a963 100644 --- a/packages/core-data/src/entity-types/widget-type.ts +++ b/packages/core-data/src/entity-types/widget-type.ts @@ -4,6 +4,7 @@ import type { Context, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -32,6 +33,6 @@ declare module './base-entity-records' { } } -export type WidgetType< C extends Context > = OmitNevers< - _BaseEntityRecords.WidgetType< C > ->; +export type WidgetType< + C extends Context = DefaultContextOf< 'root', 'widgetType' > +> = OmitNevers< _BaseEntityRecords.WidgetType< C > >; diff --git a/packages/core-data/src/entity-types/widget.ts b/packages/core-data/src/entity-types/widget.ts index 1c8e2653822a47..24c6ca2be86360 100644 --- a/packages/core-data/src/entity-types/widget.ts +++ b/packages/core-data/src/entity-types/widget.ts @@ -4,6 +4,7 @@ import type { Context, ContextualField, OmitNevers } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -59,6 +60,6 @@ declare module './base-entity-records' { } } -export type Widget< C extends Context > = OmitNevers< - _BaseEntityRecords.Widget< C > ->; +export type Widget< + C extends Context = DefaultContextOf< 'root', 'widget' > +> = OmitNevers< _BaseEntityRecords.Widget< C > >; diff --git a/packages/core-data/src/entity-types/wp-template-part.ts b/packages/core-data/src/entity-types/wp-template-part.ts index f569eb2d3cf401..c62d90ab909085 100644 --- a/packages/core-data/src/entity-types/wp-template-part.ts +++ b/packages/core-data/src/entity-types/wp-template-part.ts @@ -10,6 +10,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -89,6 +90,6 @@ declare module './base-entity-records' { } } -export type WpTemplatePart< C extends Context > = OmitNevers< - _BaseEntityRecords.WpTemplatePart< C > ->; +export type WpTemplatePart< + C extends Context = DefaultContextOf< 'root', 'wp_template_part' > +> = OmitNevers< _BaseEntityRecords.WpTemplatePart< C > >; diff --git a/packages/core-data/src/entity-types/wp-template.ts b/packages/core-data/src/entity-types/wp-template.ts index a36678fc267a6c..49dc6968288cd8 100644 --- a/packages/core-data/src/entity-types/wp-template.ts +++ b/packages/core-data/src/entity-types/wp-template.ts @@ -10,6 +10,7 @@ import type { } from './helpers'; import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; +import type { DefaultContextOf } from './entities'; declare module './base-entity-records' { export namespace BaseEntityRecords { @@ -89,6 +90,6 @@ declare module './base-entity-records' { } } -export type WpTemplate< C extends Context > = OmitNevers< - _BaseEntityRecords.WpTemplate< C > ->; +export type WpTemplate< + C extends Context = DefaultContextOf< 'root', 'wp_template' > +> = OmitNevers< _BaseEntityRecords.WpTemplate< C > >; diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js deleted file mode 100644 index fb6a311be00991..00000000000000 --- a/packages/core-data/src/selectors.js +++ /dev/null @@ -1,964 +0,0 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; -import { set, map, find, get, filter, compact } from 'lodash'; - -/** - * WordPress dependencies - */ -import { createRegistrySelector } from '@wordpress/data'; -import { addQueryArgs } from '@wordpress/url'; -import deprecated from '@wordpress/deprecated'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from './name'; -import { getQueriedItems } from './queried-data'; -import { DEFAULT_ENTITY_KEY } from './entities'; -import { getNormalizedCommaSeparable, isRawAttribute } from './utils'; - -/** - * Shared reference to an empty object for cases where it is important to avoid - * returning a new object reference on every invocation, as in a connected or - * other pure component which performs `shouldComponentUpdate` check on props. - * This should be used as a last resort, since the normalized data should be - * maintained by the reducer result in state. - */ -const EMPTY_OBJECT = {}; - -/** - * Returns true if a request is in progress for embed preview data, or false - * otherwise. - * - * @param {Object} state Data state. - * @param {string} url URL the preview would be for. - * - * @return {boolean} Whether a request is in progress for an embed preview. - */ -export const isRequestingEmbedPreview = createRegistrySelector( - ( select ) => ( state, url ) => { - return select( STORE_NAME ).isResolving( 'getEmbedPreview', [ url ] ); - } -); - -/** - * Returns all available authors. - * - * @deprecated since 11.3. Callers should use `select( 'core' ).getUsers({ who: 'authors' })` instead. - * - * @param {Object} state Data state. - * @param {Object|undefined} query Optional object of query parameters to - * include with request. - * @return {Array} Authors list. - */ -export function getAuthors( state, query ) { - deprecated( "select( 'core' ).getAuthors()", { - since: '5.9', - alternative: "select( 'core' ).getUsers({ who: 'authors' })", - } ); - - const path = addQueryArgs( - '/wp/v2/users/?who=authors&per_page=100', - query - ); - return getUserQueryResults( state, path ); -} - -/** - * Returns the current user. - * - * @param {Object} state Data state. - * - * @return {Object} Current user object. - */ -export function getCurrentUser( state ) { - return state.currentUser; -} - -/** - * Returns all the users returned by a query ID. - * - * @param {Object} state Data state. - * @param {string} queryID Query ID. - * - * @return {Array} Users list. - */ -export const getUserQueryResults = createSelector( - ( state, queryID ) => { - const queryResults = state.users.queries[ queryID ]; - - return map( queryResults, ( id ) => state.users.byId[ id ] ); - }, - ( state, queryID ) => [ state.users.queries[ queryID ], state.users.byId ] -); - -/** - * Returns the loaded entities for the given kind. - * - * @deprecated since WordPress 6.0. Use getEntitiesConfig instead - * @param {Object} state Data state. - * @param {string} kind Entity kind. - * - * @return {Array} Array of entities with config matching kind. - */ -export function getEntitiesByKind( state, kind ) { - deprecated( "wp.data.select( 'core' ).getEntitiesByKind()", { - since: '6.0', - alternative: "wp.data.select( 'core' ).getEntitiesConfig()", - } ); - return getEntitiesConfig( state, kind ); -} - -/** - * Returns the loaded entities for the given kind. - * - * @param {Object} state Data state. - * @param {string} kind Entity kind. - * - * @return {Array} Array of entities with config matching kind. - */ -export function getEntitiesConfig( state, kind ) { - return filter( state.entities.config, { kind } ); -} - -/** - * Returns the entity config given its kind and name. - * - * @deprecated since WordPress 6.0. Use getEntityConfig instead - * @param {Object} state Data state. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * - * @return {Object} Entity config - */ -export function getEntity( state, kind, name ) { - deprecated( "wp.data.select( 'core' ).getEntity()", { - since: '6.0', - alternative: "wp.data.select( 'core' ).getEntityConfig()", - } ); - return getEntityConfig( state, kind, name ); -} - -/** - * Returns the entity config given its kind and name. - * - * @param {Object} state Data state. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * - * @return {Object} Entity config - */ -export function getEntityConfig( state, kind, name ) { - return find( state.entities.config, { kind, name } ); -} - -/** - * Returns the Entity's record object by key. Returns `null` if the value is not - * yet received, undefined if the value entity is known to not exist, or the - * entity object if it exists and is received. - * - * @param {Object} state State tree - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} key Record's key - * @param {?Object} query Optional query. - * - * @return {Object?} Record. - */ -export const getEntityRecord = createSelector( - ( state, kind, name, key, query ) => { - const queriedState = get( state.entities.records, [ - kind, - name, - 'queriedData', - ] ); - if ( ! queriedState ) { - return undefined; - } - const context = query?.context ?? 'default'; - - if ( query === undefined ) { - // If expecting a complete item, validate that completeness. - if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) { - return undefined; - } - - return queriedState.items[ context ][ key ]; - } - - const item = queriedState.items[ context ]?.[ key ]; - if ( item && query._fields ) { - const filteredItem = {}; - const fields = getNormalizedCommaSeparable( query._fields ); - for ( let f = 0; f < fields.length; f++ ) { - const field = fields[ f ].split( '.' ); - const value = get( item, field ); - set( filteredItem, field, value ); - } - return filteredItem; - } - - return item; - }, - ( state, kind, name, recordId, query ) => { - const context = query?.context ?? 'default'; - return [ - get( state.entities.records, [ - kind, - name, - 'queriedData', - 'items', - context, - recordId, - ] ), - get( state.entities.records, [ - kind, - name, - 'queriedData', - 'itemIsComplete', - context, - recordId, - ] ), - ]; - } -); - -/** - * Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity records from the API if the entity record isn't available in the local state. - * - * @param {Object} state State tree - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} key Record's key - * - * @return {Object|null} Record. - */ -export function __experimentalGetEntityRecordNoResolver( - state, - kind, - name, - key -) { - return getEntityRecord( state, kind, name, key ); -} - -/** - * Returns the entity's record object by key, - * with its attributes mapped to their raw values. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} key Record's key. - * - * @return {Object?} Object with the entity's raw attributes. - */ -export const getRawEntityRecord = createSelector( - ( state, kind, name, key ) => { - const record = getEntityRecord( state, kind, name, key ); - return ( - record && - Object.keys( record ).reduce( ( accumulator, _key ) => { - if ( - isRawAttribute( getEntityConfig( state, kind, name ), _key ) - ) { - // Because edits are the "raw" attribute values, - // we return those from record selectors to make rendering, - // comparisons, and joins with edits easier. - accumulator[ _key ] = get( - record[ _key ], - 'raw', - record[ _key ] - ); - } else { - accumulator[ _key ] = record[ _key ]; - } - return accumulator; - }, {} ) - ); - }, - ( state, kind, name, recordId, query ) => { - const context = query?.context ?? 'default'; - return [ - state.entities.config, - get( state.entities.records, [ - kind, - name, - 'queriedData', - 'items', - context, - recordId, - ] ), - get( state.entities.records, [ - kind, - name, - 'queriedData', - 'itemIsComplete', - context, - recordId, - ] ), - ]; - } -); - -/** - * Returns true if records have been received for the given set of parameters, - * or false otherwise. - * - * @param {Object} state State tree - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {?Object} query Optional terms query. - * - * @return {boolean} Whether entity records have been received. - */ -export function hasEntityRecords( state, kind, name, query ) { - return Array.isArray( getEntityRecords( state, kind, name, query ) ); -} - -/** - * Returns the Entity's records. - * - * @param {Object} state State tree - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {?Object} query Optional terms query. - * - * @return {?Array} Records. - */ -export function getEntityRecords( state, kind, name, query ) { - // Queried data state is prepopulated for all known entities. If this is not - // assigned for the given parameters, then it is known to not exist. - const queriedState = get( state.entities.records, [ - kind, - name, - 'queriedData', - ] ); - if ( ! queriedState ) { - return null; - } - return getQueriedItems( queriedState, query ); -} - -/** - * Returns the list of dirty entity records. - * - * @param {Object} state State tree. - * - * @return {[{ title: string, key: string, name: string, kind: string }]} The list of updated records - */ -export const __experimentalGetDirtyEntityRecords = createSelector( - ( state ) => { - const { - entities: { records }, - } = state; - const dirtyRecords = []; - Object.keys( records ).forEach( ( kind ) => { - Object.keys( records[ kind ] ).forEach( ( name ) => { - const primaryKeys = Object.keys( - records[ kind ][ name ].edits - ).filter( - ( primaryKey ) => - // The entity record must exist (not be deleted), - // and it must have edits. - getEntityRecord( state, kind, name, primaryKey ) && - hasEditsForEntityRecord( state, kind, name, primaryKey ) - ); - - if ( primaryKeys.length ) { - const entityConfig = getEntityConfig( state, kind, name ); - primaryKeys.forEach( ( primaryKey ) => { - const entityRecord = getEditedEntityRecord( - state, - kind, - name, - primaryKey - ); - dirtyRecords.push( { - // We avoid using primaryKey because it's transformed into a string - // when it's used as an object key. - key: - entityRecord[ - entityConfig.key || DEFAULT_ENTITY_KEY - ], - title: - entityConfig?.getTitle?.( entityRecord ) || '', - name, - kind, - } ); - } ); - } - } ); - } ); - - return dirtyRecords; - }, - ( state ) => [ state.entities.records ] -); - -/** - * Returns the list of entities currently being saved. - * - * @param {Object} state State tree. - * - * @return {[{ title: string, key: string, name: string, kind: string }]} The list of records being saved. - */ -export const __experimentalGetEntitiesBeingSaved = createSelector( - ( state ) => { - const { - entities: { records }, - } = state; - const recordsBeingSaved = []; - Object.keys( records ).forEach( ( kind ) => { - Object.keys( records[ kind ] ).forEach( ( name ) => { - const primaryKeys = Object.keys( - records[ kind ][ name ].saving - ).filter( ( primaryKey ) => - isSavingEntityRecord( state, kind, name, primaryKey ) - ); - - if ( primaryKeys.length ) { - const entityConfig = getEntityConfig( state, kind, name ); - primaryKeys.forEach( ( primaryKey ) => { - const entityRecord = getEditedEntityRecord( - state, - kind, - name, - primaryKey - ); - recordsBeingSaved.push( { - // We avoid using primaryKey because it's transformed into a string - // when it's used as an object key. - key: - entityRecord[ - entityConfig.key || DEFAULT_ENTITY_KEY - ], - title: - entityConfig?.getTitle?.( entityRecord ) || '', - name, - kind, - } ); - } ); - } - } ); - } ); - return recordsBeingSaved; - }, - ( state ) => [ state.entities.records ] -); - -/** - * Returns the specified entity record's edits. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {Object?} The entity record's edits. - */ -export function getEntityRecordEdits( state, kind, name, recordId ) { - return get( state.entities.records, [ kind, name, 'edits', recordId ] ); -} - -/** - * Returns the specified entity record's non transient edits. - * - * Transient edits don't create an undo level, and - * are not considered for change detection. - * They are defined in the entity's config. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {Object?} The entity record's non transient edits. - */ -export const getEntityRecordNonTransientEdits = createSelector( - ( state, kind, name, recordId ) => { - const { transientEdits } = getEntityConfig( state, kind, name ) || {}; - const edits = getEntityRecordEdits( state, kind, name, recordId ) || {}; - if ( ! transientEdits ) { - return edits; - } - return Object.keys( edits ).reduce( ( acc, key ) => { - if ( ! transientEdits[ key ] ) { - acc[ key ] = edits[ key ]; - } - return acc; - }, {} ); - }, - ( state, kind, name, recordId ) => [ - state.entities.config, - get( state.entities.records, [ kind, name, 'edits', recordId ] ), - ] -); - -/** - * Returns true if the specified entity record has edits, - * and false otherwise. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {boolean} Whether the entity record has edits or not. - */ -export function hasEditsForEntityRecord( state, kind, name, recordId ) { - return ( - isSavingEntityRecord( state, kind, name, recordId ) || - Object.keys( - getEntityRecordNonTransientEdits( state, kind, name, recordId ) - ).length > 0 - ); -} - -/** - * Returns the specified entity record, merged with its edits. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {Object?} The entity record, merged with its edits. - */ -export const getEditedEntityRecord = createSelector( - ( state, kind, name, recordId ) => ( { - ...getRawEntityRecord( state, kind, name, recordId ), - ...getEntityRecordEdits( state, kind, name, recordId ), - } ), - ( state, kind, name, recordId, query ) => { - const context = query?.context ?? 'default'; - return [ - state.entities.config, - get( state.entities.records, [ - kind, - name, - 'queriedData', - 'items', - context, - recordId, - ] ), - get( state.entities.records, [ - kind, - name, - 'queriedData', - 'itemIsComplete', - context, - recordId, - ] ), - get( state.entities.records, [ kind, name, 'edits', recordId ] ), - ]; - } -); - -/** - * Returns true if the specified entity record is autosaving, and false otherwise. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {boolean} Whether the entity record is autosaving or not. - */ -export function isAutosavingEntityRecord( state, kind, name, recordId ) { - const { pending, isAutosave } = get( - state.entities.records, - [ kind, name, 'saving', recordId ], - {} - ); - return Boolean( pending && isAutosave ); -} - -/** - * Returns true if the specified entity record is saving, and false otherwise. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {boolean} Whether the entity record is saving or not. - */ -export function isSavingEntityRecord( state, kind, name, recordId ) { - return get( - state.entities.records, - [ kind, name, 'saving', recordId, 'pending' ], - false - ); -} - -/** - * Returns true if the specified entity record is deleting, and false otherwise. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {boolean} Whether the entity record is deleting or not. - */ -export function isDeletingEntityRecord( state, kind, name, recordId ) { - return get( - state.entities.records, - [ kind, name, 'deleting', recordId, 'pending' ], - false - ); -} - -/** - * Returns the specified entity record's last save error. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {Object?} The entity record's save error. - */ -export function getLastEntitySaveError( state, kind, name, recordId ) { - return get( state.entities.records, [ - kind, - name, - 'saving', - recordId, - 'error', - ] ); -} - -/** - * Returns the specified entity record's last delete error. - * - * @param {Object} state State tree. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {number} recordId Record ID. - * - * @return {Object?} The entity record's save error. - */ -export function getLastEntityDeleteError( state, kind, name, recordId ) { - return get( state.entities.records, [ - kind, - name, - 'deleting', - recordId, - 'error', - ] ); -} - -/** - * Returns the current undo offset for the - * entity records edits history. The offset - * represents how many items from the end - * of the history stack we are at. 0 is the - * last edit, -1 is the second last, and so on. - * - * @param {Object} state State tree. - * - * @return {number} The current undo offset. - */ -function getCurrentUndoOffset( state ) { - return state.undo.offset; -} - -/** - * Returns the previous edit from the current undo offset - * for the entity records edits history, if any. - * - * @param {Object} state State tree. - * - * @return {Object?} The edit. - */ -export function getUndoEdit( state ) { - return state.undo[ state.undo.length - 2 + getCurrentUndoOffset( state ) ]; -} - -/** - * Returns the next edit from the current undo offset - * for the entity records edits history, if any. - * - * @param {Object} state State tree. - * - * @return {Object?} The edit. - */ -export function getRedoEdit( state ) { - return state.undo[ state.undo.length + getCurrentUndoOffset( state ) ]; -} - -/** - * Returns true if there is a previous edit from the current undo offset - * for the entity records edits history, and false otherwise. - * - * @param {Object} state State tree. - * - * @return {boolean} Whether there is a previous edit or not. - */ -export function hasUndo( state ) { - return Boolean( getUndoEdit( state ) ); -} - -/** - * Returns true if there is a next edit from the current undo offset - * for the entity records edits history, and false otherwise. - * - * @param {Object} state State tree. - * - * @return {boolean} Whether there is a next edit or not. - */ -export function hasRedo( state ) { - return Boolean( getRedoEdit( state ) ); -} - -/** - * Return the current theme. - * - * @param {Object} state Data state. - * - * @return {Object} The current theme. - */ -export function getCurrentTheme( state ) { - return getEntityRecord( state, 'root', 'theme', state.currentTheme ); -} - -/** - * Return the ID of the current global styles object. - * - * @param {Object} state Data state. - * - * @return {string} The current global styles ID. - */ -export function __experimentalGetCurrentGlobalStylesId( state ) { - return state.currentGlobalStylesId; -} - -/** - * Return theme supports data in the index. - * - * @param {Object} state Data state. - * - * @return {*} Index data. - */ -export function getThemeSupports( state ) { - return getCurrentTheme( state )?.theme_supports ?? EMPTY_OBJECT; -} - -/** - * Returns the embed preview for the given URL. - * - * @param {Object} state Data state. - * @param {string} url Embedded URL. - * - * @return {*} Undefined if the preview has not been fetched, otherwise, the preview fetched from the embed preview API. - */ -export function getEmbedPreview( state, url ) { - return state.embedPreviews[ url ]; -} - -/** - * Determines if the returned preview is an oEmbed link fallback. - * - * WordPress can be configured to return a simple link to a URL if it is not embeddable. - * We need to be able to determine if a URL is embeddable or not, based on what we - * get back from the oEmbed preview API. - * - * @param {Object} state Data state. - * @param {string} url Embedded URL. - * - * @return {boolean} Is the preview for the URL an oEmbed link fallback. - */ -export function isPreviewEmbedFallback( state, url ) { - const preview = state.embedPreviews[ url ]; - const oEmbedLinkCheck = '' + url + ''; - if ( ! preview ) { - return false; - } - return preview.html === oEmbedLinkCheck; -} - -/** - * Returns whether the current user can perform the given action on the given - * REST resource. - * - * Calling this may trigger an OPTIONS request to the REST API via the - * `canUser()` resolver. - * - * https://developer.wordpress.org/rest-api/reference/ - * - * @param {Object} state Data state. - * @param {string} action Action to check. One of: 'create', 'read', 'update', 'delete'. - * @param {string} resource REST resource to check, e.g. 'media' or 'posts'. - * @param {string=} id Optional ID of the rest resource to check. - * - * @return {boolean|undefined} Whether or not the user can perform the action, - * or `undefined` if the OPTIONS request is still being made. - */ -export function canUser( state, action, resource, id ) { - const key = compact( [ action, resource, id ] ).join( '/' ); - return get( state, [ 'userPermissions', key ] ); -} - -/** - * Returns whether the current user can edit the given entity. - * - * Calling this may trigger an OPTIONS request to the REST API via the - * `canUser()` resolver. - * - * https://developer.wordpress.org/rest-api/reference/ - * - * @param {Object} state Data state. - * @param {string} kind Entity kind. - * @param {string} name Entity name. - * @param {string} recordId Record's id. - * @return {boolean|undefined} Whether or not the user can edit, - * or `undefined` if the OPTIONS request is still being made. - */ -export function canUserEditEntityRecord( state, kind, name, recordId ) { - const entityConfig = getEntityConfig( state, kind, name ); - if ( ! entityConfig ) { - return false; - } - const resource = entityConfig.__unstable_rest_base; - - return canUser( state, 'update', resource, recordId ); -} - -/** - * Returns the latest autosaves for the post. - * - * May return multiple autosaves since the backend stores one autosave per - * author for each post. - * - * @param {Object} state State tree. - * @param {string} postType The type of the parent post. - * @param {number} postId The id of the parent post. - * - * @return {?Array} An array of autosaves for the post, or undefined if there is none. - */ -export function getAutosaves( state, postType, postId ) { - return state.autosaves[ postId ]; -} - -/** - * Returns the autosave for the post and author. - * - * @param {Object} state State tree. - * @param {string} postType The type of the parent post. - * @param {number} postId The id of the parent post. - * @param {number} authorId The id of the author. - * - * @return {?Object} The autosave for the post and author. - */ -export function getAutosave( state, postType, postId, authorId ) { - if ( authorId === undefined ) { - return; - } - - const autosaves = state.autosaves[ postId ]; - return find( autosaves, { author: authorId } ); -} - -/** - * Returns true if the REST request for autosaves has completed. - * - * @param {Object} state State tree. - * @param {string} postType The type of the parent post. - * @param {number} postId The id of the parent post. - * - * @return {boolean} True if the REST request was completed. False otherwise. - */ -export const hasFetchedAutosaves = createRegistrySelector( - ( select ) => ( state, postType, postId ) => { - return select( STORE_NAME ).hasFinishedResolution( 'getAutosaves', [ - postType, - postId, - ] ); - } -); - -/** - * Returns a new reference when edited values have changed. This is useful in - * inferring where an edit has been made between states by comparison of the - * return values using strict equality. - * - * @example - * - * ``` - * const hasEditOccurred = ( - * getReferenceByDistinctEdits( beforeState ) !== - * getReferenceByDistinctEdits( afterState ) - * ); - * ``` - * - * @param {Object} state Editor state. - * - * @return {*} A value whose reference will change only when an edit occurs. - */ -export const getReferenceByDistinctEdits = createSelector( - () => [], - ( state ) => [ - state.undo.length, - state.undo.offset, - state.undo.flattenedUndo, - ] -); - -/** - * Retrieve the frontend template used for a given link. - * - * @param {Object} state Editor state. - * @param {string} link Link. - * - * @return {Object?} The template record. - */ -export function __experimentalGetTemplateForLink( state, link ) { - const records = getEntityRecords( state, 'postType', 'wp_template', { - 'find-template': link, - } ); - - const template = records?.length ? records[ 0 ] : null; - if ( template ) { - return getEditedEntityRecord( - state, - 'postType', - 'wp_template', - template.id - ); - } - return template; -} - -/** - * Retrieve the current theme's base global styles - * - * @param {Object} state Editor state. - * - * @return {Object?} The Global Styles object. - */ -export function __experimentalGetCurrentThemeBaseGlobalStyles( state ) { - const currentTheme = getCurrentTheme( state ); - if ( ! currentTheme ) { - return null; - } - return state.themeBaseGlobalStyles[ currentTheme.stylesheet ]; -} - -/** - * Return the ID of the current global styles object. - * - * @param {Object} state Data state. - * - * @return {string} The current global styles ID. - */ -export function __experimentalGetCurrentThemeGlobalStylesVariations( state ) { - const currentTheme = getCurrentTheme( state ); - if ( ! currentTheme ) { - return null; - } - return state.themeGlobalStyleVariations[ currentTheme.stylesheet ]; -}