UNPKG

@wordpress/core-data

Version:
478 lines (455 loc) 11.6 kB
/** * External dependencies */ import { capitalCase, pascalCase } from 'change-case'; /** * WordPress dependencies */ import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; import { RichTextData } from '@wordpress/rich-text'; export const DEFAULT_ENTITY_KEY = 'id'; const POST_RAW_ATTRIBUTES = [ 'title', 'excerpt', 'content' ]; export const rootEntitiesConfig = [ { label: __( 'Base' ), kind: 'root', name: '__unstableBase', baseURL: '/', baseURLParams: { // Please also change the preload path when changing this. // @see lib/compat/wordpress-6.8/preload.php _fields: [ 'description', 'gmt_offset', 'home', 'name', 'site_icon', 'site_icon_url', 'site_logo', 'timezone_string', 'url', 'page_for_posts', 'page_on_front', 'show_on_front', ].join( ',' ), }, // The entity doesn't support selecting multiple records. // The property is maintained for backward compatibility. plural: '__unstableBases', syncConfig: { fetch: async () => { return apiFetch( { path: '/' } ); }, applyChangesToDoc: ( doc, changes ) => { const document = doc.getMap( 'document' ); Object.entries( changes ).forEach( ( [ key, value ] ) => { if ( document.get( key ) !== value ) { document.set( key, value ); } } ); }, fromCRDTDoc: ( doc ) => { return doc.getMap( 'document' ).toJSON(); }, }, syncObjectType: 'root/base', getSyncObjectId: () => 'index', }, { label: __( 'Post Type' ), name: 'postType', kind: 'root', key: 'slug', baseURL: '/wp/v2/types', baseURLParams: { context: 'edit' }, plural: 'postTypes', syncConfig: { fetch: async ( id ) => { return apiFetch( { path: `/wp/v2/types/${ id }?context=edit`, } ); }, applyChangesToDoc: ( doc, changes ) => { const document = doc.getMap( 'document' ); Object.entries( changes ).forEach( ( [ key, value ] ) => { if ( document.get( key ) !== value ) { document.set( key, value ); } } ); }, fromCRDTDoc: ( doc ) => { return doc.getMap( 'document' ).toJSON(); }, }, syncObjectType: 'root/postType', getSyncObjectId: ( id ) => id, }, { name: 'media', kind: 'root', baseURL: '/wp/v2/media', baseURLParams: { context: 'edit' }, plural: 'mediaItems', label: __( 'Media' ), rawAttributes: [ 'caption', 'title', 'description' ], supportsPagination: true, }, { 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', baseURLParams: { context: 'edit' }, 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', getTitle: ( record ) => record?.name || record?.slug, baseURLParams: { context: 'edit' }, plural: 'users', supportsPagination: true, }, { name: 'comment', kind: 'root', baseURL: '/wp/v2/comments', baseURLParams: { context: 'edit' }, plural: 'comments', label: __( 'Comment' ), supportsPagination: true, }, { name: 'menu', kind: 'root', baseURL: '/wp/v2/menus', baseURLParams: { context: 'edit' }, plural: 'menus', label: __( 'Menu' ), supportsPagination: true, }, { name: 'menuItem', kind: 'root', baseURL: '/wp/v2/menu-items', baseURLParams: { context: 'edit' }, plural: 'menuItems', label: __( 'Menu Item' ), rawAttributes: [ 'title' ], supportsPagination: true, }, { name: 'menuLocation', kind: 'root', baseURL: '/wp/v2/menu-locations', baseURLParams: { context: 'edit' }, plural: 'menuLocations', label: __( 'Menu Location' ), key: 'name', }, { label: __( 'Global Styles' ), name: 'globalStyles', kind: 'root', baseURL: '/wp/v2/global-styles', baseURLParams: { context: 'edit' }, plural: 'globalStylesVariations', // Should be different from name. getTitle: () => __( 'Custom Styles' ), getRevisionsUrl: ( parentId, revisionId ) => `/wp/v2/global-styles/${ parentId }/revisions${ revisionId ? '/' + revisionId : '' }`, supportsPagination: true, }, { label: __( 'Themes' ), name: 'theme', kind: 'root', baseURL: '/wp/v2/themes', baseURLParams: { context: 'edit' }, plural: 'themes', key: 'stylesheet', }, { label: __( 'Plugins' ), name: 'plugin', kind: 'root', baseURL: '/wp/v2/plugins', baseURLParams: { context: 'edit' }, plural: 'plugins', key: 'plugin', }, { label: __( 'Status' ), name: 'status', kind: 'root', baseURL: '/wp/v2/statuses', baseURLParams: { context: 'edit' }, plural: 'statuses', key: 'slug', }, ]; export const deprecatedEntities = { root: { media: { since: '6.9', alternative: { kind: 'postType', name: 'attachment', }, }, }, }; export const additionalEntityConfigLoaders = [ { kind: 'postType', loadEntities: loadPostTypeEntities }, { kind: 'taxonomy', loadEntities: loadTaxonomyEntities }, { kind: 'root', name: 'site', plural: 'sites', loadEntities: loadSiteEntity, }, ]; /** * 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; }; const serialisableBlocksCache = new WeakMap(); function makeBlockAttributesSerializable( attributes ) { const newAttributes = { ...attributes }; for ( const [ key, value ] of Object.entries( attributes ) ) { if ( value instanceof RichTextData ) { newAttributes[ key ] = value.valueOf(); } } return newAttributes; } function makeBlocksSerializable( blocks ) { return blocks.map( ( block ) => { const { innerBlocks, attributes, ...rest } = block; return { ...rest, attributes: makeBlockAttributesSerializable( attributes ), innerBlocks: makeBlocksSerializable( innerBlocks ), }; } ); } /** * 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 Object.entries( postTypes ?? {} ).map( ( [ name, postType ] ) => { 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 ? capitalCase( record.slug ?? '' ) : String( record.id ) ), __unstablePrePersist: isTemplate ? undefined : prePersistPostType, __unstable_rest_base: postType.rest_base, syncConfig: { fetch: async ( id ) => { return apiFetch( { path: `/${ namespace }/${ postType.rest_base }/${ id }?context=edit`, } ); }, applyChangesToDoc: ( doc, changes ) => { const document = doc.getMap( 'document' ); Object.entries( changes ).forEach( ( [ key, value ] ) => { if ( typeof value !== 'function' ) { if ( key === 'blocks' ) { if ( ! serialisableBlocksCache.has( value ) ) { serialisableBlocksCache.set( value, makeBlocksSerializable( value ) ); } value = serialisableBlocksCache.get( value ); } if ( document.get( key ) !== value ) { document.set( key, value ); } } } ); }, fromCRDTDoc: ( doc ) => { return doc.getMap( 'document' ).toJSON(); }, }, syncObjectType: 'postType/' + postType.name, getSyncObjectId: ( id ) => id, supportsPagination: true, getRevisionsUrl: ( parentId, revisionId ) => `/${ namespace }/${ postType.rest_base }/${ parentId }/revisions${ revisionId ? '/' + revisionId : '' }`, revisionKey: DEFAULT_ENTITY_KEY, }; } ); } /** * 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 Object.entries( taxonomies ?? {} ).map( ( [ name, taxonomy ] ) => { const namespace = taxonomy?.rest_namespace ?? 'wp/v2'; return { kind: 'taxonomy', baseURL: `/${ namespace }/${ taxonomy.rest_base }`, baseURLParams: { context: 'edit' }, name, label: taxonomy.name, getTitle: ( record ) => record?.name, supportsPagination: true, }; } ); } /** * Returns the Site entity. * * @return {Promise} Entity promise */ async function loadSiteEntity() { const entity = { label: __( 'Site' ), name: 'site', kind: 'root', baseURL: '/wp/v2/settings', syncConfig: { fetch: async () => { return apiFetch( { path: '/wp/v2/settings' } ); }, applyChangesToDoc: ( doc, changes ) => { const document = doc.getMap( 'document' ); Object.entries( changes ).forEach( ( [ key, value ] ) => { if ( document.get( key ) !== value ) { document.set( key, value ); } } ); }, fromCRDTDoc: ( doc ) => { return doc.getMap( 'document' ).toJSON(); }, }, syncObjectType: 'root/site', getSyncObjectId: () => 'index', meta: {}, }; const site = await apiFetch( { path: entity.baseURL, method: 'OPTIONS', } ); const labels = {}; Object.entries( site?.schema?.properties ?? {} ).forEach( ( [ key, value ] ) => { // Ignore properties `title` and `type` keys. if ( typeof value === 'object' && value.title ) { labels[ key ] = value.title; } } ); return [ { ...entity, meta: { labels } } ]; } /** * Returns the entity's getter method name given its kind and name or plural name. * * @example * ```js * const nameSingular = getMethodName( 'root', 'theme', 'get' ); * // nameSingular is getRootTheme * * const namePlural = getMethodName( 'root', 'themes', 'set' ); * // namePlural is setRootThemes * ``` * * @param {string} kind Entity kind. * @param {string} name Entity name or plural name. * @param {string} prefix Function prefix. * * @return {string} Method name */ export const getMethodName = ( kind, name, prefix = 'get' ) => { const kindPrefix = kind === 'root' ? '' : pascalCase( kind ); const suffix = pascalCase( name ); return `${ prefix }${ kindPrefix }${ suffix }`; };