UNPKG

@wordpress/core-data

Version:
392 lines (363 loc) 10.5 kB
import { createSelector, createRegistrySelector } from '@wordpress/data'; import type { ConnectionStatus } from '@wordpress/sync'; import { getDefaultTemplateId, getEntityRecord, type State } from './selectors'; import { STORE_NAME } from './name'; import { unlock } from './lock-unlock'; import { getSyncManager } from './sync'; import logEntityDeprecation from './utils/log-entity-deprecation'; type EntityRecordKey = string | number; const EMPTY_OBJECT = {}; /** * Returns the previous edit from the current undo offset * for the entity records edits history, if any. * * @param state State tree. * * @return The undo manager. */ export function getUndoManager( state: State ) { // undoManager is undefined until the first sync-enabled entity is loaded. return getSyncManager()?.undoManager ?? state.undoManager; } /** * Retrieve the fallback Navigation. * * @param state Data state. * @return The ID for the fallback Navigation post. */ export function getNavigationFallbackId( state: State ): EntityRecordKey | undefined { return state.navigationFallbackId; } export const getBlockPatternsForPostType = createRegistrySelector( ( select: any ) => createSelector( ( state, postType ) => select( STORE_NAME ) .getBlockPatterns() .filter( ( { postTypes } ) => ! postTypes || ( Array.isArray( postTypes ) && postTypes.includes( postType ) ) ), () => [ select( STORE_NAME ).getBlockPatterns() ] ) ); /** * Returns the entity records permissions for the given entity record ids. */ export const getEntityRecordsPermissions = createRegistrySelector( ( select ) => createSelector( ( state: State, kind: string, name: string, ids: string | string[] ) => { const normalizedIds = Array.isArray( ids ) ? ids : [ ids ]; return normalizedIds.map( ( id ) => ( { delete: select( STORE_NAME ).canUser( 'delete', { kind, name, id, } ), update: select( STORE_NAME ).canUser( 'update', { kind, name, id, } ), } ) ); }, ( state ) => [ state.userPermissions ] ) ); /** * Returns the entity record permissions for the given entity record id. * * @param state Data state. * @param kind Entity kind. * @param name Entity name. * @param id Entity record id. * * @return The entity record permissions. */ export function getEntityRecordPermissions( state: State, kind: string, name: string, id: string ) { logEntityDeprecation( kind, name, 'getEntityRecordPermissions' ); return getEntityRecordsPermissions( state, kind, name, id )[ 0 ]; } /** * Returns the registered post meta fields for a given post type. * * @param state Data state. * @param postType Post type. * * @return Registered post meta fields. */ export function getRegisteredPostMeta( state: State, postType: string ) { return state.registeredPostMeta?.[ postType ] ?? {}; } function normalizePageId( value: number | string | undefined ): string | null { if ( ! value || ! [ 'number', 'string' ].includes( typeof value ) ) { return null; } // We also need to check if it's not zero (`'0'`). if ( Number( value ) === 0 ) { return null; } return value.toString(); } interface SiteData { show_on_front?: string; page_on_front?: string | number; page_for_posts?: string | number; } export const getHomePage = createRegistrySelector( ( select ) => createSelector( () => { const siteData = select( STORE_NAME ).getEntityRecord( 'root', '__unstableBase' ) as SiteData | undefined; // Still resolving getEntityRecord. if ( ! siteData ) { return null; } const homepageId = siteData?.show_on_front === 'page' ? normalizePageId( siteData.page_on_front ) : null; if ( homepageId ) { return { postType: 'page', postId: homepageId }; } const frontPageTemplateId = select( STORE_NAME ).getDefaultTemplateId( { slug: 'front-page', } ); if ( frontPageTemplateId ) { return { postType: 'wp_template', postId: frontPageTemplateId, }; } // Resolution is finished and no front-page template exists. if ( frontPageTemplateId === '' ) { return EMPTY_OBJECT; } // Still resolving getDefaultTemplateId. return null; }, ( state ) => [ // Even though getDefaultTemplateId.shouldInvalidate returns true when root/site changes, // it doesn't seem to invalidate this cache, I'm not sure why. getEntityRecord( state, 'root', 'site' ), getEntityRecord( state, 'root', '__unstableBase' ), getDefaultTemplateId( state, { slug: 'front-page', } ), ] ) ); export const getPostsPageId = createRegistrySelector( ( select ) => () => { const siteData = select( STORE_NAME ).getEntityRecord( 'root', '__unstableBase' ) as SiteData | undefined; return siteData?.show_on_front === 'page' ? normalizePageId( siteData.page_for_posts ) : null; } ); export const getTemplateId = createRegistrySelector( ( select ) => ( state, postType, postId ) => { const homepage = unlock( select( STORE_NAME ) ).getHomePage(); if ( ! homepage ) { return; } // For the front page, we always use the front page template if existing. if ( postType === 'page' && postType === homepage?.postType && postId.toString() === homepage?.postId ) { // The /lookup endpoint cannot currently handle a lookup // when a page is set as the front page, so specifically in // that case, we want to check if there is a front page // template, and instead of falling back to the home // template, we want to fall back to the page template. const templates = select( STORE_NAME ).getEntityRecords( 'postType', 'wp_template', { per_page: -1, } ); if ( ! templates ) { return; } const id = templates.find( ( { slug } ) => slug === 'front-page' ) ?.id; if ( id ) { return id; } // If no front page template is found, continue with the // logic below (fetching the page template). } const editedEntity = select( STORE_NAME ).getEditedEntityRecord( 'postType', postType, postId ); if ( ! editedEntity ) { return; } const postsPageId = unlock( select( STORE_NAME ) ).getPostsPageId(); // Check if the current page is the posts page. if ( postType === 'page' && postsPageId === postId.toString() ) { return select( STORE_NAME ).getDefaultTemplateId( { slug: 'home', } ); } // First see if the post/page has an assigned template and fetch it. const currentTemplateSlug = editedEntity.template; if ( currentTemplateSlug ) { const currentTemplate = select( STORE_NAME ) .getEntityRecords( 'postType', 'wp_template', { per_page: -1, } ) ?.find( ( { slug } ) => slug === currentTemplateSlug ); if ( currentTemplate ) { return currentTemplate.id; } } // If no template is assigned, use the default template. let slugToCheck; // In `draft` status we might not have a slug available, so we use the `single` // post type templates slug(ex page, single-post, single-product etc..). // Pages do not need the `single` prefix in the slug to be prioritized // through template hierarchy. if ( editedEntity.slug ) { slugToCheck = postType === 'page' ? `${ postType }-${ editedEntity.slug }` : `single-${ postType }-${ editedEntity.slug }`; } else { slugToCheck = postType === 'page' ? 'page' : `single-${ postType }`; } return select( STORE_NAME ).getDefaultTemplateId( { slug: slugToCheck, } ); } ); /** * Returns the editor settings. * * @param state Data state. * @return Editor settings object or null if not loaded. */ export function getEditorSettings( state: State ): Record< string, any > | null { return state.editorSettings; } /** * Returns the editor assets. * * @param state Data state. * @return Editor assets object or null if not loaded. */ export function getEditorAssets( state: State ): Record< string, any > | null { return state.editorAssets; } /** * Returns whether collaboration is supported. * * @param state Data state. * @return Whether collaboration is supported. */ export function isCollaborationSupported( state: State ): boolean { return state.collaborationSupported; } /** * Returns the view configuration for the given entity type. * * An optional fourth argument (e.g. `{ fields }`) may be passed when selecting; * it is consumed by the `getViewConfig` resolver to request a subset of the * config via the REST API `_fields` parameter and does not affect what is read * here. Partial responses are merged in the reducer, so the returned object may * accumulate properties across requests for the same entity. * * @param state Data state. * @param kind Entity kind. * @param name Entity name. * * @return The view configuration or undefined if not loaded. */ export function getViewConfig( state: State, kind: string, name: string ): Record< string, any > | undefined { return ( state.viewConfigs?.[ `${ kind }/${ name }` ] ?? { default_view: undefined, default_layouts: undefined, view_list: undefined, form: undefined, } ); } /** * Returns the current sync connection status across all entities. Prioritizes * disconnected states, then connecting, then connected. * * @param state Data state. * * @return The current sync connection state, prioritized by importance. */ export function getSyncConnectionStatus( state: State ): ConnectionStatus | undefined { if ( ! state.syncConnectionStatuses ) { return undefined; } const PRIORITIZED_STATUSES = [ 'disconnected', 'connecting', 'connected' ]; let coalesced: ConnectionStatus | undefined; for ( const status of Object.values( state.syncConnectionStatuses ) ) { if ( ! coalesced || PRIORITIZED_STATUSES.indexOf( status.status ) < PRIORITIZED_STATUSES.indexOf( coalesced.status ) ) { coalesced = status; } } return coalesced; } /** * Returns the sync connection status for a single entity, or undefined if * the entity is not being synced or no provider has reported a status yet. * * @param state Data state. * @param kind Entity kind. * @param name Entity name. * @param recordId Record ID. * * @return The sync connection status for the entity. */ export function getEntitySyncConnectionStatus( state: State, kind: string, name: string, recordId: EntityRecordKey ): ConnectionStatus | undefined { return state.syncConnectionStatuses?.[ `${ kind }/${ name }:${ recordId }` ]; }