fix: adding and replacing similarly shaped block configs (#3140)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.thumbnail-card {
|
||||
@include btn-reset;
|
||||
@include shadow;
|
||||
width: 100%;
|
||||
background: var(--theme-input-bg);
|
||||
|
||||
&__label {
|
||||
|
||||
@@ -18,7 +18,6 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
|
||||
thumbnail,
|
||||
label: labelFromProps,
|
||||
alignLabel,
|
||||
onKeyDown,
|
||||
} = props;
|
||||
|
||||
const { t, i18n } = useTranslation('general');
|
||||
@@ -43,11 +42,11 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={typeof onClick === 'function' ? onClick : undefined}
|
||||
onKeyDown={typeof onKeyDown === 'function' ? onKeyDown : undefined}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={`${baseClass}__thumbnail`}>
|
||||
{thumbnail && thumbnail}
|
||||
@@ -62,6 +61,6 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__label`}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -61,7 +61,6 @@ const Form: React.FC<Props> = (props) => {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [formattedInitialData, setFormattedInitialData] = useState(buildInitialState(initialData));
|
||||
const [collectionFieldSchemaMap, setCollectionFieldSchemaMap] = useState(new Map<string, Field[]>());
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const contextRef = useRef({} as FormContextType);
|
||||
@@ -362,18 +361,74 @@ const Form: React.FC<Props> = (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> = (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> = (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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,11 @@ export const buildFieldSchemaMap = (entityFields: Field[]): Map<string, Field[]>
|
||||
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user