feat/add support for setting collapsable fields (array, block, collapsable… (#1057)

Co-authored-by: liorix <liorix@gmail.com>
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
This commit is contained in:
Dan Ribbens
2022-09-12 13:39:49 -04:00
committed by GitHub
parent ca434b8a92
commit 834f4ebd38
12 changed files with 191 additions and 77 deletions

View File

@@ -35,10 +35,19 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. | | **`required`** | Require this field to have a value. |
| **`labels`** | Customize the row labels appearing in the Admin dashboard. | | **`labels`** | Customize the row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
*\* An asterisk denotes that a property is required.* *\* An asterisk denotes that a property is required.*
### Admin Config
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
| Option | Description |
| ---------------------- | ------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
### Example ### Example
`collections/ExampleCollection.ts` `collections/ExampleCollection.ts`

View File

@@ -36,10 +36,18 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. | | **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
*\* An asterisk denotes that a property is required.* *\* An asterisk denotes that a property is required.*
### Admin Config
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
| Option | Description |
| ---------------------- | ------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
### Block configs ### Block configs
Blocks are defined as separate configs of their own. Blocks are defined as separate configs of their own.

View File

@@ -12,14 +12,22 @@ keywords: row, fields, config, configuration, documentation, Content Management
### Config ### Config
| Option | Description | | Option | Description |
| ---------------- | ----------- | | -------------- | ------------------------------------------------------------------------- |
| **`label`** * | A label to render within the header of the collapsible component. | | **`label`** * | A label to render within the header of the collapsible component. |
| **`fields`** * | Array of field types to nest within this Collapsible. | | **`fields`** * | Array of field types to nest within this Collapsible. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
*\* An asterisk denotes that a property is required.* *\* An asterisk denotes that a property is required.*
### Admin Config
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
| Option | Description |
| ---------------------- | ------------------------------- |
| **`initCollapsed`** | Set the initial collapsed state |
### Example ### Example
`collections/ExampleCollection.ts` `collections/ExampleCollection.ts`

View File

@@ -42,6 +42,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
readOnly, readOnly,
description, description,
condition, condition,
initCollapsed,
className, className,
}, },
} = props; } = props;
@@ -128,33 +129,40 @@ const ArrayFieldType: React.FC<Props> = (props) => {
moveRow(sourceIndex, destinationIndex); moveRow(sourceIndex, destinationIndex);
}, [moveRow]); }, [moveRow]);
const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => { const setCollapse = useCallback(
dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed }); async (rowID: string, collapsed: boolean) => {
dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed });
if (preferencesKey) { if (preferencesKey) {
const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; const preferencesToSet = (await getPreference(preferencesKey)) || { fields: {} };
let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed let newCollapsedState: string[] = preferencesToSet?.fields?.[path]?.collapsed;
.filter((filterID) => (rows.find((row) => row.id === filterID)))
|| [];
if (!collapsed) { if (initCollapsed && typeof newCollapsedState === 'undefined') {
newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID); newCollapsedState = rows.map((row) => row.id);
} else { } else if (typeof newCollapsedState === 'undefined') {
newCollapsedState.push(rowID); newCollapsedState = [];
} }
setPreference(preferencesKey, { if (!collapsed) {
...preferencesToSet, newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID);
fields: { } else {
...preferencesToSet?.fields || {}, newCollapsedState.push(rowID);
[path]: { }
...preferencesToSet?.fields?.[path],
collapsed: newCollapsedState, setPreference(preferencesKey, {
...preferencesToSet,
fields: {
...(preferencesToSet?.fields || {}),
[path]: {
...preferencesToSet?.fields?.[path],
collapsed: newCollapsedState,
},
}, },
}, });
}); }
} },
}, [preferencesKey, path, setPreference, rows, getPreference]); [preferencesKey, getPreference, path, setPreference, initCollapsed, rows],
);
const toggleCollapseAll = useCallback(async (collapse: boolean) => { const toggleCollapseAll = useCallback(async (collapse: boolean) => {
dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse }); dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse });
@@ -178,12 +186,12 @@ const ArrayFieldType: React.FC<Props> = (props) => {
useEffect(() => { useEffect(() => {
const initializeRowState = async () => { const initializeRowState = async () => {
const data = formContext.getDataByPath<Row[]>(path); const data = formContext.getDataByPath<Row[]>(path);
const preferences = await getPreference(preferencesKey) || { fields: {} }; const preferences = (await getPreference(preferencesKey)) || { fields: {} };
dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed }); dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed, initCollapsed });
}; };
initializeRowState(); initializeRowState();
}, [formContext, path, getPreference, preferencesKey]); }, [formContext, path, getPreference, preferencesKey, initCollapsed]);
useEffect(() => { useEffect(() => {
setValue(rows?.length || 0, true); setValue(rows?.length || 0, true);

View File

@@ -54,6 +54,7 @@ const Index: React.FC<Props> = (props) => {
readOnly, readOnly,
description, description,
condition, condition,
initCollapsed,
className, className,
}, },
} = props; } = props;
@@ -141,9 +142,13 @@ const Index: React.FC<Props> = (props) => {
if (preferencesKey) { if (preferencesKey) {
const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; const preferencesToSet = await getPreference(preferencesKey) || { fields: {} };
let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed let newCollapsedState: string[] = preferencesToSet?.fields?.[path]?.collapsed;
.filter((filterID) => (rows.find((row) => row.id === filterID)))
|| []; if (initCollapsed && typeof newCollapsedState === 'undefined') {
newCollapsedState = rows.map((row) => row.id);
} else if (typeof newCollapsedState === 'undefined') {
newCollapsedState = [];
}
if (!collapsed) { if (!collapsed) {
newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID); newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID);
@@ -162,7 +167,7 @@ const Index: React.FC<Props> = (props) => {
}, },
}); });
} }
}, [preferencesKey, getPreference, path, setPreference, rows]); }, [preferencesKey, getPreference, path, setPreference, initCollapsed, rows]);
const toggleCollapseAll = useCallback(async (collapse: boolean) => { const toggleCollapseAll = useCallback(async (collapse: boolean) => {
dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse }); dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse });
@@ -187,12 +192,12 @@ const Index: React.FC<Props> = (props) => {
useEffect(() => { useEffect(() => {
const initializeRowState = async () => { const initializeRowState = async () => {
const data = formContext.getDataByPath<Row[]>(path); const data = formContext.getDataByPath<Row[]>(path);
const preferences = await getPreference(preferencesKey) || { fields: {} }; const preferences = (await getPreference(preferencesKey)) || { fields: {} };
dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed }); dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed, initCollapsed });
}; };
initializeRowState(); initializeRowState();
}, [formContext, path, getPreference, preferencesKey]); }, [formContext, path, getPreference, preferencesKey, initCollapsed]);
useEffect(() => { useEffect(() => {
setValue(rows?.length || 0, true); setValue(rows?.length || 0, true);

View File

@@ -24,13 +24,14 @@ const CollapsibleField: React.FC<Props> = (props) => {
admin: { admin: {
readOnly, readOnly,
className, className,
initCollapsed,
description, description,
}, },
} = props; } = props;
const { getPreference, setPreference } = usePreferences(); const { getPreference, setPreference } = usePreferences();
const { preferencesKey } = useDocumentInfo(); const { preferencesKey } = useDocumentInfo();
const [initCollapsed, setInitCollapsed] = useState<boolean>(); const [collapsedOnMount, setCollapsedOnMount] = useState<boolean>();
const [fieldPreferencesKey] = useState(() => `collapsible-${toKebabCase(label)}`); const [fieldPreferencesKey] = useState(() => `collapsible-${toKebabCase(label)}`);
const onToggle = useCallback(async (newCollapsedState: boolean) => { const onToggle = useCallback(async (newCollapsedState: boolean) => {
@@ -51,18 +52,18 @@ const CollapsibleField: React.FC<Props> = (props) => {
useEffect(() => { useEffect(() => {
const fetchInitialState = async () => { const fetchInitialState = async () => {
const preferences = await getPreference(preferencesKey); const preferences = await getPreference(preferencesKey);
setInitCollapsed(Boolean(preferences?.fields?.[fieldPreferencesKey]?.collapsed)); setCollapsedOnMount(Boolean(preferences?.fields?.[fieldPreferencesKey]?.collapsed ?? initCollapsed));
}; };
fetchInitialState(); fetchInitialState();
}, [getPreference, preferencesKey, fieldPreferencesKey]); }, [getPreference, preferencesKey, fieldPreferencesKey, initCollapsed]);
if (typeof initCollapsed !== 'boolean') return null; if (typeof collapsedOnMount !== 'boolean') return null;
return ( return (
<React.Fragment> <React.Fragment>
<Collapsible <Collapsible
initCollapsed={initCollapsed} initCollapsed={collapsedOnMount}
className={[ className={[
'field-type', 'field-type',
baseClass, baseClass,

View File

@@ -11,6 +11,7 @@ type SET_ALL = {
data: { id?: string, blockType?: string }[] data: { id?: string, blockType?: string }[]
collapsedState?: string[] collapsedState?: string[]
blockType?: string blockType?: string
initCollapsed?: boolean
} }
type SET_COLLAPSE = { type SET_COLLAPSE = {
@@ -48,13 +49,13 @@ const reducer = (currentState: Row[], action: Action): Row[] => {
switch (action.type) { switch (action.type) {
case 'SET_ALL': { case 'SET_ALL': {
const { data, collapsedState } = action; const { data, collapsedState, initCollapsed } = action;
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data.map((dataRow, i) => { return data.map((dataRow, i) => {
const row = { const row = {
id: dataRow?.id || new ObjectID().toHexString(), id: dataRow?.id || new ObjectID().toHexString(),
collapsed: (collapsedState || []).includes(dataRow?.id), collapsed: Array.isArray(collapsedState) ? collapsedState.includes(dataRow?.id) : initCollapsed,
blockType: dataRow?.blockType, blockType: dataRow?.blockType,
}; };

View File

@@ -11,6 +11,7 @@ export const baseAdminFields = joi.object().keys({
style: joi.object().unknown(), style: joi.object().unknown(),
className: joi.string(), className: joi.string(),
readOnly: joi.boolean().default(false), readOnly: joi.boolean().default(false),
initCollapsed: joi.boolean().default(false),
hidden: joi.boolean().default(false), hidden: joi.boolean().default(false),
disabled: joi.boolean().default(false), disabled: joi.boolean().default(false),
condition: joi.func(), condition: joi.func(),

View File

@@ -184,6 +184,9 @@ export type CollapsibleField = Omit<FieldBase, 'name'> & {
type: 'collapsible'; type: 'collapsible';
label: string label: string
fields: Field[]; fields: Field[];
admin?: Admin & {
initCollapsed?: boolean | false;
}
} }
export type TabsAdmin = Omit<Admin, 'description'>; export type TabsAdmin = Omit<Admin, 'description'>;
@@ -327,6 +330,9 @@ export type ArrayField = FieldBase & {
maxRows?: number; maxRows?: number;
labels?: Labels; labels?: Labels;
fields: Field[]; fields: Field[];
admin?: Admin & {
initCollapsed?: boolean | false;
}
} }
export type RadioField = FieldBase & { export type RadioField = FieldBase & {
@@ -352,6 +358,10 @@ export type BlockField = FieldBase & {
blocks: Block[]; blocks: Block[];
defaultValue?: unknown defaultValue?: unknown
labels?: Labels labels?: Labels
admin?: Admin & {
initCollapsed?: boolean | false;
}
} }
export type PointField = FieldBase & { export type PointField = FieldBase & {

View File

@@ -23,6 +23,20 @@ const ArrayFields: CollectionConfig = {
}, },
], ],
}, },
{
name: 'collapsedArray',
type: 'array',
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
admin: {
initCollapsed: true,
},
},
{ {
name: 'localized', name: 'localized',
type: 'array', type: 'array',
@@ -92,6 +106,11 @@ export const arrayDoc = {
text: 'sixth row', text: 'sixth row',
}, },
], ],
collapsedArray: [
{
text: 'initialize collapsed',
},
],
}; };
export default ArrayFields; export default ArrayFields;

View File

@@ -1,6 +1,36 @@
import type { CollectionConfig } from '../../../../src/collections/config/types'; import type { CollectionConfig } from '../../../../src/collections/config/types';
import { Field } from '../../../../src/fields/config/types'; import { Field } from '../../../../src/fields/config/types';
export const blocksFieldSeedData = [
{
blockName: 'First block',
blockType: 'text',
text: 'first block',
richText: [],
},
{
blockName: 'Second block',
blockType: 'number',
number: 342,
},
{
blockName: 'Sub-block demonstration',
blockType: 'subBlocks',
subBlocks: [
{
blockName: 'First sub block',
blockType: 'number',
number: 123,
},
{
blockName: 'Second sub block',
blockType: 'text',
text: 'second sub block',
},
],
},
] as const;
export const blocksField: Field = { export const blocksField: Field = {
name: 'blocks', name: 'blocks',
type: 'blocks', type: 'blocks',
@@ -104,12 +134,21 @@ export const blocksField: Field = {
], ],
}, },
], ],
defaultValue: blocksFieldSeedData,
}; };
const BlockFields: CollectionConfig = { const BlockFields: CollectionConfig = {
slug: 'block-fields', slug: 'block-fields',
fields: [ fields: [
blocksField, blocksField,
{
...blocksField,
name: 'collapsedByDefaultBlocks',
localized: true,
admin: {
initCollapsed: true,
},
},
{ {
...blocksField, ...blocksField,
name: 'localizedBlocks', name: 'localizedBlocks',
@@ -118,36 +157,6 @@ const BlockFields: CollectionConfig = {
], ],
}; };
export const blocksFieldSeedData = [
{
blockName: 'First block',
blockType: 'text',
text: 'first block',
richText: [],
},
{
blockName: 'Second block',
blockType: 'number',
number: 342,
},
{
blockName: 'Sub-block demonstration',
blockType: 'subBlocks',
subBlocks: [
{
blockName: 'First sub block',
blockType: 'number',
number: 123,
},
{
blockName: 'Second sub block',
blockType: 'text',
text: 'second sub block',
},
],
},
] as const;
export const blocksDoc = { export const blocksDoc = {
blocks: blocksFieldSeedData, blocks: blocksFieldSeedData,
localizedBlocks: blocksFieldSeedData, localizedBlocks: blocksFieldSeedData,

View File

@@ -9,6 +9,7 @@ const CollapsibleFields: CollectionConfig = {
type: 'collapsible', type: 'collapsible',
admin: { admin: {
description: 'This is a collapsible field.', description: 'This is a collapsible field.',
initCollapsed: false,
}, },
fields: [ fields: [
{ {
@@ -38,6 +39,40 @@ const CollapsibleFields: CollectionConfig = {
}, },
], ],
}, },
{
label: 'Collapsible Field - Collapsed by Default',
type: 'collapsible',
admin: {
description: 'This is a collapsible field.',
initCollapsed: true,
},
fields: [
{
name: 'someText',
type: 'text',
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'textWithinGroup',
type: 'text',
},
{
name: 'subGroup',
type: 'group',
fields: [
{
name: 'textWithinSubGroup',
type: 'text',
},
],
},
],
},
],
},
], ],
}; };