UNPKG

@plone/volto

Version:
1,076 lines (980 loc) 31.4 kB
/** * Blocks helper. * @module helpers/Blocks */ import omit from 'lodash/omit'; import without from 'lodash/without'; import endsWith from 'lodash/endsWith'; import find from 'lodash/find'; import isObject from 'lodash/isObject'; import keys from 'lodash/keys'; import merge from 'lodash/merge'; import move from 'lodash-move'; import { v4 as uuid } from 'uuid'; import config from '@plone/volto/registry'; import { applySchemaEnhancer } from '@plone/volto/helpers/Extensions'; import { insertInArray, removeFromArray, } from '@plone/volto/helpers/Utils/Utils'; /** * Get blocks field. * @function getBlocksFieldname * @param {Object} props Properties. * @return {string} Field name of the blocks */ export function getBlocksFieldname(props) { return ( find( keys(props), (key) => key !== 'volto.blocks' && endsWith(key, 'blocks'), ) || null ); } /** * Get blocks layout field. * @function getBlocksLayoutFieldname * @param {Object} props Properties. * @return {string} Field name of the blocks layout */ export function getBlocksLayoutFieldname(props) { return ( find( keys(props), (key) => key !== 'volto.blocks' && endsWith(key, 'blocks_layout'), ) || null ); } /** * Has blocks data. * @function hasBlocksData * @param {Object} props Properties. * @return {boolean} True if it has blocks data. */ export function hasBlocksData(props) { return ( find( keys(props), (key) => key !== 'volto.blocks' && endsWith(key, 'blocks'), ) !== undefined ); } /** * Pluggable method to test if a block has a set value (any non-empty value) * @function blockHasValue * @param {Object} data Block data * @return {boolean} True if block has a non-empty value */ export function blockHasValue(data) { const { blocks } = config; const blockType = data['@type']; const check = blocks.blocksConfig[blockType]?.blockHasValue; if (!check) { return true; } return check(data); } /** * Block id is valid (not undefined/null or the string "undefined" from object[undefined]) * @param {*} id Block id * @return {boolean} */ const isValidBlockId = (id) => id != null && id !== 'undefined' && (typeof id !== 'string' || id.length > 0); /** * Get block pairs of [id, block] from content properties * @function getBlocks * @param {Object} properties * @return {Array} a list of block [id, value] pairs, in order from layout */ export const getBlocks = (properties) => { const blocksFieldName = getBlocksFieldname(properties); const blocksLayoutFieldname = getBlocksLayoutFieldname(properties); const blocks = properties?.[blocksFieldName]; const items = properties?.[blocksLayoutFieldname]?.items; if (!items) return []; return items .filter((n) => isValidBlockId(n)) .map((n) => [n, blocks?.[n]]) .filter(([, block]) => block != null); }; /** * Get layout item IDs that are valid but have no block data (orphaned refs). * @param {Object} properties Content form properties * @return {string[]} IDs that should be removed from layout */ export const getInvalidBlockLayoutIds = (properties) => { const blocksFieldName = getBlocksFieldname(properties); const blocksLayoutFieldName = getBlocksLayoutFieldname(properties); const blocks = properties?.[blocksFieldName] ?? {}; const layoutItems = properties?.[blocksLayoutFieldName]?.items ?? []; return layoutItems.filter((id) => isValidBlockId(id) && blocks[id] == null); }; /** * Move block to different location index within blocks_layout * @function moveBlock * @param {Object} formData Form data * @param {number} source index within form blocks_layout items * @param {number} destination index within form blocks_layout items * @return {Object} New form data */ export function moveBlock(formData, source, destination) { const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); return { ...formData, [blocksLayoutFieldname]: { items: move(formData[blocksLayoutFieldname].items, source, destination), }, }; } /** * Delete block by id * @function deleteBlock * @param {Object} formData Form data * @param {string} blockId Block uid * @param {Object} intl intl object. * @return {Object} New form data */ export function deleteBlock(formData, blockId, intl) { const blocksFieldname = getBlocksFieldname(formData); const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); let newFormData = { ...formData, }; let container = findParent(newFormData, { blockId, }); if (container) { container[blocksLayoutFieldname].items = without( container[blocksLayoutFieldname].items, blockId, ); container[blocksFieldname] = omit(container[blocksFieldname], [blockId]); } else { newFormData[blocksLayoutFieldname].items = without( newFormData[blocksLayoutFieldname].items, blockId, ); newFormData[blocksFieldname] = omit(newFormData[blocksFieldname], [ blockId, ]); } if (newFormData[blocksLayoutFieldname].items.length === 0) { newFormData = addBlock( newFormData, config.settings.defaultBlockType, 0, null, intl, ); } return newFormData; } /** * Adds a block to the blocks form * @function addBlock * @param {Object} formData Form data * @param {string} type Block type * @param {number} index Destination index * @param {Object} blocksConfig Blocks configuration. * @param {Object} intl intl object. * @return {Array} New block id, New form data */ export function addBlock(formData, type, index, blocksConfig, intl) { const { settings } = config; const id = uuid(); const idTrailingBlock = uuid(); const blocksFieldname = getBlocksFieldname(formData); const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); const totalItems = formData[blocksLayoutFieldname].items.length; const insert = index === -1 ? totalItems : index; let value = applyBlockDefaults({ data: { '@type': type, }, intl: _dummyIntl, }); return [ id, applyBlockInitialValue({ id, value, blocksConfig, formData: { ...formData, [blocksLayoutFieldname]: { items: [ ...formData[blocksLayoutFieldname].items.slice(0, insert), id, ...(type !== settings.defaultBlockType ? [idTrailingBlock] : []), ...formData[blocksLayoutFieldname].items.slice(insert), ], }, [blocksFieldname]: { ...formData[blocksFieldname], [id]: value, ...(type !== settings.defaultBlockType && { [idTrailingBlock]: { '@type': settings.defaultBlockType, }, }), }, selected: id, }, intl, }), ]; } /** * Gets an initial value for a block, based on configuration * * This allows blocks that need complex initial data structures to avoid having * to call `onChangeBlock` at their creation time, as this is prone to racing * issue on block data storage. */ export const applyBlockInitialValue = ({ id, value, blocksConfig, formData, intl, }) => { const type = value['@type']; blocksConfig = blocksConfig || config.blocks.blocksConfig; if (blocksConfig[type]?.initialValue) { value = blocksConfig[type].initialValue({ id, value, formData, intl, }); const blocksFieldname = getBlocksFieldname(formData); formData[blocksFieldname][id] = value; } return formData; }; /** * Mutate block, changes the block @type * @function mutateBlock * @param {Object} formData Form data * @param {string} id Block uid to mutate * @param {number} value Block's new value * @param {Object} blocksConfig Blocks configuration. * @param {Object} intl intl object. * @return {Object} New form data */ export function mutateBlock(formData, id, value, blocksConfig, intl) { const { settings } = config; const blocksFieldname = getBlocksFieldname(formData); const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); const index = formData[blocksLayoutFieldname].items.indexOf(id) + 1; value = applyBlockDefaults({ data: value, intl: _dummyIntl, }); let newFormData; // Test if block at index is already a placeholder (trailing) block const trailId = formData[blocksLayoutFieldname].items[index]; if (trailId) { const block = formData[blocksFieldname][trailId]; newFormData = applyBlockInitialValue({ id, value, blocksConfig, formData: { ...formData, [blocksFieldname]: { ...formData[blocksFieldname], [id]: value || null, }, }, intl, }); if (!blockHasValue(block)) { return newFormData; } } const idTrailingBlock = uuid(); newFormData = applyBlockInitialValue({ id, value, blocksConfig, formData: { ...formData, [blocksFieldname]: { ...formData[blocksFieldname], [id]: value || null, [idTrailingBlock]: { '@type': settings.defaultBlockType, }, }, [blocksLayoutFieldname]: { items: [ ...formData[blocksLayoutFieldname].items.slice(0, index), idTrailingBlock, ...formData[blocksLayoutFieldname].items.slice(index), ], }, }, intl, }); return newFormData; } /** * Insert new block before another block * @function insertBlock * @param {Object} formData Form data * @param {string} id Insert new block before the block with this id * @param {number} value New block's value * @param {Object} current Current block * @param {number} offset offset position * @param {Object} blocksConfig Blocks configuration. * @param {Object} intl intl object. * @return {Array} New block id, New form data */ export function insertBlock( formData, id, value, current = {}, offset = 0, blocksConfig, intl, ) { const blocksFieldname = getBlocksFieldname(formData); const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); const index = formData[blocksLayoutFieldname].items.indexOf(id); value = applyBlockDefaults({ data: value, intl: _dummyIntl, }); const newBlockId = uuid(); const newFormData = applyBlockInitialValue({ id: newBlockId, value, blocksConfig, formData: { ...formData, [blocksFieldname]: { ...formData[blocksFieldname], [newBlockId]: value || null, [id]: { ...formData[blocksFieldname][id], ...current, }, }, [blocksLayoutFieldname]: { items: [ ...formData[blocksLayoutFieldname].items.slice(0, index + offset), newBlockId, ...formData[blocksLayoutFieldname].items.slice(index + offset), ], }, }, intl, }); return [newBlockId, newFormData]; } /** * Change block * @function changeBlock * @param {Object} formData Form data * @param {string} id Block uid to change * @param {number} value Block's new value * @return {Object} New form data */ export function changeBlock(formData, id, value) { const blocksFieldname = getBlocksFieldname(formData); return { ...formData, [blocksFieldname]: { ...formData[blocksFieldname], [id]: value || null, }, }; } /** * Get the next block UID within form * @function nextBlockId * @param {Object} formData Form data * @param {string} currentBlock Block uid * @return {string} Next block uid */ export function nextBlockId(formData, currentBlock) { const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); const currentIndex = formData[blocksLayoutFieldname].items.indexOf(currentBlock); if (currentIndex === formData[blocksLayoutFieldname].items.length - 1) { // We are already at the bottom block don't do anything return null; } const newIndex = currentIndex + 1; return formData[blocksLayoutFieldname].items[newIndex]; } /** * Get the previous block UID within form * @function previousBlockId * @param {Object} formData Form data * @param {string} currentBlock Block uid * @return {string} Previous block uid */ export function previousBlockId(formData, currentBlock) { const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); const currentIndex = formData[blocksLayoutFieldname].items.indexOf(currentBlock); if (currentIndex === 0) { // We are already at the top block don't do anything return null; } const newindex = currentIndex - 1; return formData[blocksLayoutFieldname].items[newindex]; } /** * Generate empty block form * @function emptyBlocksForm * @param {Object} formData Form data * @return {Object} Empty blocks form with one defaultBlockType block */ export function emptyBlocksForm() { const { settings } = config; const id = uuid(); return { blocks: { [id]: { '@type': settings.defaultBlockType, }, }, blocks_layout: { items: [id] }, }; } /** * Generate empty blocks blocks/blocks_layout pair given the type * (could be empty, if not type given) and the number of blocks * @function blocksFormGenerator * @param {number} number How many blocks to generate of the type (could be "empty", if no type provided) * @param {string} type The type of the blocks * @return {Object} blocks/blocks_layout pair filled with the generated blocks */ export function blocksFormGenerator(number, type) { const idMap = [...Array(number).keys()].map(() => uuid()); const start = { blocks: {}, blocks_layout: { items: idMap }, }; return { ...start, blocks: Object.fromEntries( start.blocks_layout.items.map((item) => [ item, { '@type': type || 'empty' }, ]), ), }; } /** * Recursively discover blocks in data and call the provided callback * @function visitBlocks * @param {Object} content A content data structure (an object with blocks and blocks_layout) * @param {Function} callback A function to call on each discovered block */ export function visitBlocks(content, callback) { const queue = getBlocks(content); while (queue.length > 0) { const [id, blockdata] = queue.shift(); callback([id, blockdata]); // assumes that a block value is like: {blocks, blocks_layout} or // { data: {blocks, blocks_layout}} if (Object.keys(blockdata || {}).indexOf('blocks') > -1) { queue.push(...getBlocks(blockdata)); } if (Object.keys(blockdata?.data || {}).indexOf('blocks') > -1) { queue.push(...getBlocks(blockdata.data)); } } } let _logged = false; /** * Initializes data with the default values coming from schema */ export function applySchemaDefaults({ data = {}, schema, intl }) { if (!intl && !_logged) { // Old code that doesn't pass intl doesn't get ObjectWidget defaults // eslint-disable-next-line no-console console.warn( `You should pass intl to any applySchemaDefaults call. By failing to pass the intl object, your ObjectWidget fields will not get default values extracted from their schema.`, ); _logged = true; } const derivedData = merge( Object.keys(schema.properties).reduce((accumulator, currentField) => { return typeof schema.properties[currentField].default !== 'undefined' ? { ...accumulator, [currentField]: schema.properties[currentField].default, } : intl && schema.properties[currentField].schema && !(schema.properties[currentField].widget === 'object_list') // TODO: this should be renamed as itemSchema ? { ...accumulator, [currentField]: { ...applySchemaDefaults({ data: { ...data[currentField], ...accumulator[currentField] }, schema: typeof schema.properties[currentField].schema === 'function' ? schema.properties[currentField].schema({ data: accumulator[currentField], formData: accumulator[currentField], intl, }) : schema.properties[currentField].schema, intl, }), }, } : accumulator; }, {}), data, ); return derivedData; } /** * Apply the block's default (as defined in schema) to the block data. * * @function applyBlockDefaults * @param {Object} params An object with data, intl and anything else * @return {Object} Derived data, with the defaults extracted from the schema */ export function applyBlockDefaults( { data, intl, navRoot, contentType, ...rest }, blocksConfig, ) { // We pay attention to not break on a missing (invalid) block. const block_type = data?.['@type']; const { blockSchema } = (blocksConfig || config.blocks.blocksConfig)[block_type] || {}; if (!blockSchema) return data; let schema = typeof blockSchema === 'function' ? blockSchema({ data, intl, ...rest }) : blockSchema; schema = applySchemaEnhancer({ schema, formData: data, intl, navRoot, contentType, }); return applySchemaDefaults({ data, schema, intl }); } /** * Converts a name+value style pair (ex: color/red) to a classname, * such as "has--color--red" * * This can be expanded via the style names, by suffixing them with special * converters. See config.settings.styleClassNameConverters. Examples: * * styleToClassName('theme:noprefix', 'primary') returns "primary" * styleToClassName('inverted:bool', true) returns 'inverted' * styleToClassName('inverted:bool', false) returns '' */ export const styleToClassName = (key, value, prefix = '') => { const converters = config.settings.styleClassNameConverters; const [name, ...convIds] = key.split(':'); return (convIds.length ? convIds : ['default']) .map((id) => converters[id]) .reduce((acc, conv) => conv(acc, value, prefix), name); }; export const buildStyleClassNamesFromData = (obj = {}, prefix = '') => { // style wrapper object has the form: // const styles = { // color: 'red', // backgroundColor: '#AABBCC', // } // Returns: ['has--color--red', 'has--backgroundColor--AABBCC'] return Object.entries(obj) .filter(([k, v]) => !k.startsWith('--')) .reduce( (acc, [k, v]) => [ ...acc, ...(isObject(v) ? buildStyleClassNamesFromData(v, `${prefix}${k}--`) : [styleToClassName(k, v, prefix)]), ], [], ) .filter((v) => !!v); }; /** * Generate classNames from extenders * * @function buildStyleClassNamesExtenders * @param {Object} params An object with data, content and block (current block id) * @return {Array} Extender classNames resultant array */ export const buildStyleClassNamesExtenders = ({ block, content, data, classNames, }) => { return config.settings.styleClassNameExtenders.reduce( (acc, extender) => extender({ block, content, data, classNames: acc }), classNames, ); }; /** * Converts a name+value style pair (ex: color/red) to a pair of [k, v], * such as ["color", "red"] so it can be converted back to an object. * For now, only covering the 'CSSProperty' use case. */ export const styleDataToStyleObject = (key, value, prefix = '') => { if (prefix) { return [`--${prefix}${key.replace('--', '')}`, value]; } else { return [key, value]; } }; /** * Generate styles object from data * * @function buildStyleObjectFromData * @param {Object} data A block data object * @param {string} prefix The prefix (could be dragged from a recursive call, initially empty) * @return {Object} The style object ready to be passed as prop */ export const buildStyleObjectFromData = ( data = {}, prefix = '', container = {}, ) => { // style wrapper object has the form: // const styles = { // color: 'red', // '--background-color': '#AABBCC', // } // Returns: {'--background-color: '#AABBCC'} function recursiveBuildStyleObjectFromData(obj, prefix) { return Object.fromEntries( Object.entries(obj) .filter(([k, v]) => k.startsWith('--') || isObject(v)) .reduce( (acc, [k, v]) => [ ...acc, // Kept for easy debugging // ...(() => { // if (isObject(v)) { // return Object.entries( // buildStyleObjectFromData( // v, // `${k.endsWith(':noprefix') ? '' : `${prefix}${k}--`}`, // ), // ); // } // return [styleDataToStyleObject(k, v, prefix)]; // })(), ...(isObject(v) ? Object.entries( recursiveBuildStyleObjectFromData( v, `${k.endsWith(':noprefix') ? '' : `${prefix}${k}--`}`, // We don't add a prefix if the key ends with the marker suffix ), ) : [styleDataToStyleObject(k, v, prefix)]), ], [], ) .filter((v) => !!v), ); } // If the block has a `@type`, it's a full data block object // Then apply the style enhancers if (data['@type']) { const styleObj = data.styles || {}; const stylesFromCSSproperties = recursiveBuildStyleObjectFromData( styleObj, prefix, ); let stylesFromObjectStyleEnhancers = {}; const enhancers = config.getUtilities({ type: 'styleWrapperStyleObjectEnhancer', }); enhancers.forEach(({ method }) => { stylesFromObjectStyleEnhancers = { ...stylesFromObjectStyleEnhancers, ...method({ data, container }), }; }); return { ...stylesFromCSSproperties, ...stylesFromObjectStyleEnhancers }; } else { return recursiveBuildStyleObjectFromData(data, prefix); } }; /** * Find a matching style by name given a style definition * * @function findStyleByName * @param {Object} styleDefinitions An object with the style definitions * @param {string} name The name of the style to find * @return {Object} The style object of the matching name */ export function findStyleByName(styleDefinitions, name) { return styleDefinitions.find((color) => color.name === name)?.style; } /** * Return previous/next blocks given the content object and the current block id * * @function getPreviousNextBlock * @param {Object} params An object with the content object and block (current block id) * @return {Array} An array with the signature [previousBlock, nextBlock] */ export const getPreviousNextBlock = ({ content, block }) => { const previousBlock = content['blocks'][ content['blocks_layout'].items[ content['blocks_layout'].items.indexOf(block) - 1 ] ]; const nextBlock = content['blocks'][ content['blocks_layout'].items[ content['blocks_layout'].items.indexOf(block) + 1 ] ]; return [previousBlock, nextBlock]; }; /** * Check if a block is a container block * check blocks from data as well since some add-ons use that * such as @eeacms/volto-tabs-block */ export function isBlockContainer(block) { return ( block && (hasBlocksData(block) || (block.hasOwnProperty('data') && hasBlocksData(block.data))) ); } /** * Given a `block` object and a list of block types, return a list of block ids matching the types * * @function findBlocks * @param {Object} types A list with the list of types to be matched * @return {Array} An array of block ids */ export function findBlocks(blocks = {}, types, result = []) { Object.keys(blocks).forEach((blockId) => { const block = blocks[blockId]; // check blocks from data as well since some add-ons use that // such as @eeacms/volto-tabs-block const child_blocks = block.blocks || block.data?.blocks; if (types.includes(block['@type'])) { result.push(blockId); } else if (isBlockContainer(block)) { findBlocks(child_blocks, types, result); } }); return result; } /** * Build a block's hierarchy that the order tab can understand and uses */ export const getBlocksHierarchy = (properties) => { const blocksFieldName = getBlocksFieldname(properties); const blocksLayoutFieldname = getBlocksLayoutFieldname(properties); return properties[blocksLayoutFieldname]?.items?.map((n) => ({ id: n, title: properties[blocksFieldName][n]?.['@type'], data: properties[blocksFieldName][n], children: isBlockContainer(properties[blocksFieldName][n]) ? properties[blocksFieldName][n].data ? getBlocksHierarchy(properties[blocksFieldName][n].data) : getBlocksHierarchy(properties[blocksFieldName][n]) : [], })); }; /** * Move block to different location index within blocks_layout * @function moveBlock * @param {Object} formData Form data * @param {number} source index within form blocks_layout items * @param {number} destination index within form blocks_layout items * @return {Object} New form data */ export function moveBlockEnhanced(formData, { source, destination }) { const blocksLayoutFieldname = getBlocksLayoutFieldname(formData); const blocksFieldName = getBlocksFieldname(formData); // If either one of source and destination are present // (Moves intra-container or container <-> main container) if (source.parent || destination.parent) { // Move from a container to the main container if (source.parent && !destination.parent) { let clonedFormData = { ...formData }; clonedFormData[blocksFieldName][source.id] = formData[blocksFieldName][source.parent][blocksFieldName][source.id]; clonedFormData[blocksLayoutFieldname].items = insertInArray( formData[blocksLayoutFieldname].items, source.id, destination.position, ); // Remove the source block from the source parent const sourceContainer = findContainer(clonedFormData, { containerId: source.parent, }); delete sourceContainer[blocksFieldName][source.id]; sourceContainer[blocksLayoutFieldname].items = removeFromArray( sourceContainer[blocksLayoutFieldname].items, source.position, ); return clonedFormData; } // Move from the main container to an inner container if (!source.parent && destination.parent) { let clonedFormData = { ...formData }; const destinationContainer = findContainer(clonedFormData, { containerId: destination.parent, }); destinationContainer[blocksFieldName][source.id] = clonedFormData[blocksFieldName][source.id]; destinationContainer[blocksLayoutFieldname].items = insertInArray( destinationContainer[blocksLayoutFieldname].items, source.id, destination.position, ); // Remove the source block from the source parent delete clonedFormData[blocksFieldName][source.id]; clonedFormData[blocksLayoutFieldname].items = removeFromArray( clonedFormData[blocksLayoutFieldname].items, source.position, ); return clonedFormData; } // Move within the same container (except moves within the main container) if (source.parent === destination.parent) { let clonedFormData = { ...formData }; const destinationContainer = findContainer(clonedFormData, { containerId: destination.parent, }); destinationContainer[blocksLayoutFieldname].items = move( destinationContainer[blocksLayoutFieldname].items, source.position, destination.position, ); return clonedFormData; } // Move between containers if (source.parent !== destination.parent) { let clonedFormData = { ...formData }; const destinationContainer = findContainer(clonedFormData, { containerId: destination.parent, }); const sourceContainer = findContainer(clonedFormData, { containerId: source.parent, }); destinationContainer[blocksFieldName][source.id] = sourceContainer[blocksFieldName]?.[source.id] || sourceContainer.data?.[blocksFieldName][source.id]; destinationContainer[blocksLayoutFieldname].items = insertInArray( destinationContainer[blocksLayoutFieldname].items, source.id, destination.position, ); // Remove the source block from the source parent delete sourceContainer[blocksFieldName][source.id]; sourceContainer[blocksLayoutFieldname].items = removeFromArray( sourceContainer[blocksLayoutFieldname].items, source.position, ); return clonedFormData; } } // Default catch all, no source/destination parent specified // Move within the main container return { ...formData, [blocksLayoutFieldname]: { items: move( formData[blocksLayoutFieldname].items, source.position, destination.position, ), }, }; } /** * Finds the container with the specified containerId in the given formData. * * @param {object} formData - The form data object. * @param {object} options - The options object. * @param {string} options.containerId - The ID of the container to find. * @returns {object|undefined} - The container object if found, otherwise undefined. */ export const findContainer = (formData, { containerId }) => { const block = formData.blocks[containerId]?.data || formData.blocks[containerId]; if ( block && Object.keys(block).includes('blocks') && Object.keys(block).includes('blocks_layout') ) { return block; } let container; Object.keys(formData.blocks).every((blockId) => { const subBlock = formData.blocks[blockId].data || formData.blocks[blockId]; if ( subBlock && Object.keys(subBlock).includes('blocks') && Object.keys(subBlock).includes('blocks_layout') ) { container = findContainer(subBlock, { containerId }); } if (container) { return false; } else { return true; } }); return container; }; /** * Finds parent container of the specified blockId in the given formData. * * @param {object} formData - The form data object. * @param {object} options - The options object. * @param {string} options.blockId - The ID of the block to find. * @returns {object|undefined} - The container object if found, otherwise undefined. */ export const findParent = (formData, { blockId }) => { const block = formData.data || formData; if (block && block.blocks && Object.keys(block.blocks).includes(blockId)) { return block; } let found = false; if (block && block.blocks) { Object.keys(block.blocks).every((subBlockId) => { const subBlock = block.blocks[subBlockId].data || block.blocks[subBlockId]; if (subBlock && subBlock.blocks) { if (Object.keys(subBlock.blocks).includes(blockId)) { found = subBlock; } const parent = findParent(subBlock, { blockId }); if (parent) { found = parent; } } if (found) { return false; } else { return true; } }); } return found; }; const _dummyIntl = { formatMessage() {}, };