fix: adding and replacing similarly shaped block configs (#3140)

This commit is contained in:
Jarrod Flesch
2023-08-08 12:38:14 -04:00
committed by GitHub
parent 8ae98503f5
commit 8e188cfe61
9 changed files with 243 additions and 49 deletions

View File

@@ -1,7 +1,9 @@
@import '../../../scss/styles.scss';
.thumbnail-card {
@include btn-reset;
@include shadow;
width: 100%;
background: var(--theme-input-bg);
&__label {

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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,
},
],
},
],
}],
},
],
};

View File

@@ -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', () => {

View File

@@ -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,
},
],
},
],
}],
},
],
},
],