feat: simplifies collapsible label API, adds e2e tests

This commit is contained in:
Jarrod Flesch
2022-11-16 12:57:54 -05:00
parent 999c8fc08b
commit d9df98ff22
11 changed files with 105 additions and 110 deletions

View File

@@ -4,24 +4,27 @@ import { useWatchForm } from '../Form/context';
const baseClass = 'row-label';
export const RowLabel: React.FC<Props> = (props) => {
export const RowLabel: React.FC<Props> = ({ className, ...rest }) => {
return (
<span style={{
pointerEvents: 'none',
}}
<span
style={{
pointerEvents: 'none',
}}
className={[
baseClass,
className,
].filter(Boolean).join(' ')}
>
<RowLabelContent {...props} />
<RowLabelContent {...rest} />
</span>
);
};
const RowLabelContent: React.FC<Props> = (props) => {
const RowLabelContent: React.FC<Omit<Props, 'className'>> = (props) => {
const {
path,
fallback,
label,
rowNumber,
className,
} = props;
const { getDataByPath, getSiblingData } = useWatchForm();
@@ -35,37 +38,18 @@ const RowLabelContent: React.FC<Props> = (props) => {
<Label
data={data}
path={path}
fallback={fallback}
index={rowNumber}
/>
);
}
if (label) {
return (
<span
className={[
baseClass,
className,
].filter(Boolean).join(' ')}
>
{typeof label === 'function' ? label({
data,
path,
fallback,
index: rowNumber,
}) : label}
</span>
);
}
if (fallback) {
return (
<React.Fragment>
{fallback}
</React.Fragment>
);
}
return null;
return (
<React.Fragment>
{typeof label === 'function' ? label({
data,
path,
index: rowNumber,
}) : label}
</React.Fragment>
);
};

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { Data } from '../Form/types';
export type Props = {
fallback: string;
path: string;
label?: RowLabel;
rowNumber?: number;
@@ -12,8 +11,7 @@ export type Props = {
export type RowLabelArgs = {
data: Data,
path: string,
index: number,
fallback: string
index?: number,
}
export type RowLabelFunction = (args: RowLabelArgs) => string

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import React, { useCallback, useEffect, useReducer } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useAuth } from '../../../utilities/Auth';
import withCondition from '../../withCondition';
@@ -50,8 +50,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
},
} = props;
const RowLabelFromProps = components?.RowLabel || undefined;
const path = pathFromProps || name;
// Handle labeling for Arrays, Global Arrays, and Blocks
@@ -65,6 +63,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
// eslint-disable-next-line react/destructuring-assignment
const label = props?.label ?? props?.labels?.singular;
const CustomRowLabel = components?.RowLabel || undefined;
const { preferencesKey } = useDocumentInfo();
const { getPreference } = usePreferences();
const { setPreference } = usePreferences();
@@ -255,6 +255,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
>
{rows.length > 0 && rows.map((row, i) => {
const rowNumber = i + 1;
const fallbackLabel = `${labels.singular} ${String(rowNumber).padStart(2, '0')}`;
return (
<Draggable
@@ -278,8 +279,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
header={(
<RowLabel
path={`${path}.${i}`}
label={RowLabelFromProps}
fallback={`${labels.singular} ${String(rowNumber).padStart(2, '0')}`}
label={CustomRowLabel || fallbackLabel}
rowNumber={rowNumber}
/>
)}

View File

@@ -1,6 +1,8 @@
@import '../../../../scss/styles.scss';
.collapsible-field {
margin: 0 0 base(2);
&__label {
pointer-events: none;
}

View File

@@ -3,7 +3,6 @@ import RenderFields from '../../RenderFields';
import withCondition from '../../withCondition';
import { Props } from './types';
import { Collapsible } from '../../../elements/Collapsible';
import toKebabCase from '../../../../../utilities/toKebabCase';
import { usePreferences } from '../../../utilities/Preferences';
import { DocumentPreferences } from '../../../../../preferences/types';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
@@ -28,45 +27,57 @@ const CollapsibleField: React.FC<Props> = (props) => {
className,
initCollapsed,
description,
components,
},
} = props;
const CollapsibleLabelFromProps = components?.CollapsibleLabel || undefined;
const { getPreference, setPreference } = usePreferences();
const { preferencesKey } = useDocumentInfo();
const [collapsedOnMount, setCollapsedOnMount] = useState<boolean>();
const [fieldPreferencesKey] = useState(() => `collapsible-${toKebabCase(label)}`);
const fieldPreferencesKey = `collapsible-${indexPath.replace(/\./gi, '__')}`;
const onToggle = useCallback(async (newCollapsedState: boolean) => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey);
setPreference(preferencesKey, {
...existingPreferences,
fields: {
...existingPreferences?.fields || {},
[fieldPreferencesKey]: {
...existingPreferences?.fields?.[fieldPreferencesKey],
collapsed: newCollapsedState,
...path ? {
fields: {
...existingPreferences?.fields || {},
[path]: {
...existingPreferences?.fields?.[path],
collapsed: newCollapsedState,
},
},
} : {
fields: {
...existingPreferences?.fields || {},
[fieldPreferencesKey]: {
...existingPreferences?.fields?.[fieldPreferencesKey],
collapsed: newCollapsedState,
},
},
},
});
}, [preferencesKey, fieldPreferencesKey, getPreference, setPreference]);
}, [preferencesKey, fieldPreferencesKey, getPreference, setPreference, path]);
useEffect(() => {
const fetchInitialState = async () => {
const preferences = await getPreference(preferencesKey);
setCollapsedOnMount(Boolean(preferences?.fields?.[fieldPreferencesKey]?.collapsed ?? initCollapsed));
if (preferences) {
const initCollapsedFromPref = path ? preferences?.fields?.[path]?.collapsed : preferences?.fields?.[fieldPreferencesKey]?.collapsed;
setCollapsedOnMount(Boolean(initCollapsedFromPref));
} else {
setCollapsedOnMount(typeof initCollapsed === 'boolean' ? initCollapsed : false);
}
};
fetchInitialState();
}, [getPreference, preferencesKey, fieldPreferencesKey, initCollapsed]);
}, [getPreference, preferencesKey, fieldPreferencesKey, initCollapsed, path]);
if (typeof collapsedOnMount !== 'boolean') return null;
return (
<div id={fieldPreferencesKey}>
<div id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./gi, '__')}` : ''}`}>
<Collapsible
initCollapsed={collapsedOnMount}
className={[
@@ -77,8 +88,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
header={(
<RowLabel
path={path}
fallback={label}
label={CollapsibleLabelFromProps}
label={label}
/>
)}
onToggle={onToggle}

View File

@@ -183,14 +183,13 @@ export const row = baseField.keys({
});
export const collapsible = baseField.keys({
label: joi.string().required(),
label: joi.alternatives().try(
joi.string(),
componentSchema,
),
type: joi.string().valid('collapsible').required(),
fields: joi.array().items(joi.link('#field')),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
CollapsibleLabel: componentSchema,
}).default({}),
}).default({}),
admin: baseAdminFields.default(),
});
const tab = baseField.keys({

View File

@@ -186,15 +186,12 @@ export type RowField = Omit<FieldBase, 'admin' | 'name'> & {
fields: Field[];
}
export type CollapsibleField = Omit<FieldBase, 'name'> & {
export type CollapsibleField = Omit<FieldBase, 'name' | 'label'> & {
type: 'collapsible';
label: string
label: RowLabel
fields: Field[];
admin?: Admin & {
initCollapsed?: boolean | false;
components?: {
CollapsibleLabel?: RowLabel
} & Admin['components']
}
}

View File

@@ -2,6 +2,6 @@ import React from 'react';
import { RowLabelComponent } from '../../../../src/admin/components/forms/RowLabel/types';
export const ArrayRowLabel: RowLabelComponent = (props) => {
const { data, fallback } = props;
return <div style={{ textTransform: 'uppercase' }}>{data.title || fallback}</div>;
const { data } = props;
return <div style={{ textTransform: 'uppercase' }}>{data.title || 'Untitled'}</div>;
};

View File

@@ -2,6 +2,6 @@ import React from 'react';
import { RowLabelComponent } from '../../../../src/admin/components/forms/RowLabel/types';
export const CollapsibleLabelComponent: RowLabelComponent = (props) => {
const { data, fallback } = props;
return <div style={{ textTransform: 'uppercase' }}>{data.componentTitleField || fallback}</div>;
const { data } = props;
return <div style={{ textTransform: 'uppercase' }}>{data.title || 'Untitled'}</div>;
};

View File

@@ -77,14 +77,11 @@ const CollapsibleFields: CollectionConfig = {
],
},
{
label: 'Collapsible Header Function',
label: ({ data }) => data.functionTitleField || 'Custom Collapsible Label',
type: 'collapsible',
admin: {
description: 'Collapsible label rendered from a function.',
initCollapsed: true,
components: {
CollapsibleLabel: ({ data }) => data.functionTitleField || 'Untitled',
},
},
fields: [
{
@@ -94,14 +91,10 @@ const CollapsibleFields: CollectionConfig = {
],
},
{
label: 'Collapsible Header Component',
label: ({ data }) => data?.componentTitleField || 'Untitled',
type: 'collapsible',
admin: {
description: 'Collapsible label rendered as a react component.',
initCollapsed: true,
components: {
CollapsibleLabel: CollapsibleLabelComponent,
},
},
fields: [
{
@@ -110,11 +103,26 @@ const CollapsibleFields: CollectionConfig = {
},
{
type: 'collapsible',
label: 'Nested Collapsible',
label: ({ data }) => data?.nestedTitle || 'Nested Collapsible',
fields: [
{
type: 'text',
name: 'nestedText',
name: 'nestedTitle',
},
],
},
],
},
{
type: 'row',
fields: [
{
label: CollapsibleLabelComponent,
type: 'collapsible',
fields: [
{
name: 'title',
type: 'text',
},
],
},

View File

@@ -67,32 +67,29 @@ describe('fields', () => {
});
});
// describe('fields - collapsible', () => {
// let url: AdminUrlUtil;
// beforeAll(() => {
// url = new AdminUrlUtil(serverURL, collapsibleFieldsSlug);
// });
describe('fields - collapsible', () => {
let url: AdminUrlUtil;
beforeAll(() => {
url = new AdminUrlUtil(serverURL, collapsibleFieldsSlug);
});
// test('should render CollapsibleLabel using a function', async () => {
// await page.goto(url.create);
// await page.locator('#field-collapsibleLabelAsFunction >> .array-field__add-button-wrap >> button').click();
test('should render CollapsibleLabel using a function', async () => {
await page.goto(url.create);
await page.locator('#field-collapsible-3__1 >> #field-nestedTitle').fill('custom row label');
await wait(100);
const customCollapsibleLabel = await page.locator('#field-collapsible-3__1 >> .row-label');
await expect(customCollapsibleLabel).toContainText('custom row label');
});
// await page.locator('#field-collapsibleLabelAsFunction__0__title').fill('custom row label');
// await wait(100);
// const customCollapsibleLabel = await page.locator('#collapsibleLabelAsFunction-row-0 >> .row-label');
// await expect(customCollapsibleLabel).toContainText('custom row label');
// });
// test('should render CollapsibleLabel using a component', async () => {
// await page.goto(url.create);
// await page.locator('#field-collapsibleLabelAsComponent>> .array-field__add-button-wrap >> button').click();
// await page.locator('#field-collapsibleLabelAsComponent__0__title').fill('custom row label');
// await wait(100);
// const customCollapsibleLabel = await page.locator('#collapsibleLabelAsComponent-row-0 >> .row-label :text("custom row label")');
// await expect(customCollapsibleLabel).toHaveCSS('text-transform', 'uppercase');
// });
// });
test('should render CollapsibleLabel using a component', async () => {
const customLabel = 'custom row label as component';
await page.goto(url.create);
await page.locator('#field-collapsible-4__0 >> #field-title').fill(customLabel);
await wait(100);
const customCollapsibleLabel = await page.locator(`#field-collapsible-4__0 >> .row-label :text("${customLabel}")`);
await expect(customCollapsibleLabel).toHaveCSS('text-transform', 'uppercase');
});
});
describe('fields - array', () => {
let url: AdminUrlUtil;