UNPKG

@wordpress/core-data

Version:
312 lines (290 loc) 8.49 kB
/** * WordPress dependencies */ import { createSelector, createRegistrySelector } from '@wordpress/data'; /** * Internal dependencies */ 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; /** * Returns the previous edit from the current undo offset * for the entity records edits history, if any. * * Known Issue: Every-time state.undoManager changes, the getUndoManager * private selector is called (if used within useSelect and things like that) * which ensures the UI is always properly reactive. But, it's not the case with * the custom "sync" undo manager. * * Assumption: When an undo/redo is created, other parts of the core-data state * are likely changing simultaneously, which will trigger the selectors again. * * This issue is acceptable based on the assumption above. * * @see https://github.com/WordPress/gutenberg/pull/72407/files#r2580214235 for more details. * * @param state State tree. * * @return The undo manager. */ export function getUndoManager( state: State ) { if ( window.__experimentalEnableSync ) { if ( globalThis.IS_GUTENBERG_PLUGIN ) { return getSyncManager()?.undoManager ?? state.undoManager; } } return 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', } ); // Still resolving getDefaultTemplateId. if ( ! frontPageTemplateId ) { return null; } return { postType: 'wp_template', postId: frontPageTemplateId }; }, ( 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; }