From 4900fa799ffbeb70e689622b269dc04a67978552 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Tue, 23 Aug 2022 15:41:52 -0400 Subject: [PATCH] fix: remove lazy loading of array and blocks --- .../forms/field-types/Array/Array.tsx | 350 --------------- .../forms/field-types/Array/index.tsx | 355 ++++++++++++++- .../forms/field-types/Blocks/Blocks.tsx | 417 ----------------- .../forms/field-types/Blocks/index.tsx | 422 +++++++++++++++++- 4 files changed, 759 insertions(+), 785 deletions(-) delete mode 100644 src/admin/components/forms/field-types/Array/Array.tsx delete mode 100644 src/admin/components/forms/field-types/Blocks/Blocks.tsx diff --git a/src/admin/components/forms/field-types/Array/Array.tsx b/src/admin/components/forms/field-types/Array/Array.tsx deleted file mode 100644 index 2e7723493d..0000000000 --- a/src/admin/components/forms/field-types/Array/Array.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; -import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; -import { useAuth } from '../../../utilities/Auth'; -import withCondition from '../../withCondition'; -import Button from '../../../elements/Button'; -import reducer, { Row } from '../rowReducer'; -import { useForm } from '../../Form/context'; -import buildStateFromSchema from '../../Form/buildStateFromSchema'; -import useField from '../../useField'; -import { useLocale } from '../../../utilities/Locale'; -import Error from '../../Error'; -import { array } from '../../../../../fields/validations'; -import Banner from '../../../elements/Banner'; -import FieldDescription from '../../FieldDescription'; -import { useDocumentInfo } from '../../../utilities/DocumentInfo'; -import { useOperation } from '../../../utilities/OperationProvider'; -import { Collapsible } from '../../../elements/Collapsible'; -import RenderFields from '../../RenderFields'; -import { fieldAffectsData } from '../../../../../fields/config/types'; -import { Props } from './types'; -import { usePreferences } from '../../../utilities/Preferences'; -import { ArrayAction } from '../../../elements/ArrayAction'; -import { scrollToID } from '../../../../utilities/scrollToID'; -import HiddenInput from '../HiddenInput'; - -import './index.scss'; - -const baseClass = 'array-field'; - -const ArrayFieldType: React.FC = (props) => { - const { - name, - path: pathFromProps, - fields, - fieldTypes, - validate = array, - required, - maxRows, - minRows, - permissions, - admin: { - readOnly, - description, - condition, - className, - }, - } = props; - - const path = pathFromProps || name; - - // Handle labeling for Arrays, Global Arrays, and Blocks - const getLabels = (p: Props) => { - if (p?.labels) return p.labels; - if (p?.label) return { singular: p.label, plural: undefined }; - return { singular: 'Row', plural: 'Rows' }; - }; - - const labels = getLabels(props); - // eslint-disable-next-line react/destructuring-assignment - const label = props?.label ?? props?.labels?.singular; - - const { preferencesKey } = useDocumentInfo(); - const { getPreference } = usePreferences(); - const { setPreference } = usePreferences(); - const [rows, dispatchRows] = useReducer(reducer, undefined); - const formContext = useForm(); - const { user } = useAuth(); - const { id } = useDocumentInfo(); - const locale = useLocale(); - const operation = useOperation(); - - const { dispatchFields } = formContext; - - const memoizedValidate = useCallback((value, options) => { - return validate(value, { ...options, minRows, maxRows, required }); - }, [maxRows, minRows, required, validate]); - - const [disableFormData, setDisableFormData] = useState(false); - - const { - showError, - errorMessage, - value, - setValue, - } = useField({ - path, - validate: memoizedValidate, - disableFormData, - condition, - }); - - const addRow = useCallback(async (rowIndex: number) => { - const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale }); - dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path }); - dispatchRows({ type: 'ADD', rowIndex }); - setValue(value as number + 1); - - setTimeout(() => { - scrollToID(`${path}-row-${rowIndex + 1}`); - }, 0); - }, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user, locale]); - - const duplicateRow = useCallback(async (rowIndex: number) => { - dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path }); - dispatchRows({ type: 'ADD', rowIndex }); - setValue(value as number + 1); - - setTimeout(() => { - scrollToID(`${path}-row-${rowIndex + 1}`); - }, 0); - }, [dispatchRows, dispatchFields, path, setValue, value]); - - const removeRow = useCallback((rowIndex: number) => { - dispatchRows({ type: 'REMOVE', rowIndex }); - dispatchFields({ type: 'REMOVE_ROW', rowIndex, path }); - setValue(value as number - 1); - }, [dispatchRows, dispatchFields, path, value, setValue]); - - const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => { - dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex }); - dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path }); - }, [dispatchRows, dispatchFields, path]); - - const onDragEnd = useCallback((result) => { - if (!result.destination) return; - const sourceIndex = result.source.index; - const destinationIndex = result.destination.index; - moveRow(sourceIndex, destinationIndex); - }, [moveRow]); - - const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => { - dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed }); - - if (preferencesKey) { - const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; - let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed - .filter((filterID) => (rows.find((row) => row.id === filterID))) - || []; - - if (!collapsed) { - newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID); - } else { - newCollapsedState.push(rowID); - } - - setPreference(preferencesKey, { - ...preferencesToSet, - fields: { - ...preferencesToSet?.fields || {}, - [path]: { - ...preferencesToSet?.fields?.[path], - collapsed: newCollapsedState, - }, - }, - }); - } - }, [preferencesKey, path, setPreference, rows, getPreference]); - - const toggleCollapseAll = useCallback(async (collapse: boolean) => { - dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse }); - - if (preferencesKey) { - const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; - - setPreference(preferencesKey, { - ...preferencesToSet, - fields: { - ...preferencesToSet?.fields || {}, - [path]: { - ...preferencesToSet?.fields?.[path], - collapsed: collapse ? rows.map(({ id: rowID }) => rowID) : [], - }, - }, - }); - } - }, [path, getPreference, preferencesKey, rows, setPreference]); - - useEffect(() => { - const initializeRowState = async () => { - const data = formContext.getDataByPath(path); - const preferences = await getPreference(preferencesKey) || { fields: {} }; - dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed }); - }; - - initializeRowState(); - }, [formContext, path, getPreference, preferencesKey]); - - useEffect(() => { - setValue(rows?.length || 0, true); - - if (rows?.length === 0) { - setDisableFormData(false); - } else { - setDisableFormData(true); - } - }, [rows, setValue]); - - const hasMaxRows = maxRows && rows?.length >= maxRows; - - const classes = [ - 'field-type', - baseClass, - className, - ].filter(Boolean).join(' '); - - if (!rows) return null; - - return ( - -
-
- -
-
-
-

{label}

-
    -
  • - -
  • -
  • - -
  • -
-
- -
- - {(provided) => ( -
- {rows.length > 0 && rows.map((row, i) => { - const rowNumber = i + 1; - - return ( - - {(providedDrag) => ( -
- setCollapse(row.id, collapsed)} - className={`${baseClass}__row`} - key={row.id} - dragHandleProps={providedDrag.dragHandleProps} - header={`${labels.singular} ${rowNumber >= 10 ? rowNumber : `0${rowNumber}`}`} - actions={!readOnly ? ( - - ) : undefined} - > - - ({ - ...field, - path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`, - }))} - /> - - -
- )} -
- ); - })} - {(rows.length < minRows || (required && rows.length === 0)) && ( - - This field requires at least - {' '} - {minRows - ? `${minRows} ${labels.plural}` - : `1 ${labels.singular}`} - - )} - {(rows.length === 0 && readOnly) && ( - - This field has no - {' '} - {labels.plural} - . - - )} - {provided.placeholder} -
- )} -
- {(!readOnly && !hasMaxRows) && ( -
- -
- )} -
-
- ); -}; - -export default withCondition(ArrayFieldType); diff --git a/src/admin/components/forms/field-types/Array/index.tsx b/src/admin/components/forms/field-types/Array/index.tsx index 831fe49a87..2e7723493d 100644 --- a/src/admin/components/forms/field-types/Array/index.tsx +++ b/src/admin/components/forms/field-types/Array/index.tsx @@ -1,13 +1,350 @@ -import React, { Suspense, lazy } from 'react'; -import Loading from '../../../elements/Loading'; +import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; +import { useAuth } from '../../../utilities/Auth'; +import withCondition from '../../withCondition'; +import Button from '../../../elements/Button'; +import reducer, { Row } from '../rowReducer'; +import { useForm } from '../../Form/context'; +import buildStateFromSchema from '../../Form/buildStateFromSchema'; +import useField from '../../useField'; +import { useLocale } from '../../../utilities/Locale'; +import Error from '../../Error'; +import { array } from '../../../../../fields/validations'; +import Banner from '../../../elements/Banner'; +import FieldDescription from '../../FieldDescription'; +import { useDocumentInfo } from '../../../utilities/DocumentInfo'; +import { useOperation } from '../../../utilities/OperationProvider'; +import { Collapsible } from '../../../elements/Collapsible'; +import RenderFields from '../../RenderFields'; +import { fieldAffectsData } from '../../../../../fields/config/types'; import { Props } from './types'; +import { usePreferences } from '../../../utilities/Preferences'; +import { ArrayAction } from '../../../elements/ArrayAction'; +import { scrollToID } from '../../../../utilities/scrollToID'; +import HiddenInput from '../HiddenInput'; -const ArrayField = lazy(() => import('./Array')); +import './index.scss'; -const ArrayFieldType: React.FC = (props) => ( - }> - - -); +const baseClass = 'array-field'; -export default ArrayFieldType; +const ArrayFieldType: React.FC = (props) => { + const { + name, + path: pathFromProps, + fields, + fieldTypes, + validate = array, + required, + maxRows, + minRows, + permissions, + admin: { + readOnly, + description, + condition, + className, + }, + } = props; + + const path = pathFromProps || name; + + // Handle labeling for Arrays, Global Arrays, and Blocks + const getLabels = (p: Props) => { + if (p?.labels) return p.labels; + if (p?.label) return { singular: p.label, plural: undefined }; + return { singular: 'Row', plural: 'Rows' }; + }; + + const labels = getLabels(props); + // eslint-disable-next-line react/destructuring-assignment + const label = props?.label ?? props?.labels?.singular; + + const { preferencesKey } = useDocumentInfo(); + const { getPreference } = usePreferences(); + const { setPreference } = usePreferences(); + const [rows, dispatchRows] = useReducer(reducer, undefined); + const formContext = useForm(); + const { user } = useAuth(); + const { id } = useDocumentInfo(); + const locale = useLocale(); + const operation = useOperation(); + + const { dispatchFields } = formContext; + + const memoizedValidate = useCallback((value, options) => { + return validate(value, { ...options, minRows, maxRows, required }); + }, [maxRows, minRows, required, validate]); + + const [disableFormData, setDisableFormData] = useState(false); + + const { + showError, + errorMessage, + value, + setValue, + } = useField({ + path, + validate: memoizedValidate, + disableFormData, + condition, + }); + + const addRow = useCallback(async (rowIndex: number) => { + const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale }); + dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path }); + dispatchRows({ type: 'ADD', rowIndex }); + setValue(value as number + 1); + + setTimeout(() => { + scrollToID(`${path}-row-${rowIndex + 1}`); + }, 0); + }, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user, locale]); + + const duplicateRow = useCallback(async (rowIndex: number) => { + dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path }); + dispatchRows({ type: 'ADD', rowIndex }); + setValue(value as number + 1); + + setTimeout(() => { + scrollToID(`${path}-row-${rowIndex + 1}`); + }, 0); + }, [dispatchRows, dispatchFields, path, setValue, value]); + + const removeRow = useCallback((rowIndex: number) => { + dispatchRows({ type: 'REMOVE', rowIndex }); + dispatchFields({ type: 'REMOVE_ROW', rowIndex, path }); + setValue(value as number - 1); + }, [dispatchRows, dispatchFields, path, value, setValue]); + + const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => { + dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex }); + dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path }); + }, [dispatchRows, dispatchFields, path]); + + const onDragEnd = useCallback((result) => { + if (!result.destination) return; + const sourceIndex = result.source.index; + const destinationIndex = result.destination.index; + moveRow(sourceIndex, destinationIndex); + }, [moveRow]); + + const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => { + dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed }); + + if (preferencesKey) { + const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; + let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed + .filter((filterID) => (rows.find((row) => row.id === filterID))) + || []; + + if (!collapsed) { + newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID); + } else { + newCollapsedState.push(rowID); + } + + setPreference(preferencesKey, { + ...preferencesToSet, + fields: { + ...preferencesToSet?.fields || {}, + [path]: { + ...preferencesToSet?.fields?.[path], + collapsed: newCollapsedState, + }, + }, + }); + } + }, [preferencesKey, path, setPreference, rows, getPreference]); + + const toggleCollapseAll = useCallback(async (collapse: boolean) => { + dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse }); + + if (preferencesKey) { + const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; + + setPreference(preferencesKey, { + ...preferencesToSet, + fields: { + ...preferencesToSet?.fields || {}, + [path]: { + ...preferencesToSet?.fields?.[path], + collapsed: collapse ? rows.map(({ id: rowID }) => rowID) : [], + }, + }, + }); + } + }, [path, getPreference, preferencesKey, rows, setPreference]); + + useEffect(() => { + const initializeRowState = async () => { + const data = formContext.getDataByPath(path); + const preferences = await getPreference(preferencesKey) || { fields: {} }; + dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed }); + }; + + initializeRowState(); + }, [formContext, path, getPreference, preferencesKey]); + + useEffect(() => { + setValue(rows?.length || 0, true); + + if (rows?.length === 0) { + setDisableFormData(false); + } else { + setDisableFormData(true); + } + }, [rows, setValue]); + + const hasMaxRows = maxRows && rows?.length >= maxRows; + + const classes = [ + 'field-type', + baseClass, + className, + ].filter(Boolean).join(' '); + + if (!rows) return null; + + return ( + +
+
+ +
+
+
+

{label}

+
    +
  • + +
  • +
  • + +
  • +
+
+ +
+ + {(provided) => ( +
+ {rows.length > 0 && rows.map((row, i) => { + const rowNumber = i + 1; + + return ( + + {(providedDrag) => ( +
+ setCollapse(row.id, collapsed)} + className={`${baseClass}__row`} + key={row.id} + dragHandleProps={providedDrag.dragHandleProps} + header={`${labels.singular} ${rowNumber >= 10 ? rowNumber : `0${rowNumber}`}`} + actions={!readOnly ? ( + + ) : undefined} + > + + ({ + ...field, + path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`, + }))} + /> + + +
+ )} +
+ ); + })} + {(rows.length < minRows || (required && rows.length === 0)) && ( + + This field requires at least + {' '} + {minRows + ? `${minRows} ${labels.plural}` + : `1 ${labels.singular}`} + + )} + {(rows.length === 0 && readOnly) && ( + + This field has no + {' '} + {labels.plural} + . + + )} + {provided.placeholder} +
+ )} +
+ {(!readOnly && !hasMaxRows) && ( +
+ +
+ )} +
+
+ ); +}; + +export default withCondition(ArrayFieldType); diff --git a/src/admin/components/forms/field-types/Blocks/Blocks.tsx b/src/admin/components/forms/field-types/Blocks/Blocks.tsx deleted file mode 100644 index dda9e502cc..0000000000 --- a/src/admin/components/forms/field-types/Blocks/Blocks.tsx +++ /dev/null @@ -1,417 +0,0 @@ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; -import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; - -import { useAuth } from '../../../utilities/Auth'; -import { usePreferences } from '../../../utilities/Preferences'; -import { useLocale } from '../../../utilities/Locale'; -import withCondition from '../../withCondition'; -import Button from '../../../elements/Button'; -import reducer, { Row } from '../rowReducer'; -import { useDocumentInfo } from '../../../utilities/DocumentInfo'; -import { useForm } from '../../Form/context'; -import buildStateFromSchema from '../../Form/buildStateFromSchema'; -import Error from '../../Error'; -import useField from '../../useField'; -import Popup from '../../../elements/Popup'; -import BlockSelector from './BlockSelector'; -import { blocks as blocksValidator } from '../../../../../fields/validations'; -import Banner from '../../../elements/Banner'; -import FieldDescription from '../../FieldDescription'; -import { Props } from './types'; -import { useOperation } from '../../../utilities/OperationProvider'; -import { Collapsible } from '../../../elements/Collapsible'; -import { ArrayAction } from '../../../elements/ArrayAction'; -import RenderFields from '../../RenderFields'; -import { fieldAffectsData } from '../../../../../fields/config/types'; -import SectionTitle from './SectionTitle'; -import Pill from '../../../elements/Pill'; -import { scrollToID } from '../../../../utilities/scrollToID'; -import HiddenInput from '../HiddenInput'; - -import './index.scss'; - -const baseClass = 'blocks-field'; - -const labelDefaults = { - singular: 'Block', - plural: 'Blocks', -}; - -const Blocks: React.FC = (props) => { - const { - label, - name, - path: pathFromProps, - blocks, - labels = labelDefaults, - fieldTypes, - maxRows, - minRows, - required, - validate = blocksValidator, - permissions, - admin: { - readOnly, - description, - condition, - className, - }, - } = props; - - const path = pathFromProps || name; - - const { preferencesKey } = useDocumentInfo(); - const { getPreference } = usePreferences(); - const { setPreference } = usePreferences(); - const [rows, dispatchRows] = useReducer(reducer, undefined); - const formContext = useForm(); - const { user } = useAuth(); - const { id } = useDocumentInfo(); - const locale = useLocale(); - const operation = useOperation(); - const { dispatchFields } = formContext; - - const memoizedValidate = useCallback((value, options) => { - return validate(value, { ...options, minRows, maxRows, required }); - }, [maxRows, minRows, required, validate]); - - const [disableFormData, setDisableFormData] = useState(false); - const [selectorIndexOpen, setSelectorIndexOpen] = useState(); - - const { - showError, - errorMessage, - value, - setValue, - } = useField({ - path, - validate: memoizedValidate, - disableFormData, - condition, - }); - - const onAddPopupToggle = useCallback((open) => { - if (!open) { - setSelectorIndexOpen(undefined); - } - }, []); - - const addRow = useCallback(async (rowIndex: number, blockType: string) => { - const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType); - const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale }); - dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType }); - dispatchRows({ type: 'ADD', rowIndex, blockType }); - setValue(value as number + 1); - - setTimeout(() => { - scrollToID(`${path}-row-${rowIndex + 1}`); - }, 0); - }, [path, setValue, value, blocks, dispatchFields, operation, id, user, locale]); - - const duplicateRow = useCallback(async (rowIndex: number, blockType: string) => { - dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path }); - dispatchRows({ type: 'ADD', rowIndex, blockType }); - setValue(value as number + 1); - - setTimeout(() => { - scrollToID(`${path}-row-${rowIndex + 1}`); - }, 0); - }, [dispatchRows, dispatchFields, path, setValue, value]); - - const removeRow = useCallback((rowIndex: number) => { - dispatchRows({ type: 'REMOVE', rowIndex }); - dispatchFields({ type: 'REMOVE_ROW', rowIndex, path }); - setValue(value as number - 1); - }, [path, setValue, value, dispatchFields]); - - const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => { - dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex }); - dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path }); - }, [dispatchRows, dispatchFields, path]); - - const onDragEnd = useCallback((result) => { - if (!result.destination) return; - const sourceIndex = result.source.index; - const destinationIndex = result.destination.index; - moveRow(sourceIndex, destinationIndex); - }, [moveRow]); - - const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => { - dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed }); - - if (preferencesKey) { - const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; - let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed - .filter((filterID) => (rows.find((row) => row.id === filterID))) - || []; - - if (!collapsed) { - newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID); - } else { - newCollapsedState.push(rowID); - } - - setPreference(preferencesKey, { - ...preferencesToSet, - fields: { - ...preferencesToSet?.fields || {}, - [path]: { - ...preferencesToSet?.fields?.[path], - collapsed: newCollapsedState, - }, - }, - }); - } - }, [preferencesKey, getPreference, path, setPreference, rows]); - - const toggleCollapseAll = useCallback(async (collapse: boolean) => { - dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse }); - - if (preferencesKey) { - const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; - - setPreference(preferencesKey, { - ...preferencesToSet, - fields: { - ...preferencesToSet?.fields || {}, - [path]: { - ...preferencesToSet?.fields?.[path], - collapsed: collapse ? rows.map(({ id: rowID }) => rowID) : [], - }, - }, - }); - } - }, [getPreference, path, preferencesKey, rows, setPreference]); - - // Set row count on mount and when form context is reset - useEffect(() => { - const initializeRowState = async () => { - const data = formContext.getDataByPath(path); - const preferences = await getPreference(preferencesKey) || { fields: {} }; - dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed }); - }; - - initializeRowState(); - }, [formContext, path, getPreference, preferencesKey]); - - useEffect(() => { - setValue(rows?.length || 0, true); - - if (rows?.length === 0) { - setDisableFormData(false); - } else { - setDisableFormData(true); - } - }, [rows, setValue]); - - const hasMaxRows = maxRows && rows?.length >= maxRows; - - const classes = [ - 'field-type', - baseClass, - className, - ].filter(Boolean).join(' '); - - if (!rows) return null; - - return ( - -
-
- -
-
-
-

{label}

-
    -
  • - -
  • -
  • - -
  • -
-
- -
- - - {(provided) => ( -
- {rows.length > 0 && rows.map((row, i) => { - const { blockType } = row; - const blockToRender = blocks.find((block) => block.slug === blockType); - - const rowNumber = i + 1; - - if (blockToRender) { - return ( - - {(providedDrag) => ( -
- setCollapse(row.id, collapsed)} - className={`${baseClass}__row`} - key={row.id} - dragHandleProps={providedDrag.dragHandleProps} - header={( -
- - {rowNumber >= 10 ? rowNumber : `0${rowNumber}`} - - - {blockToRender.labels.singular} - - -
- )} - actions={!readOnly ? ( - - ( - - )} - /> - duplicateRow(i, blockType)} - addRow={() => setSelectorIndexOpen(i)} - moveRow={moveRow} - removeRow={removeRow} - index={i} - /> - - ) : undefined} - > - - ({ - ...field, - path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`, - }))} - /> - -
-
- )} -
- ); - } - - return null; - })} - {(rows.length < minRows || (required && rows.length === 0)) && ( - - This field requires at least - {' '} - {`${minRows || 1} ${minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural}`} - - )} - {(rows.length === 0 && readOnly) && ( - - This field has no - {' '} - {labels.plural} - . - - )} - {provided.placeholder} -
- )} -
- - {(!readOnly && !hasMaxRows) && ( -
- - {`Add ${labels.singular}`} - - )} - render={({ close }) => ( - - )} - /> -
- )} -
-
- ); -}; - -export default withCondition(Blocks); diff --git a/src/admin/components/forms/field-types/Blocks/index.tsx b/src/admin/components/forms/field-types/Blocks/index.tsx index 97c6d1c8ba..ea09fb458a 100644 --- a/src/admin/components/forms/field-types/Blocks/index.tsx +++ b/src/admin/components/forms/field-types/Blocks/index.tsx @@ -1,13 +1,417 @@ -import React, { Suspense, lazy } from 'react'; -import Loading from '../../../elements/Loading'; +import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; + +import { useAuth } from '../../../utilities/Auth'; +import { usePreferences } from '../../../utilities/Preferences'; +import { useLocale } from '../../../utilities/Locale'; +import withCondition from '../../withCondition'; +import Button from '../../../elements/Button'; +import reducer, { Row } from '../rowReducer'; +import { useDocumentInfo } from '../../../utilities/DocumentInfo'; +import { useForm } from '../../Form/context'; +import buildStateFromSchema from '../../Form/buildStateFromSchema'; +import Error from '../../Error'; +import useField from '../../useField'; +import Popup from '../../../elements/Popup'; +import BlockSelector from './BlockSelector'; +import { blocks as blocksValidator } from '../../../../../fields/validations'; +import Banner from '../../../elements/Banner'; +import FieldDescription from '../../FieldDescription'; import { Props } from './types'; +import { useOperation } from '../../../utilities/OperationProvider'; +import { Collapsible } from '../../../elements/Collapsible'; +import { ArrayAction } from '../../../elements/ArrayAction'; +import RenderFields from '../../RenderFields'; +import { fieldAffectsData } from '../../../../../fields/config/types'; +import SectionTitle from './SectionTitle'; +import Pill from '../../../elements/Pill'; +import { scrollToID } from '../../../../utilities/scrollToID'; +import HiddenInput from '../HiddenInput'; -const Blocks = lazy(() => import('./Blocks')); +import './index.scss'; -const BlocksField: React.FC = (props) => ( - }> - - -); +const baseClass = 'blocks-field'; -export default BlocksField; +const labelDefaults = { + singular: 'Block', + plural: 'Blocks', +}; + +const Index: React.FC = (props) => { + const { + label, + name, + path: pathFromProps, + blocks, + labels = labelDefaults, + fieldTypes, + maxRows, + minRows, + required, + validate = blocksValidator, + permissions, + admin: { + readOnly, + description, + condition, + className, + }, + } = props; + + const path = pathFromProps || name; + + const { preferencesKey } = useDocumentInfo(); + const { getPreference } = usePreferences(); + const { setPreference } = usePreferences(); + const [rows, dispatchRows] = useReducer(reducer, undefined); + const formContext = useForm(); + const { user } = useAuth(); + const { id } = useDocumentInfo(); + const locale = useLocale(); + const operation = useOperation(); + const { dispatchFields } = formContext; + + const memoizedValidate = useCallback((value, options) => { + return validate(value, { ...options, minRows, maxRows, required }); + }, [maxRows, minRows, required, validate]); + + const [disableFormData, setDisableFormData] = useState(false); + const [selectorIndexOpen, setSelectorIndexOpen] = useState(); + + const { + showError, + errorMessage, + value, + setValue, + } = useField({ + path, + validate: memoizedValidate, + disableFormData, + condition, + }); + + const onAddPopupToggle = useCallback((open) => { + if (!open) { + setSelectorIndexOpen(undefined); + } + }, []); + + const addRow = useCallback(async (rowIndex: number, blockType: string) => { + const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType); + const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale }); + dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType }); + dispatchRows({ type: 'ADD', rowIndex, blockType }); + setValue(value as number + 1); + + setTimeout(() => { + scrollToID(`${path}-row-${rowIndex + 1}`); + }, 0); + }, [path, setValue, value, blocks, dispatchFields, operation, id, user, locale]); + + const duplicateRow = useCallback(async (rowIndex: number, blockType: string) => { + dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path }); + dispatchRows({ type: 'ADD', rowIndex, blockType }); + setValue(value as number + 1); + + setTimeout(() => { + scrollToID(`${path}-row-${rowIndex + 1}`); + }, 0); + }, [dispatchRows, dispatchFields, path, setValue, value]); + + const removeRow = useCallback((rowIndex: number) => { + dispatchRows({ type: 'REMOVE', rowIndex }); + dispatchFields({ type: 'REMOVE_ROW', rowIndex, path }); + setValue(value as number - 1); + }, [path, setValue, value, dispatchFields]); + + const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => { + dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex }); + dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path }); + }, [dispatchRows, dispatchFields, path]); + + const onDragEnd = useCallback((result) => { + if (!result.destination) return; + const sourceIndex = result.source.index; + const destinationIndex = result.destination.index; + moveRow(sourceIndex, destinationIndex); + }, [moveRow]); + + const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => { + dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed }); + + if (preferencesKey) { + const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; + let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed + .filter((filterID) => (rows.find((row) => row.id === filterID))) + || []; + + if (!collapsed) { + newCollapsedState = newCollapsedState.filter((existingID) => existingID !== rowID); + } else { + newCollapsedState.push(rowID); + } + + setPreference(preferencesKey, { + ...preferencesToSet, + fields: { + ...preferencesToSet?.fields || {}, + [path]: { + ...preferencesToSet?.fields?.[path], + collapsed: newCollapsedState, + }, + }, + }); + } + }, [preferencesKey, getPreference, path, setPreference, rows]); + + const toggleCollapseAll = useCallback(async (collapse: boolean) => { + dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse }); + + if (preferencesKey) { + const preferencesToSet = await getPreference(preferencesKey) || { fields: {} }; + + setPreference(preferencesKey, { + ...preferencesToSet, + fields: { + ...preferencesToSet?.fields || {}, + [path]: { + ...preferencesToSet?.fields?.[path], + collapsed: collapse ? rows.map(({ id: rowID }) => rowID) : [], + }, + }, + }); + } + }, [getPreference, path, preferencesKey, rows, setPreference]); + + // Set row count on mount and when form context is reset + useEffect(() => { + const initializeRowState = async () => { + const data = formContext.getDataByPath(path); + const preferences = await getPreference(preferencesKey) || { fields: {} }; + dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed }); + }; + + initializeRowState(); + }, [formContext, path, getPreference, preferencesKey]); + + useEffect(() => { + setValue(rows?.length || 0, true); + + if (rows?.length === 0) { + setDisableFormData(false); + } else { + setDisableFormData(true); + } + }, [rows, setValue]); + + const hasMaxRows = maxRows && rows?.length >= maxRows; + + const classes = [ + 'field-type', + baseClass, + className, + ].filter(Boolean).join(' '); + + if (!rows) return null; + + return ( + +
+
+ +
+
+
+

{label}

+
    +
  • + +
  • +
  • + +
  • +
+
+ +
+ + + {(provided) => ( +
+ {rows.length > 0 && rows.map((row, i) => { + const { blockType } = row; + const blockToRender = blocks.find((block) => block.slug === blockType); + + const rowNumber = i + 1; + + if (blockToRender) { + return ( + + {(providedDrag) => ( +
+ setCollapse(row.id, collapsed)} + className={`${baseClass}__row`} + key={row.id} + dragHandleProps={providedDrag.dragHandleProps} + header={( +
+ + {rowNumber >= 10 ? rowNumber : `0${rowNumber}`} + + + {blockToRender.labels.singular} + + +
+ )} + actions={!readOnly ? ( + + ( + + )} + /> + duplicateRow(i, blockType)} + addRow={() => setSelectorIndexOpen(i)} + moveRow={moveRow} + removeRow={removeRow} + index={i} + /> + + ) : undefined} + > + + ({ + ...field, + path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`, + }))} + /> + +
+
+ )} +
+ ); + } + + return null; + })} + {(rows.length < minRows || (required && rows.length === 0)) && ( + + This field requires at least + {' '} + {`${minRows || 1} ${minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural}`} + + )} + {(rows.length === 0 && readOnly) && ( + + This field has no + {' '} + {labels.plural} + . + + )} + {provided.placeholder} +
+ )} +
+ + {(!readOnly && !hasMaxRows) && ( +
+ + {`Add ${labels.singular}`} + + )} + render={({ close }) => ( + + )} + /> +
+ )} +
+
+ ); +}; + +export default withCondition(Index);