diff --git a/src/admin/components/elements/ThumbnailCard/index.scss b/src/admin/components/elements/ThumbnailCard/index.scss index d3cba266bb..9038c0603f 100644 --- a/src/admin/components/elements/ThumbnailCard/index.scss +++ b/src/admin/components/elements/ThumbnailCard/index.scss @@ -1,7 +1,9 @@ @import '../../../scss/styles.scss'; .thumbnail-card { + @include btn-reset; @include shadow; + width: 100%; background: var(--theme-input-bg); &__label { diff --git a/src/admin/components/elements/ThumbnailCard/index.tsx b/src/admin/components/elements/ThumbnailCard/index.tsx index daa0524585..513088c94a 100644 --- a/src/admin/components/elements/ThumbnailCard/index.tsx +++ b/src/admin/components/elements/ThumbnailCard/index.tsx @@ -18,7 +18,6 @@ export const ThumbnailCard: React.FC = (props) => { thumbnail, label: labelFromProps, alignLabel, - onKeyDown, } = props; const { t, i18n } = useTranslation('general'); @@ -43,11 +42,11 @@ export const ThumbnailCard: React.FC = (props) => { } return ( -
{thumbnail && thumbnail} @@ -62,6 +61,6 @@ export const ThumbnailCard: React.FC = (props) => {
{title}
-
+ ); }; diff --git a/src/admin/components/forms/Form/index.tsx b/src/admin/components/forms/Form/index.tsx index c2249cebfa..5632fb22eb 100644 --- a/src/admin/components/forms/Form/index.tsx +++ b/src/admin/components/forms/Form/index.tsx @@ -61,7 +61,6 @@ const Form: React.FC = (props) => { const [processing, setProcessing] = useState(false); const [submitted, setSubmitted] = useState(false); const [formattedInitialData, setFormattedInitialData] = useState(buildInitialState(initialData)); - const [collectionFieldSchemaMap, setCollectionFieldSchemaMap] = useState(new Map()); const formRef = useRef(null); const contextRef = useRef({} as FormContextType); @@ -362,18 +361,74 @@ const Form: React.FC = (props) => { waitForAutocomplete, ]); + const traverseRowConfigs = React.useCallback(({ pathPrefix, path, fieldConfig }: { + path: string, + fieldConfig: Field[] + pathPrefix?: string, + }) => { + const config = fieldConfig; + const pathSegments = splitPathByArrayFields(path); + const configMap = buildFieldSchemaMap(config); + + for (let i = 0; i < pathSegments.length; i += 1) { + const pathSegment = pathSegments[i]; + + if (isNumber(pathSegment)) { + const rowIndex = parseInt(pathSegment, 10); + const parentFieldPath = pathSegments.slice(0, i).join('.'); + const remainingPath = pathSegments.slice(i + 1).join('.'); + const arrayFieldPath = pathPrefix ? `${pathPrefix}.${parentFieldPath}` : parentFieldPath; + const parentArrayField = contextRef.current.getField(arrayFieldPath); + const rowField = parentArrayField.rows[rowIndex]; + + if (rowField.blockType) { + const blockConfig = configMap.get(`${parentFieldPath}.${rowField.blockType}`); + if (blockConfig) { + return traverseRowConfigs({ + pathPrefix: `${arrayFieldPath}.${rowIndex}`, + path: remainingPath, + fieldConfig: blockConfig, + }); + } + + throw new Error(`Block config not found for ${rowField.blockType} at path ${path}`); + } else { + return traverseRowConfigs({ + pathPrefix: `${arrayFieldPath}.${rowIndex}`, + path: remainingPath, + fieldConfig: configMap.get(parentFieldPath), + }); + } + } + } + + return config; + }, []); + + const getRowConfigByPath = React.useCallback(({ path, blockType }: { + path: string, + blockType?: string + }) => { + const rowConfig = traverseRowConfigs({ path, fieldConfig: collection?.fields || global?.fields }); + const rowFieldConfigs = buildFieldSchemaMap(rowConfig); + const pathSegments = splitPathByArrayFields(path); + const fieldKey = pathSegments.at(-1); + return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey); + }, [traverseRowConfigs, collection?.fields, global?.fields]); + // Array/Block row manipulation const addFieldRow: Context['addFieldRow'] = useCallback(async ({ path, rowIndex, data }) => { const preferences = await getDocPreferences(); - const nonIndexedPath = path.split('.').filter((segment) => !isNumber(segment)).join('.'); - const schemaKey = data?.blockType ? `${nonIndexedPath}.${data.blockType}` : nonIndexedPath; - const rowFieldSchema = collectionFieldSchemaMap.get(schemaKey); + const fieldConfig = getRowConfigByPath({ + path, + blockType: data?.blockType, + }); - if (rowFieldSchema) { - const subFieldState = await buildStateFromSchema({ fieldSchema: rowFieldSchema, data, preferences, operation, id, user, locale, t }); + if (fieldConfig) { + const subFieldState = await buildStateFromSchema({ fieldSchema: fieldConfig, data, preferences, operation, id, user, locale, t }); dispatchFields({ type: 'ADD_ROW', rowIndex, path, blockType: data?.blockType, subFieldState }); } - }, [dispatchFields, collectionFieldSchemaMap, getDocPreferences, id, user, operation, locale, t]); + }, [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath]); const removeFieldRow: Context['removeFieldRow'] = useCallback(async ({ path, rowIndex }) => { dispatchFields({ type: 'REMOVE_ROW', rowIndex, path }); @@ -381,15 +436,16 @@ const Form: React.FC = (props) => { const replaceFieldRow: Context['replaceFieldRow'] = useCallback(async ({ path, rowIndex, data }) => { const preferences = await getDocPreferences(); - const nonIndexedPath = path.split('.').filter((segment) => !isNumber(segment)).join('.'); - const schemaKey = data?.blockType ? `${nonIndexedPath}.${data.blockType}` : nonIndexedPath; - const rowFieldSchema = collectionFieldSchemaMap.get(schemaKey); + const fieldConfig = getRowConfigByPath({ + path, + blockType: data?.blockType, + }); - if (rowFieldSchema) { - const subFieldState = await buildStateFromSchema({ fieldSchema: rowFieldSchema, data, preferences, operation, id, user, locale, t }); + if (fieldConfig) { + const subFieldState = await buildStateFromSchema({ fieldSchema: fieldConfig, data, preferences, operation, id, user, locale, t }); dispatchFields({ type: 'REPLACE_ROW', rowIndex, path, blockType: data?.blockType, subFieldState }); } - }, [dispatchFields, collectionFieldSchemaMap, getDocPreferences, id, user, operation, locale, t]); + }, [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath]); const getFields = useCallback(() => contextRef.current.fields, [contextRef]); const getField = useCallback((path: string) => contextRef.current.fields[path], [contextRef]); @@ -455,13 +511,6 @@ const Form: React.FC = (props) => { contextRef.current.removeFieldRow = removeFieldRow; contextRef.current.replaceFieldRow = replaceFieldRow; - useEffect(() => { - const entityFields = collection?.fields || global?.fields || []; - if (entityFields.length === 0) return; - const fieldSchemaMap = buildFieldSchemaMap(entityFields); - setCollectionFieldSchemaMap(fieldSchemaMap); - }, [collection?.fields, global?.fields]); - useEffect(() => { if (initialState) { contextRef.current = { ...initContextState } as FormContextType; diff --git a/src/admin/components/forms/field-types/Blocks/BlocksDrawer/index.scss b/src/admin/components/forms/field-types/Blocks/BlocksDrawer/index.scss index 00394725d0..fc0a5fcccf 100644 --- a/src/admin/components/forms/field-types/Blocks/BlocksDrawer/index.scss +++ b/src/admin/components/forms/field-types/Blocks/BlocksDrawer/index.scss @@ -2,22 +2,16 @@ .blocks-drawer { &__blocks-wrapper { - padding: base(0.5); - margin-top: base(1.5); + padding-top: base(1.5); } &__blocks { position: relative; - margin: -#{base(1)}; - display: flex; - flex-wrap: wrap; padding: 0; list-style: none; - } - - &__block { - margin: base(0.5); - width: calc((100% / 6) - #{base(1)}); + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: base(1); } &__default-image { @@ -27,33 +21,28 @@ } @include large-break { - &__block { - width: calc(20% - #{base(1)}); + &__blocks { + grid-template-columns: repeat(5, 1fr); } } @include mid-break { &__blocks-wrapper { - padding: base(0.25); + padding-top: base(1.75); } - &__blocks { - margin: -#{base(0.5)}; - } - - &__block { - margin: base(0.25); - width: calc(33.33% - #{base(0.5)}); + grid-template-columns: repeat(3, 1fr); + gap: base(0.5); } } @include small-break { &__blocks-wrapper { - margin-top: base(0.75); + padding-top: base(.75); } - &__block { - width: calc(50% - #{base(0.5)}); + &__blocks { + grid-template-columns: repeat(2, 1fr); } } } diff --git a/src/admin/scss/resets.scss b/src/admin/scss/resets.scss index 37534f9bd1..eeda892c2d 100644 --- a/src/admin/scss/resets.scss +++ b/src/admin/scss/resets.scss @@ -6,3 +6,12 @@ padding: 0; color: currentColor; } + +@mixin btn-reset { + border: 0; + background: none; + box-shadow: none; + border-radius: 0; + padding: 0; + color: currentColor; +} diff --git a/src/utilities/buildFieldSchemaMap.ts b/src/utilities/buildFieldSchemaMap.ts index 6616110595..8184a95c2c 100644 --- a/src/utilities/buildFieldSchemaMap.ts +++ b/src/utilities/buildFieldSchemaMap.ts @@ -47,7 +47,11 @@ export const buildFieldSchemaMap = (entityFields: Field[]): Map case 'tabs': field.tabs.forEach((tab) => { - nextPath = 'name' in tab ? `${nextPath}.${tab.name}` : nextPath; + if (nextPath) { + nextPath = 'name' in tab ? `${nextPath}.${tab.name}` : nextPath; + } else { + nextPath = 'name' in tab ? `${tab.name}` : nextPath; + } buildUpMap(tab.fields, nextPath); }); break; diff --git a/test/fields/collections/Blocks/index.ts b/test/fields/collections/Blocks/index.ts index 8dc904c3e9..e80b334812 100644 --- a/test/fields/collections/Blocks/index.ts +++ b/test/fields/collections/Blocks/index.ts @@ -203,6 +203,42 @@ const BlockFields: CollectionConfig = { }, ], }, + { + type: 'blocks', + name: 'blocksWithSimilarConfigs', + blocks: [{ + slug: 'block-1', + fields: [ + { + type: 'array', + name: 'items', + fields: [ + { + type: 'text', + name: 'title', + required: true, + }, + ], + }, + ], + }, + { + slug: 'block-2', + fields: [ + { + type: 'array', + name: 'items', + fields: [ + { + type: 'text', + name: 'title2', + required: true, + }, + ], + }, + ], + }], + }, ], }; diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index f76d0c8c07..b50fb49f29 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -368,6 +368,29 @@ describe('fields', () => { await expect(firstRow).toBeVisible(); await expect(firstRow.locator('.blocks-field__block-pill-text')).toContainText('Text en'); }); + + test('should add different blocks with similar field configs', async () => { + await page.goto(url.create); + + async function addBlock(name: 'Block 1' | 'Block 2') { + await page.locator('#field-blocksWithSimilarConfigs').getByRole('button', { name: 'Add Blocks With Similar Config' }).click(); + await page.getByRole('button', { name }).click(); + } + + await addBlock('Block 1'); + + await page.locator('#blocksWithSimilarConfigs-row-0').getByRole('button', { name: 'Add Item' }).click(); + await page.locator('input[name="blocksWithSimilarConfigs.0.items.0.title"]').fill('items>0>title'); + + expect(await page.locator('input[name="blocksWithSimilarConfigs.0.items.0.title"]').inputValue()).toEqual('items>0>title'); + + await addBlock('Block 2'); + + await page.locator('#blocksWithSimilarConfigs-row-1').getByRole('button', { name: 'Add Item' }).click(); + await page.locator('input[name="blocksWithSimilarConfigs.1.items.0.title2"]').fill('items>1>title'); + + expect(await page.locator('input[name="blocksWithSimilarConfigs.1.items.0.title2"]').inputValue()).toEqual('items>1>title'); + }); }); describe('array', () => { diff --git a/test/nested-fields/config.ts b/test/nested-fields/config.ts index b40c8fcec5..1ed46f76f2 100644 --- a/test/nested-fields/config.ts +++ b/test/nested-fields/config.ts @@ -85,6 +85,89 @@ export default buildConfigWithDefaults({ }, ], }, + + { + type: 'tabs', + label: 'Tabs', + tabs: [{ + label: 'Tab 1', + name: 'tab1', + fields: [ + { + type: 'blocks', + name: 'layout', + blocks: [{ + slug: 'block-1', + fields: [ + { + type: 'array', + name: 'items', + fields: [ + { + type: 'text', + name: 'title', + required: true, + }, + ], + }, + ], + }, + { + slug: 'block-2', + fields: [ + { + type: 'array', + name: 'items', + fields: [ + { + type: 'text', + name: 'title2', + required: true, + }, + ], + }, + ], + }], + }, + ], + }], + }, + { + type: 'blocks', + name: 'blocksWithSimilarConfigs', + blocks: [{ + slug: 'block-1', + fields: [ + { + type: 'array', + name: 'items', + fields: [ + { + type: 'text', + name: 'title', + required: true, + }, + ], + }, + ], + }, + { + slug: 'block-2', + fields: [ + { + type: 'array', + name: 'items', + fields: [ + { + type: 'text', + name: 'title2', + required: true, + }, + ], + }, + ], + }], + }, ], }, ],