UNPKG

@wordpress/core-data

Version:
780 lines (779 loc) 24.4 kB
// packages/core-data/src/resolvers.js import { camelCase } from "change-case"; import { addQueryArgs } from "@wordpress/url"; import { decodeEntities } from "@wordpress/html-entities"; import apiFetch from "@wordpress/api-fetch"; import { STORE_NAME } from "./name"; import { additionalEntityConfigLoaders, DEFAULT_ENTITY_KEY } from "./entities"; import { getSyncManager } from "./sync"; import { forwardResolver, getNormalizedCommaSeparable, getUserPermissionCacheKey, getUserPermissionsFromAllowHeader, ALLOWED_RESOURCE_ACTIONS, RECEIVE_INTERMEDIATE_RESULTS, isNumericID } from "./utils"; import { fetchBlockPatterns } from "./fetch"; var getAuthors = (query) => async ({ dispatch }) => { const path = addQueryArgs( "/wp/v2/users/?who=authors&per_page=100", query ); const users = await apiFetch({ path }); dispatch.receiveUserQuery(path, users); }; var getCurrentUser = () => async ({ dispatch }) => { const currentUser = await apiFetch({ path: "/wp/v2/users/me" }); dispatch.receiveCurrentUser(currentUser); }; var getEntityRecord = (kind, name, key = "", query) => async ({ select, dispatch, registry, resolveSelect }) => { const configs = await resolveSelect.getEntitiesConfig(kind); const entityConfig = configs.find( (config) => config.name === name && config.kind === kind ); if (!entityConfig) { return; } const lock = await dispatch.__unstableAcquireStoreLock( STORE_NAME, ["entities", "records", kind, name, key], { exclusive: false } ); try { if (query !== void 0 && query._fields) { query = { ...query, _fields: [ .../* @__PURE__ */ new Set([ ...getNormalizedCommaSeparable(query._fields) || [], entityConfig.key || DEFAULT_ENTITY_KEY ]) ].join() }; } if (query !== void 0 && query._fields) { const hasRecord = select.hasEntityRecord( kind, name, key, query ); if (hasRecord) { return; } } let { baseURL } = entityConfig; if (kind === "postType" && name === "wp_template" && (key && typeof key === "string" && !/^\d+$/.test(key) || !window?.__experimentalTemplateActivate)) { baseURL = baseURL.slice(0, baseURL.lastIndexOf("/")) + "/templates"; } const path = addQueryArgs(baseURL + (key ? "/" + key : ""), { ...entityConfig.baseURLParams, ...query }); const response = await apiFetch({ path, parse: false }); const record = await response.json(); const permissions = getUserPermissionsFromAllowHeader( response.headers?.get("allow") ); const canUserResolutionsArgs = []; const receiveUserPermissionArgs = {}; for (const action of ALLOWED_RESOURCE_ACTIONS) { receiveUserPermissionArgs[getUserPermissionCacheKey(action, { kind, name, id: key })] = permissions[action]; canUserResolutionsArgs.push([ action, { kind, name, id: key } ]); } if (window.__experimentalEnableSync && entityConfig.syncConfig && isNumericID(key) && !query) { if (globalThis.IS_GUTENBERG_PLUGIN) { const objectType = `${kind}/${name}`; const objectId = key; const recordWithTransients = { ...record }; Object.entries(entityConfig.transientEdits ?? {}).filter( ([propName, transientConfig]) => void 0 === recordWithTransients[propName] && transientConfig && "object" === typeof transientConfig && "read" in transientConfig && "function" === typeof transientConfig.read ).forEach(([propName, transientConfig]) => { recordWithTransients[propName] = transientConfig.read(recordWithTransients); }); await getSyncManager()?.load( entityConfig.syncConfig, objectType, objectId, recordWithTransients, { // Handle edits sourced from the sync manager. editRecord: (edits) => { if (!Object.keys(edits).length) { return; } dispatch({ type: "EDIT_ENTITY_RECORD", kind, name, recordId: key, edits, meta: { undo: void 0 } }); }, // Get the current entity record (with edits) getEditedRecord: async () => await resolveSelect.getEditedEntityRecord( kind, name, key ), // Save the current entity record's unsaved edits. saveRecord: () => { dispatch.saveEditedEntityRecord( kind, name, key ); } } ); } } registry.batch(() => { dispatch.receiveEntityRecords(kind, name, record, query); dispatch.receiveUserPermissions(receiveUserPermissionArgs); dispatch.finishResolutions("canUser", canUserResolutionsArgs); }); } finally { dispatch.__unstableReleaseStoreLock(lock); } }; getEntityRecord.shouldInvalidate = (action, kind, name) => { return kind === "root" && name === "site" && (action.type === "RECEIVE_ITEMS" && // Making sure persistedEdits is set seems to be the only way of // knowing whether it's an update or fetch. Only an update would // have persistedEdits. action.persistedEdits && action.persistedEdits.status !== "auto-draft" || action.type === "REMOVE_ITEMS") && action.kind === "postType" && action.name === "wp_template"; }; var getRawEntityRecord = forwardResolver("getEntityRecord"); var getEditedEntityRecord = forwardResolver("getEntityRecord"); var getEntityRecords = (kind, name, query = {}) => async ({ dispatch, registry, resolveSelect }) => { const configs = await resolveSelect.getEntitiesConfig(kind); const entityConfig = configs.find( (config) => config.name === name && config.kind === kind ); if (!entityConfig) { return; } const lock = await dispatch.__unstableAcquireStoreLock( STORE_NAME, ["entities", "records", kind, name], { exclusive: false } ); const rawQuery = { ...query }; const key = entityConfig.key || DEFAULT_ENTITY_KEY; function getResolutionsArgs(records, recordsQuery) { const queryArgs = Object.fromEntries( Object.entries(recordsQuery).filter(([k, v]) => { return ["context", "_fields"].includes(k) && !!v; }) ); return records.filter((record) => record?.[key]).map((record) => [ kind, name, record[key], Object.keys(queryArgs).length > 0 ? queryArgs : void 0 ]); } try { if (query._fields) { query = { ...query, _fields: [ .../* @__PURE__ */ new Set([ ...getNormalizedCommaSeparable(query._fields) || [], key ]) ].join() }; } let { baseURL } = entityConfig; const { combinedTemplates = true } = query; if (kind === "postType" && name === "wp_template" && combinedTemplates) { baseURL = baseURL.slice(0, baseURL.lastIndexOf("/")) + "/templates"; } const path = addQueryArgs(baseURL, { ...entityConfig.baseURLParams, ...query }); let records = [], meta; if (entityConfig.supportsPagination && query.per_page !== -1) { const response = await apiFetch({ path, parse: false }); records = Object.values(await response.json()); meta = { totalItems: parseInt( response.headers.get("X-WP-Total") ), totalPages: parseInt( response.headers.get("X-WP-TotalPages") ) }; } else if (query.per_page === -1 && query[RECEIVE_INTERMEDIATE_RESULTS] === true) { let page = 1; let totalPages; do { const response = await apiFetch({ path: addQueryArgs(path, { page, per_page: 100 }), parse: false }); const pageRecords = Object.values(await response.json()); totalPages = parseInt( response.headers.get("X-WP-TotalPages") ); if (!meta) { meta = { totalItems: parseInt( response.headers.get("X-WP-Total") ), totalPages: 1 }; } records.push(...pageRecords); registry.batch(() => { dispatch.receiveEntityRecords( kind, name, records, query, false, void 0, meta ); dispatch.finishResolutions( "getEntityRecord", getResolutionsArgs(pageRecords, rawQuery) ); }); page++; } while (page <= totalPages); } else { records = Object.values(await apiFetch({ path })); meta = { totalItems: records.length, totalPages: 1 }; } if (query._fields) { records = records.map((record) => { query._fields.split(",").forEach((field) => { if (!record.hasOwnProperty(field)) { record[field] = void 0; } }); return record; }); } registry.batch(() => { dispatch.receiveEntityRecords( kind, name, records, query, false, void 0, meta ); const targetHints = records.filter( (record) => !!record?.[key] && !!record?._links?.self?.[0]?.targetHints?.allow ).map((record) => ({ id: record[key], permissions: getUserPermissionsFromAllowHeader( record._links.self[0].targetHints.allow ) })); const canUserResolutionsArgs = []; const receiveUserPermissionArgs = {}; for (const targetHint of targetHints) { for (const action of ALLOWED_RESOURCE_ACTIONS) { canUserResolutionsArgs.push([ action, { kind, name, id: targetHint.id } ]); receiveUserPermissionArgs[getUserPermissionCacheKey(action, { kind, name, id: targetHint.id })] = targetHint.permissions[action]; } } if (targetHints.length > 0) { dispatch.receiveUserPermissions( receiveUserPermissionArgs ); dispatch.finishResolutions( "canUser", canUserResolutionsArgs ); } dispatch.finishResolutions( "getEntityRecord", getResolutionsArgs(records, rawQuery) ); dispatch.__unstableReleaseStoreLock(lock); }); } catch (e) { dispatch.__unstableReleaseStoreLock(lock); } }; getEntityRecords.shouldInvalidate = (action, kind, name) => { return (action.type === "RECEIVE_ITEMS" || action.type === "REMOVE_ITEMS") && action.invalidateCache && kind === action.kind && name === action.name; }; var getEntityRecordsTotalItems = forwardResolver("getEntityRecords"); var getEntityRecordsTotalPages = forwardResolver("getEntityRecords"); var getCurrentTheme = () => async ({ dispatch, resolveSelect }) => { const activeThemes = await resolveSelect.getEntityRecords( "root", "theme", { status: "active" } ); dispatch.receiveCurrentTheme(activeThemes[0]); }; var getThemeSupports = forwardResolver("getCurrentTheme"); var getEmbedPreview = (url) => async ({ dispatch }) => { try { const embedProxyResponse = await apiFetch({ path: addQueryArgs("/oembed/1.0/proxy", { url }) }); dispatch.receiveEmbedPreview(url, embedProxyResponse); } catch (error) { dispatch.receiveEmbedPreview(url, false); } }; var canUser = (requestedAction, resource, id) => async ({ dispatch, registry, resolveSelect }) => { if (!ALLOWED_RESOURCE_ACTIONS.includes(requestedAction)) { throw new Error(`'${requestedAction}' is not a valid action.`); } const { hasStartedResolution } = registry.select(STORE_NAME); for (const relatedAction of ALLOWED_RESOURCE_ACTIONS) { if (relatedAction === requestedAction) { continue; } const isAlreadyResolving = hasStartedResolution("canUser", [ relatedAction, resource, id ]); if (isAlreadyResolving) { return; } } let resourcePath = null; if (typeof resource === "object") { if (!resource.kind || !resource.name) { throw new Error("The entity resource object is not valid."); } const configs = await resolveSelect.getEntitiesConfig( resource.kind ); const entityConfig = configs.find( (config) => config.name === resource.name && config.kind === resource.kind ); if (!entityConfig) { return; } resourcePath = entityConfig.baseURL + (resource.id ? "/" + resource.id : ""); } else { resourcePath = `/wp/v2/${resource}` + (id ? "/" + id : ""); } let response; try { response = await apiFetch({ path: resourcePath, method: "OPTIONS", parse: false }); } catch (error) { return; } const permissions = getUserPermissionsFromAllowHeader( response.headers?.get("allow") ); registry.batch(() => { for (const action of ALLOWED_RESOURCE_ACTIONS) { const key = getUserPermissionCacheKey(action, resource, id); dispatch.receiveUserPermission(key, permissions[action]); if (action !== requestedAction) { dispatch.finishResolution("canUser", [ action, resource, id ]); } } }); }; var canUserEditEntityRecord = (kind, name, recordId) => async ({ dispatch }) => { await dispatch(canUser("update", { kind, name, id: recordId })); }; var getAutosaves = (postType, postId) => async ({ dispatch, resolveSelect }) => { const { rest_base: restBase, rest_namespace: restNamespace = "wp/v2", supports } = await resolveSelect.getPostType(postType); if (!supports?.autosave) { return; } const autosaves = await apiFetch({ path: `/${restNamespace}/${restBase}/${postId}/autosaves?context=edit` }); if (autosaves && autosaves.length) { dispatch.receiveAutosaves(postId, autosaves); } }; var getAutosave = (postType, postId) => async ({ resolveSelect }) => { await resolveSelect.getAutosaves(postType, postId); }; var __experimentalGetCurrentGlobalStylesId = () => async ({ dispatch, resolveSelect }) => { const activeThemes = await resolveSelect.getEntityRecords( "root", "theme", { status: "active" } ); const globalStylesURL = activeThemes?.[0]?._links?.["wp:user-global-styles"]?.[0]?.href; if (!globalStylesURL) { return; } const matches = globalStylesURL.match(/\/(\d+)(?:\?|$)/); const id = matches ? Number(matches[1]) : null; if (id) { dispatch.__experimentalReceiveCurrentGlobalStylesId(id); } }; var __experimentalGetCurrentThemeBaseGlobalStyles = () => async ({ resolveSelect, dispatch }) => { const currentTheme = await resolveSelect.getCurrentTheme(); const themeGlobalStyles = await apiFetch({ path: `/wp/v2/global-styles/themes/${currentTheme.stylesheet}?context=view` }); dispatch.__experimentalReceiveThemeBaseGlobalStyles( currentTheme.stylesheet, themeGlobalStyles ); }; var __experimentalGetCurrentThemeGlobalStylesVariations = () => async ({ resolveSelect, dispatch }) => { const currentTheme = await resolveSelect.getCurrentTheme(); const variations = await apiFetch({ path: `/wp/v2/global-styles/themes/${currentTheme.stylesheet}/variations?context=view` }); dispatch.__experimentalReceiveThemeGlobalStyleVariations( currentTheme.stylesheet, variations ); }; var getCurrentThemeGlobalStylesRevisions = () => async ({ resolveSelect, dispatch }) => { const globalStylesId = await resolveSelect.__experimentalGetCurrentGlobalStylesId(); const record = globalStylesId ? await resolveSelect.getEntityRecord( "root", "globalStyles", globalStylesId ) : void 0; const revisionsURL = record?._links?.["version-history"]?.[0]?.href; if (revisionsURL) { const resetRevisions = await apiFetch({ url: revisionsURL }); const revisions = resetRevisions?.map( (revision) => Object.fromEntries( Object.entries(revision).map(([key, value]) => [ camelCase(key), value ]) ) ); dispatch.receiveThemeGlobalStyleRevisions( globalStylesId, revisions ); } }; getCurrentThemeGlobalStylesRevisions.shouldInvalidate = (action) => { return action.type === "SAVE_ENTITY_RECORD_FINISH" && action.kind === "root" && !action.error && action.name === "globalStyles"; }; var getBlockPatterns = () => async ({ dispatch }) => { const patterns = await fetchBlockPatterns(); dispatch({ type: "RECEIVE_BLOCK_PATTERNS", patterns }); }; var getBlockPatternCategories = () => async ({ dispatch }) => { const categories = await apiFetch({ path: "/wp/v2/block-patterns/categories" }); dispatch({ type: "RECEIVE_BLOCK_PATTERN_CATEGORIES", categories }); }; var getUserPatternCategories = () => async ({ dispatch, resolveSelect }) => { const patternCategories = await resolveSelect.getEntityRecords( "taxonomy", "wp_pattern_category", { per_page: -1, _fields: "id,name,description,slug", context: "view" } ); const mappedPatternCategories = patternCategories?.map((userCategory) => ({ ...userCategory, label: decodeEntities(userCategory.name), name: userCategory.slug })) || []; dispatch({ type: "RECEIVE_USER_PATTERN_CATEGORIES", patternCategories: mappedPatternCategories }); }; var getNavigationFallbackId = () => async ({ dispatch, select, registry }) => { const fallback = await apiFetch({ path: addQueryArgs("/wp-block-editor/v1/navigation-fallback", { _embed: true }) }); const record = fallback?._embedded?.self; registry.batch(() => { dispatch.receiveNavigationFallbackId(fallback?.id); if (!record) { return; } const existingFallbackEntityRecord = select.getEntityRecord( "postType", "wp_navigation", fallback.id ); const invalidateNavigationQueries = !existingFallbackEntityRecord; dispatch.receiveEntityRecords( "postType", "wp_navigation", record, void 0, invalidateNavigationQueries ); dispatch.finishResolution("getEntityRecord", [ "postType", "wp_navigation", fallback.id ]); }); }; var getDefaultTemplateId = (query) => async ({ dispatch, registry, resolveSelect }) => { const template = await apiFetch({ path: addQueryArgs("/wp/v2/templates/lookup", query) }); await resolveSelect.getEntitiesConfig("postType"); const id = window?.__experimentalTemplateActivate ? template?.wp_id || template?.id : template?.id; if (id) { template.id = id; registry.batch(() => { dispatch.receiveDefaultTemplateId(query, id); dispatch.receiveEntityRecords("postType", template.type, [ template ]); dispatch.finishResolution("getEntityRecord", [ "postType", template.type, id ]); }); } }; getDefaultTemplateId.shouldInvalidate = (action) => { return action.type === "RECEIVE_ITEMS" && action.kind === "root" && action.name === "site"; }; var getRevisions = (kind, name, recordKey, query = {}) => async ({ dispatch, registry, resolveSelect }) => { const configs = await resolveSelect.getEntitiesConfig(kind); const entityConfig = configs.find( (config) => config.name === name && config.kind === kind ); if (!entityConfig) { return; } if (query._fields) { query = { ...query, _fields: [ .../* @__PURE__ */ new Set([ ...getNormalizedCommaSeparable(query._fields) || [], entityConfig.revisionKey || DEFAULT_ENTITY_KEY ]) ].join() }; } const path = addQueryArgs( entityConfig.getRevisionsUrl(recordKey), query ); let records, response; const meta = {}; const isPaginated = entityConfig.supportsPagination && query.per_page !== -1; try { response = await apiFetch({ path, parse: !isPaginated }); } catch (error) { return; } if (response) { if (isPaginated) { records = Object.values(await response.json()); meta.totalItems = parseInt( response.headers.get("X-WP-Total") ); } else { records = Object.values(response); } if (query._fields) { records = records.map((record) => { query._fields.split(",").forEach((field) => { if (!record.hasOwnProperty(field)) { record[field] = void 0; } }); return record; }); } registry.batch(() => { dispatch.receiveRevisions( kind, name, recordKey, records, query, false, meta ); if (!query?._fields && !query.context) { const key = entityConfig.key || DEFAULT_ENTITY_KEY; const resolutionsArgs = records.filter((record) => record[key]).map((record) => [ kind, name, recordKey, record[key] ]); dispatch.finishResolutions( "getRevision", resolutionsArgs ); } }); } }; getRevisions.shouldInvalidate = (action, kind, name, recordKey) => action.type === "SAVE_ENTITY_RECORD_FINISH" && name === action.name && kind === action.kind && !action.error && recordKey === action.recordId; var getRevision = (kind, name, recordKey, revisionKey, query) => async ({ dispatch, resolveSelect }) => { const configs = await resolveSelect.getEntitiesConfig(kind); const entityConfig = configs.find( (config) => config.name === name && config.kind === kind ); if (!entityConfig) { return; } if (query !== void 0 && query._fields) { query = { ...query, _fields: [ .../* @__PURE__ */ new Set([ ...getNormalizedCommaSeparable(query._fields) || [], entityConfig.revisionKey || DEFAULT_ENTITY_KEY ]) ].join() }; } const path = addQueryArgs( entityConfig.getRevisionsUrl(recordKey, revisionKey), query ); let record; try { record = await apiFetch({ path }); } catch (error) { return; } if (record) { dispatch.receiveRevisions(kind, name, recordKey, record, query); } }; var getRegisteredPostMeta = (postType) => async ({ dispatch, resolveSelect }) => { let options; try { const { rest_namespace: restNamespace = "wp/v2", rest_base: restBase } = await resolveSelect.getPostType(postType) || {}; options = await apiFetch({ path: `${restNamespace}/${restBase}/?context=edit`, method: "OPTIONS" }); } catch (error) { return; } if (options) { dispatch.receiveRegisteredPostMeta( postType, options?.schema?.properties?.meta?.properties ); } }; var getEntitiesConfig = (kind) => async ({ dispatch }) => { const loader = additionalEntityConfigLoaders.find( (l) => l.kind === kind ); if (!loader) { return; } try { const configs = await loader.loadEntities(); if (!configs.length) { return; } dispatch.addEntities(configs); } catch { } }; var getEditorSettings = () => async ({ dispatch }) => { const settings = await apiFetch({ path: "/wp-block-editor/v1/settings" }); dispatch.receiveEditorSettings(settings); }; var getEditorAssets = () => async ({ dispatch }) => { const assets = await apiFetch({ path: "/wp-block-editor/v1/assets" }); dispatch.receiveEditorAssets(assets); }; export { __experimentalGetCurrentGlobalStylesId, __experimentalGetCurrentThemeBaseGlobalStyles, __experimentalGetCurrentThemeGlobalStylesVariations, canUser, canUserEditEntityRecord, getAuthors, getAutosave, getAutosaves, getBlockPatternCategories, getBlockPatterns, getCurrentTheme, getCurrentThemeGlobalStylesRevisions, getCurrentUser, getDefaultTemplateId, getEditedEntityRecord, getEditorAssets, getEditorSettings, getEmbedPreview, getEntitiesConfig, getEntityRecord, getEntityRecords, getEntityRecordsTotalItems, getEntityRecordsTotalPages, getNavigationFallbackId, getRawEntityRecord, getRegisteredPostMeta, getRevision, getRevisions, getThemeSupports, getUserPatternCategories }; //# sourceMappingURL=resolvers.js.map