@eightshift/frontend-libs
Version:
A collection of useful frontend utility modules. powered by Eightshift
797 lines (689 loc) • 28.5 kB
JavaScript
import React from 'react';
import { __ } from '@wordpress/i18n';
import { InnerBlocks } from '@wordpress/block-editor';
import { registerBlockType, registerBlockVariation } from '@wordpress/blocks';
import { dispatch, select } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
import { createElement } from '@wordpress/element';
import { getUnique } from './css-variables';
import { blockIcons } from '@eightshift/ui-components/icons';
import { STORE_NAME, setStoreGlobalWindow, setStore, setConfigFlags } from './store';
import { camelCase, kebabCase, lowerFirst, upperFirst } from '@eightshift/ui-components/utilities';
/**
* Register all Block Editor blocks using WP `registerBlockType` method.
* Due to restrictions in dynamic import using dynamic names all blocks are registered using `require.context`.
*
* @param {object} globalManifest - Must provide global blocks setting manifest.json.
* @param {function?} [wrapperComponent] - Callback function that returns a `Wrapper`.
* @param {object} wrapperManifest - `Wrapper` manifest.
* @param {function} componentsManifestPath - **Must provide `require.context` for all components `manifest.json`s.**
* @param {function} blocksManifestPath - **Must provide `require.context` for all blocks manifest.json-s.**
* @param {function} blocksEditComponentPath - **Must provide `require.context` for all blocks JavaScript files (unable to add only block edit file due to dynamic naming).**
* @param {function?} [hooksComponentPath] - Function of hooks JavaScript files in a block from `require.context`.
* @param {function?} [transformsComponentPath] - Function of transforms JavaScript files in a block from `require.context`.
* @param {function?} [deprecationsComponentPath] - Function of deprecations JavaScript files in a block from `require.context`.
* @param {function?} [overridesComponentPath] - Function of overrides JavaScript files in a block from `require.context`.
*
* @access public
*
* @returns {mixed}
*
* Usage:
* ```js
* registerBlocks(
* globalSettings,
* Wrapper,
* WrapperManifest,
* require.context('./../../components', true, /manifest.json$/),
* require.context('./../../custom', true, /manifest.json$/),
* require.context('./../../custom', true, /-block.js$/),
* require.context('./../../custom', true, /-hooks.js$/),
* require.context('./../../custom', true, /-transforms.js$/),
* require.context('./../../custom', true, /-deprecations.js$/),
* require.context('./../../custom', true, /-overrides.js$/),
* );
* ```
*/
export const registerBlocks = (globalManifest = {}, wrapperComponent = null, wrapperManifest = {}, componentsManifestPath, blocksManifestPath, blocksEditComponentPath, hooksComponentPath = null, transformsComponentPath = null, deprecationsComponentPath = null, overridesComponentPath = null) => {
const componentsManifest = componentsManifestPath.keys().map(componentsManifestPath);
const blocksManifests = blocksManifestPath.keys().map(blocksManifestPath);
// Set all store values.
setStore();
dispatch(STORE_NAME).setSettings(globalManifest);
dispatch(STORE_NAME).setBlocks(blocksManifests);
dispatch(STORE_NAME).setComponents(componentsManifest);
setConfigFlags();
if (select(STORE_NAME).getConfigUseWrapper()) {
dispatch(STORE_NAME).setWrapper(wrapperManifest);
}
setStoreGlobalWindow();
// Iterate blocks to register.
blocksManifests.forEach((blockManifestOriginal) => {
const { active = true } = blockManifestOriginal;
// If block has active key set to false the block will not show in the block editor.
if (active) {
let blockManifest = { ...blockManifestOriginal };
// Get Block edit component from block name and blocksEditComponentPath.
const blockComponent = getBlockEditComponent(blockManifest.blockName, blocksEditComponentPath, 'block');
// Get Block Transforms component from block name and transformsComponentPath.
if (transformsComponentPath !== null) {
const blockTransformsComponent = getBlockGenericComponent(blockManifest.blockName, transformsComponentPath, 'transforms');
if (blockTransformsComponent !== null) {
blockManifest = { ...blockManifest, transforms: blockTransformsComponent };
}
}
// Get Block Deprecations component from block name and deprecationsComponentPath.
if (deprecationsComponentPath !== null) {
const blockDeprecationsComponent = getBlockGenericComponent(blockManifest.blockName, deprecationsComponentPath, 'deprecations');
if (blockDeprecationsComponent !== null) {
blockManifest = { ...blockManifest, deprecated: blockDeprecationsComponent };
}
}
// Get Block Hooks component from block name and hooksComponentPath.
if (hooksComponentPath !== null) {
const blockHooksComponent = getBlockGenericComponent(blockManifest.blockName, hooksComponentPath, 'hooks');
if (blockHooksComponent !== null) {
blockHooksComponent();
}
}
// Get Block Overrides component from block name and overridesComponentPath.
if (overridesComponentPath !== null) {
const blockOverridesComponent = getBlockGenericComponent(blockManifest.blockName, overridesComponentPath, 'overrides');
if (blockOverridesComponent !== null) {
blockManifest = { ...blockManifest, ...blockOverridesComponent };
}
}
// Pass data to registerBlock helper to get final output for registerBlockType.
const blockDetails = registerBlock(globalManifest, wrapperManifest, componentsManifest, blockManifest, wrapperComponent, blockComponent);
// Format the 'deprecated' attribute details to match the format Gutenberg wants.
if (blockDetails?.options?.deprecated) {
// Compute once and let both the attributes spread and the migrate closure capture it.
const baseAttributes = getAttributes(globalManifest, wrapperManifest, componentsManifest, blockManifest);
blockDetails.options.deprecated = blockDetails.options.deprecated.map((deprecation) => {
if (deprecation?.attributes && deprecation?.migrate) {
return {
...deprecation,
isEligible: deprecation?.isEligible ?? (() => true),
save: blockDetails.options.save,
};
}
return {
attributes: {
...baseAttributes,
...deprecation.oldAttributes,
},
migrate: (attributes) => {
return {
...baseAttributes,
...attributes,
...deprecation.newAttributes(attributes),
};
},
isEligible: deprecation?.isEligible ?? ((attributes) => Object.keys(deprecation.oldAttributes).every((v) => Object.keys(attributes).includes(v))),
save: blockDetails.options.save,
};
});
}
// Native WP method for block registration.
registerBlockType(blockDetails.blockName, blockDetails.options);
}
});
// Add icon foreground and background colors as CSS variables for later use.
const { background: backgroundGlobal, foreground: foregroundGlobal } = globalManifest;
document.documentElement.style.setProperty('--es-admin-block-icon-foreground', foregroundGlobal);
document.documentElement.style.setProperty('--es-admin-block-icon-background', backgroundGlobal);
// Set all data to the dom that is necessary for project.
if (process.env.NODE_ENV !== 'test') {
// Require set like this because some import issue with jest unit tests.
const { blocksFilterHook } = require('./hooks');
addFilter('editor.BlockListBlock', `eightshift/${select(STORE_NAME).getSettingsNamespace()}`, blocksFilterHook);
}
};
/**
* Register all Variations Editor blocks using WP `registerBlockVariation` method.
* Due to restrictions in dynamic import using dynamic names all block are register using `require.context`.
*
* @param {object} globalManifest - **Must provide global blocks setting `manifest.json`.**
* @param {function} variationsManifestPath - **Must provide require.context for all variations `manifest.json`s.**
* @param {function} [blocksManifestPath] - **require.context for all blocks `manifest.json`s.**
* @param {function?} [overridesComponentPath] - Function of overrides JavaScript files in a block from `require.context`.
*
* @access public
*
* @returns {null}
*
* Usage:
* ```js
* registerVariations(
* globalSettings,
* require.context('./../../variations', true, /manifest.json$/),
* require.context('./../../custom', true, /manifest.json$/),
* require.context('./../../variations', true, /-overrides.js$/),
* );
* ```
*/
export const registerVariations = (globalManifest = {}, variationsManifestPath, blocksManifestPath = null, overridesComponentPath = null) => {
const variationsManifests = variationsManifestPath.keys().map(variationsManifestPath);
// Set all store values.
dispatch(STORE_NAME).setVariations(variationsManifests);
// Iterate variations to register.
variationsManifests.forEach((variationManifestOriginal) => {
const { active = true } = variationManifestOriginal;
// If variation has active key set to false the variation will not show in the block editor.
if (active) {
let variationManifest = { ...variationManifestOriginal };
// Get Block Overrides component from block name and overridesComponentPath.
if (overridesComponentPath !== null) {
const blockOverridesComponent = getBlockGenericComponent(variationManifest.name, overridesComponentPath, 'overrides');
if (blockOverridesComponent !== null) {
variationManifest = { ...variationManifest, ...blockOverridesComponent };
}
}
// Pass data to registerVariation helper to get final output for registerBlockVariation.
const blockDetails = registerVariation(globalManifest, variationManifest, blocksManifestPath !== null ? blocksManifestPath.keys().map(blocksManifestPath) : []);
// Native WP method for block registration.
registerBlockVariation(blockDetails.blockName, blockDetails.options);
}
});
};
//---------------------------------------------------------------
// Private methods
const getBlockComponent = (blockName, paths, fileName, required) => {
const component = paths
.keys()
.filter((filePath) => filePath === `./${blockName}/${blockName}-${fileName}.js`)
.map(paths)[0];
if (typeof component === 'undefined') {
if (!required) return null;
throw Error(`It looks like you are missing block edit component for block: ${blockName}, please check if you have ${blockName}-block.js file in your block folder.`);
}
const callback = component[Object.keys(component)[0]];
if (required && typeof callback === 'undefined') {
throw Error(`It looks like you are missing block edit component for block: ${blockName}, please check if you have ${blockName}-block.js file in your block folder.`);
}
return callback ?? null;
};
/**
* Filter array of JS paths and get the correct edit components.
*
* @param {string} blockName - Provided block name to find corresponding edit component.
* @param {function} paths - Function of all JavaScript files in a block got from require.context.
* @param {string} fileName - Block partial name.
*
* @access private
*
* @returns {function}
*
*/
export const getBlockEditComponent = (blockName, paths, fileName) => getBlockComponent(blockName, paths, fileName, true);
/**
* Filter array of JS paths and get the correct transforms, hooks, etc components.
*
* @param {string} blockName - Provided block name to find corresponding edit component.
* @param {function} paths - Function of all JavaScript files in a block got from require.context.
* @param {string} fileName - Block partial name.
*
* @access private
*
* @returns {function}
*
*/
export const getBlockGenericComponent = (blockName, paths, fileName) => getBlockComponent(blockName, paths, fileName, false);
/**
* Check if namespace is defined in block or in global manifest settings and return namespace.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {string?}
*/
export const getNamespace = (globalManifest, blockManifest) => {
return typeof blockManifest.namespace === 'undefined' ? globalManifest.namespace : blockManifest.namespace;
};
/**
* Return full block name used in Block Editor with correct namespace.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {string}
*/
export const getFullBlockName = (globalManifest, blockManifest) => {
return `${getNamespace(globalManifest, blockManifest)}/${blockManifest.blockName}`;
};
/**
* Return full block name used in Block Editor with correct namespace.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {string}
*/
export const getFullBlockNameVariation = (globalManifest, blockManifest) => {
return `${getNamespace(globalManifest, blockManifest)}/${blockManifest.parentName}`;
};
/**
* Return save function based on hasInnerBlocks option of block.
*
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {function} Save callback.
*/
export const getSaveCallback = (blockManifest) => {
const { hasInnerBlocks } = blockManifest;
if (hasInnerBlocks && typeof InnerBlocks !== 'undefined') {
return () => createElement(InnerBlocks.Content);
}
return () => null;
};
/**
* Return merge function based on existence of `mergeableAttributes` option of block.
*
* @access private
*
* @param {object} blockManifest - Block manifest.
*/
export const getMergeCallback = (blockManifest) => {
const { mergeableAttributes } = blockManifest;
if (mergeableAttributes) {
return (receiver, merger) => {
let outputObject = {};
for (const { attribute: attributeName, mergeStrategy } of mergeableAttributes) {
const attribute = Object.keys(receiver).find((k) => k === attributeName);
switch (mergeStrategy) {
case 'append': {
outputObject[attribute] = `${receiver[attribute] ?? ''}${merger[attribute] ?? ''}`;
break;
}
case 'useDestinationAttribute': {
outputObject[attribute] = merger[attribute] ?? '';
break;
}
case 'addNumericIntValue': {
outputObject[attribute] = parseInt(receiver[attribute] ?? '0') + parseInt(merger[attribute] ?? '0');
break;
}
case 'addNumericFloatValue': {
outputObject[attribute] = parseFloat(receiver[attribute] ?? '0') + parseFloat(merger[attribute] ?? '0');
break;
}
case 'addNumericPixelValue': {
const receiverRaw = receiver[attribute] ?? '0px';
const mergerRaw = merger[attribute] ?? '0px';
const receiverUnit = String(receiverRaw).replace(/[\d.-]/g, '');
const receiverValue = parseInt(receiverRaw, 10) || 0;
const mergerValue = parseInt(mergerRaw, 10) || 0;
outputObject[attribute] = `${receiverValue + mergerValue}${receiverUnit}`;
break;
}
default: {
// "useSourceAttribute" is default
outputObject[attribute] = receiver[attribute] ?? '';
break;
}
}
}
return outputObject;
};
}
return () => null;
};
/**
* Return edit function wrapped with Wrapper component.
*
* @param {React.Component} Component - Component to render inside the wrapper.
* @param {React.Component} Wrapper - Wrapper component.
*
* @access private
*
* @returns {React.Component}
*/
export const getEditCallback = (Component, Wrapper) => {
// Config is set once during init via setConfigFlags() — read it now instead of on every render.
const useWrapper = select(STORE_NAME).getConfigUseWrapper();
if (useWrapper) {
return (props) => (
<Wrapper props={props}>
<Component {...props} />
</Wrapper>
);
}
return (props) => <Component {...props} />;
};
/**
* Set icon object with icon, background and foreground.
*
* @param {object} globalManifest - Global manifest.
* @param {object} blockManifest - Block manifest.
*
* @access private
*
* @returns {object}
*/
export const getIconOptions = (globalManifest, blockManifest) => {
const { background: backgroundGlobal, foreground: foregroundGlobal } = globalManifest;
const { icon } = blockManifest;
if (typeof icon === 'undefined') {
return {};
}
// Use built-in icons if 'src' is provided and the
// icon exists in the library
if (icon.src !== undefined && blockIcons[icon.src] !== undefined) {
return {
background: typeof icon.background === 'undefined' ? backgroundGlobal : icon.background,
foreground: typeof icon.foreground === 'undefined' ? foregroundGlobal : icon.foreground,
src: <span dangerouslySetInnerHTML={{ __html: blockIcons[icon.src] }} />,
};
}
return {
background: typeof icon.background === 'undefined' ? backgroundGlobal : icon.background,
foreground: typeof icon.foreground === 'undefined' ? foregroundGlobal : icon.foreground,
src: icon.src.includes('<svg') ? <span dangerouslySetInnerHTML={{ __html: icon.src }} /> : icon.src,
};
};
/**
* Iterate over attributes or example attributes object in block/component manifest and append the parent prefixes.
*
* @param {object} manifest - Object of component/block manifest to get data from.
* @param {string} newName - New renamed component name.
* @param {string} realName - Original real component name.
* @param {boolean} [isExample=false] - Type of items to iterate, if false example key will be use, if true attributes will be used.
* @param {string} [parent=''] - Parent component key with stacked parent component names for the final output.
* @param {boolean} [currentAttributes=false] - Check if current attribute is a part of the current component.
*
* @access private
*
* @returns {object}
*/
export const prepareComponentAttribute = (manifest, newName, realName, isExample = false, parent = '', currentAttributes = false) => {
const output = {};
// Define different data point for attributes or example.
const componentAttributes = isExample ? manifest?.example?.attributes : manifest?.attributes;
// It can occur that attributes or example key is missing in manifest so bailout.
if (typeof componentAttributes === 'undefined') {
return output;
}
// Prepare parent case.
const newParent = camelCase(parent);
// Loop-invariant — compute once if we'll need it.
const realNameLowerCamel = currentAttributes ? lowerFirst(camelCase(realName)) : '';
// Iterate each attribute and attach parent prefixes.
for (const [componentAttribute, value] of Object.entries(componentAttributes)) {
let attribute = componentAttribute;
// If there is a attribute name switch use the new one.
if (newName !== realName) {
attribute = componentAttribute.replace(realName, newName);
}
// Check if current attribute is used strip component prefix from attribute and replace it with parent prefix.
if (currentAttributes) {
attribute = componentAttribute.replace(realNameLowerCamel, '');
}
// Wrapper attributes that should not be modified.
const isWrapperAttribute = attribute.startsWith('wrapper') || attribute.startsWith('showWrapper');
// Determine if parent is empty and if parent name is the same as component/block name and skip wrapper attributes.
const attributeName = isWrapperAttribute ? attribute : `${newParent}${upperFirst(attribute)}`;
// Output new attribute names.
output[attributeName] = value;
}
return output;
};
/**
* Iterate over component object in block manifest and check if the component exists in the project.
* If components contains more component this function will run recursively.
*
* @param {object} componentsManifest - Object of components manifest to iterate.
* @param {object} manifest - Object of component/block manifest to get the data from.
* @param {boolean} [isExample=false] - Type of items to iterate, if true example key will be used, if false attributes will be used.
* @param {string} [parent=''] - Parent component key with stacked parent component names for the final output.
*
* @access private
*
* @returns {object}
*/
export const prepareComponentAttributes = (componentsManifest, manifest, isExample = false, parent = '') => {
const output = {};
const { components = {} } = manifest;
// Determine if this is component or block and provide the name, not used for anything important but only to output the error msg.
const name = manifest?.blockName ? manifest.blockName : manifest.componentName;
const newParent = parent === '' ? name : parent;
// Iterate over components key in manifest recursively and check component names.
for (const [newComponentName, realComponentName] of Object.entries(components)) {
// Hoist kebabCase out of the predicate and short-circuit with .find() instead of .filter()[0].
const realComponentKebab = kebabCase(realComponentName);
const component = componentsManifest.find((item) => item.componentName === realComponentKebab);
// Bailout if component doesn't exist.
if (!component) {
throw Error(`Component specified in "${name}" manifest doesn't exist in your components list. Please check if you project has "${realComponentName}" component.`);
}
let outputAttributes = {};
// If component has more components do recursive loop.
if (component?.components) {
outputAttributes = prepareComponentAttributes(componentsManifest, component, isExample, `${newParent}${upperFirst(camelCase(newComponentName))}`);
} else {
// Output the component attributes if there is no nesting left, and append the parent prefixes.
outputAttributes = prepareComponentAttribute(component, newComponentName, realComponentName, isExample, newParent);
}
// Populate the output recursively.
Object.assign(output, outputAttributes);
}
// Add the current block/component attributes to the output.
Object.assign(output, prepareComponentAttribute(manifest, '', name, isExample, newParent, true));
return output;
};
/**
* Get Block attributes combined in one: "shared, global, wrapper, components, block".
*
* @param {object} globalManifest - Global manifest.
* @param {object} wrapperManifest - `Wrapper` manifest.
* @param {object} componentsManifest - Component manifest to iterate through.
* @param {object} parentManifest - Block or component (parent) manifest.
*
* @returns {object} Object of all attributes registered for a specific block.
*
* @access private
*
* Usage:
* ```js
* getAttributes(globalManifest, wrapperManifest, componentManifests, manifest)
* ```
*/
export const getAttributes = (globalManifest, wrapperManifest, componentsManifest, parentManifest) => {
const { blockName } = parentManifest;
const { attributes: attributesGlobal, blockClassPrefix = 'block' } = globalManifest;
const output = {
blockName: {
type: 'string',
default: blockName,
},
blockClientId: {
type: 'string',
},
blockTopLevelId: {
// Used to pass reference to all components.
type: 'string',
default: getUnique(),
},
blockFullName: {
type: 'string',
default: getFullBlockName(globalManifest, parentManifest),
},
blockClass: {
type: 'string',
default: `${blockClassPrefix}-${blockName}`,
},
blockJsClass: {
type: 'string',
default: `js-${blockClassPrefix}-${blockName}`,
},
...(typeof attributesGlobal === 'undefined' ? {} : attributesGlobal),
...(wrapperManifest?.attributes ?? {}),
...prepareComponentAttributes(componentsManifest, parentManifest),
};
return output;
};
/**
* Get Block example attributes combined in one: "components and block".
*
* @param {object} manifest - Block/component manifest.
* @param {string} [parent=''] - Parent component key with stacked parent component names for the final output.
*
* @returns {object}
*
* @access private
*
* Manifest:
* ```js
* {
* attributes: {
* buttonUse: {
* type: "string",
* default: true
* },
* buttonSize: {
* type: "string",
* default: "big"
* },
* buttonContent: {
* type: "string"
* },
* }
* }
* ```
*
* Usage:
* ```js
* getExample('button', manifest);
* ```
*
* Output:
* ```js
* {
* "buttonUse": true,
* "buttonSize": "big",
* "buttonContent": "",
* }
* ```
*/
export const getExample = (parent = '', manifest = {}) => {
return prepareComponentAttributes(select(STORE_NAME).getComponents(), manifest, true, parent);
};
/**
* Map and prepare all options from block manifest.json file for usage in registerBlockVariation method.
*
* @param {object} globalManifest - Global manifest.
* @param {object} variationManifest - Variation manifest.
* @param {Array} blocksManifest - Blocks manifests.
*
* @access private
*
* @returns {object}
*/
export const registerVariation = (globalManifest = {}, variationManifest = {}) => {
const parentBlockManifest = select(STORE_NAME).getBlock(variationManifest.parentName);
// Append globalManifest data in to output.
if (variationManifest['icon']) {
variationManifest['icon'] = getIconOptions(globalManifest, variationManifest);
} else {
// There is no icon passed to variation, use it's parent icon instead
variationManifest['icon'] = parentBlockManifest?.icon;
}
// Set full example list.
if (typeof variationManifest['example'] === 'undefined') {
variationManifest['example'] = {};
}
// Set full examples list.
variationManifest['example'].viewportWidth = 800;
variationManifest['example'].attributes = {
...parentBlockManifest?.example?.attributes,
...variationManifest['attributes'],
...variationManifest['example']?.attributes,
};
// This is a full block name used in Block Editor.
const fullBlockName = getFullBlockNameVariation(globalManifest, variationManifest);
return {
blockName: fullBlockName,
options: {
...variationManifest,
blockName: fullBlockName,
},
};
};
/**
* Map and prepare all options from block manifest.json file for usage in registerBlockType method.
*
* @param {object} globalManifest - Global manifest.
* @param {object} wrapperManifest - `Wrapper` manifest.
* @param {object} componentsManifest - Manifest of all components in a single object.
* @param {object} blockManifest - Block manifest.
* @param {function} wrapperComponent - Callback function that returns a `Wrapper`.
* @param {function} blockComponent - Edit callback function.
*
* @access private
*
* @returns {object}
*/
export const registerBlock = (globalManifest = {}, wrapperManifest = {}, componentsManifest = {}, blockManifest = {}, wrapperComponent, blockComponent) => {
const icon = getIconOptions(globalManifest, blockManifest);
const fullBlockName = getFullBlockName(globalManifest, blockManifest);
const attributes = getAttributes(globalManifest, wrapperManifest, componentsManifest, blockManifest);
const fullAttributes = {
metadata: {
type: 'object',
},
...attributes,
};
const exampleAttributes = {};
for (const [key, value] of Object.entries(attributes)) {
if (value?.default) {
exampleAttributes[key] = value.default;
}
}
const example = {
...(blockManifest['example'] ?? {}),
viewportWidth: 800,
attributes: {
...exampleAttributes,
...getExample('', blockManifest),
},
};
// Closure-static values for the label callback — read once at register time.
const blockTitle = blockManifest?.title;
const labelFallback = blockTitle ?? fullBlockName;
return {
blockName: fullBlockName,
options: {
...blockManifest,
apiVersion: 3,
icon,
attributes: fullAttributes,
example,
blockName: fullBlockName,
edit: getEditCallback(blockComponent, wrapperComponent),
save: getSaveCallback(blockManifest),
merge: getMergeCallback(blockManifest),
// WP 6.4+ Block renaming support
__experimentalBlockRenaming: true,
__experimentalLabel: (attributes, { context }) => {
const customName = attributes?.metadata?.name ?? labelFallback;
if (context === 'visual' && customName !== blockTitle) {
return `${customName} (${blockTitle})`;
}
if (context === 'accessibility') {
const { content } = attributes;
return !content || content?.length === 0 ? __('Empty', 'eightshift-frontend-libs') : content;
}
return customName;
},
__experimentalMetadata: true,
...(globalManifest?.blockOptions ?? {}),
},
};
};