fix: remove lazy loading of array and blocks
This commit is contained in:
@@ -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> = (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<Row[]>(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 (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{label}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Collapse All
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Show All
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
<Droppable droppableId="array-drop">
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
const rowNumber = i + 1;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={row.id}
|
||||
draggableId={row.id}
|
||||
index={i}
|
||||
isDragDisabled={readOnly}
|
||||
>
|
||||
{(providedDrag) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
ref={providedDrag.innerRef}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
header={`${labels.singular} ${rowNumber >= 10 ? rowNumber : `0${rowNumber}`}`}
|
||||
actions={!readOnly ? (
|
||||
<ArrayAction
|
||||
rowCount={rows.length}
|
||||
duplicateRow={duplicateRow}
|
||||
addRow={addRow}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
index={i}
|
||||
/>
|
||||
) : undefined}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
This field requires at least
|
||||
{' '}
|
||||
{minRows
|
||||
? `${minRows} ${labels.plural}`
|
||||
: `1 ${labels.singular}`}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
This field has no
|
||||
{' '}
|
||||
{labels.plural}
|
||||
.
|
||||
</Banner>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Button
|
||||
onClick={() => addRow(value as number)}
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
iconPosition="left"
|
||||
>
|
||||
{`Add ${labels.singular}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default withCondition(ArrayFieldType);
|
||||
@@ -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> = (props) => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ArrayField {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
const baseClass = 'array-field';
|
||||
|
||||
export default ArrayFieldType;
|
||||
const ArrayFieldType: React.FC<Props> = (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<Row[]>(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 (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{label}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Collapse All
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Show All
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
<Droppable droppableId="array-drop">
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
const rowNumber = i + 1;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={row.id}
|
||||
draggableId={row.id}
|
||||
index={i}
|
||||
isDragDisabled={readOnly}
|
||||
>
|
||||
{(providedDrag) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
ref={providedDrag.innerRef}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
header={`${labels.singular} ${rowNumber >= 10 ? rowNumber : `0${rowNumber}`}`}
|
||||
actions={!readOnly ? (
|
||||
<ArrayAction
|
||||
rowCount={rows.length}
|
||||
duplicateRow={duplicateRow}
|
||||
addRow={addRow}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
index={i}
|
||||
/>
|
||||
) : undefined}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
This field requires at least
|
||||
{' '}
|
||||
{minRows
|
||||
? `${minRows} ${labels.plural}`
|
||||
: `1 ${labels.singular}`}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
This field has no
|
||||
{' '}
|
||||
{labels.plural}
|
||||
.
|
||||
</Banner>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Button
|
||||
onClick={() => addRow(value as number)}
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
iconPosition="left"
|
||||
>
|
||||
{`Add ${labels.singular}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default withCondition(ArrayFieldType);
|
||||
|
||||
@@ -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> = (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<number>();
|
||||
|
||||
const {
|
||||
showError,
|
||||
errorMessage,
|
||||
value,
|
||||
setValue,
|
||||
} = useField<number>({
|
||||
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<Row[]>(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 (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{label}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Collapse All
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Show All
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<Droppable
|
||||
droppableId="blocks-drop"
|
||||
isDropDisabled={readOnly}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{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 (
|
||||
<Draggable
|
||||
key={row.id}
|
||||
draggableId={row.id}
|
||||
index={i}
|
||||
isDragDisabled={readOnly}
|
||||
>
|
||||
{(providedDrag) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
ref={providedDrag.innerRef}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
header={(
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{rowNumber >= 10 ? rowNumber : `0${rowNumber}`}
|
||||
</span>
|
||||
<Pill
|
||||
pillStyle="white"
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`}
|
||||
>
|
||||
{blockToRender.labels.singular}
|
||||
</Pill>
|
||||
<SectionTitle
|
||||
path={`${path}.${i}.blockName`}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
actions={!readOnly ? (
|
||||
<React.Fragment>
|
||||
<Popup
|
||||
key={`${blockType}-${i}`}
|
||||
forceOpen={selectorIndexOpen === i}
|
||||
onToggleOpen={onAddPopupToggle}
|
||||
buttonType="none"
|
||||
size="large"
|
||||
horizontalAlign="right"
|
||||
render={({ close }) => (
|
||||
<BlockSelector
|
||||
blocks={blocks}
|
||||
addRow={addRow}
|
||||
addRowIndex={i}
|
||||
close={close}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ArrayAction
|
||||
rowCount={rows.length}
|
||||
duplicateRow={() => duplicateRow(i, blockType)}
|
||||
addRow={() => setSelectorIndexOpen(i)}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
index={i}
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : undefined}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
fieldSchema={blockToRender.fields.map((field) => ({
|
||||
...field,
|
||||
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
This field requires at least
|
||||
{' '}
|
||||
{`${minRows || 1} ${minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural}`}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
This field has no
|
||||
{' '}
|
||||
{labels.plural}
|
||||
.
|
||||
</Banner>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Popup
|
||||
buttonType="custom"
|
||||
size="large"
|
||||
horizontalAlign="left"
|
||||
button={(
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
>
|
||||
{`Add ${labels.singular}`}
|
||||
</Button>
|
||||
)}
|
||||
render={({ close }) => (
|
||||
<BlockSelector
|
||||
blocks={blocks}
|
||||
addRow={addRow}
|
||||
addRowIndex={value}
|
||||
close={close}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default withCondition(Blocks);
|
||||
@@ -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> = (props) => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Blocks {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
const baseClass = 'blocks-field';
|
||||
|
||||
export default BlocksField;
|
||||
const labelDefaults = {
|
||||
singular: 'Block',
|
||||
plural: 'Blocks',
|
||||
};
|
||||
|
||||
const Index: React.FC<Props> = (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<number>();
|
||||
|
||||
const {
|
||||
showError,
|
||||
errorMessage,
|
||||
value,
|
||||
setValue,
|
||||
} = useField<number>({
|
||||
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<Row[]>(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 (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{label}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Collapse All
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Show All
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<Droppable
|
||||
droppableId="blocks-drop"
|
||||
isDropDisabled={readOnly}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{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 (
|
||||
<Draggable
|
||||
key={row.id}
|
||||
draggableId={row.id}
|
||||
index={i}
|
||||
isDragDisabled={readOnly}
|
||||
>
|
||||
{(providedDrag) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
ref={providedDrag.innerRef}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
header={(
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{rowNumber >= 10 ? rowNumber : `0${rowNumber}`}
|
||||
</span>
|
||||
<Pill
|
||||
pillStyle="white"
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`}
|
||||
>
|
||||
{blockToRender.labels.singular}
|
||||
</Pill>
|
||||
<SectionTitle
|
||||
path={`${path}.${i}.blockName`}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
actions={!readOnly ? (
|
||||
<React.Fragment>
|
||||
<Popup
|
||||
key={`${blockType}-${i}`}
|
||||
forceOpen={selectorIndexOpen === i}
|
||||
onToggleOpen={onAddPopupToggle}
|
||||
buttonType="none"
|
||||
size="large"
|
||||
horizontalAlign="right"
|
||||
render={({ close }) => (
|
||||
<BlockSelector
|
||||
blocks={blocks}
|
||||
addRow={addRow}
|
||||
addRowIndex={i}
|
||||
close={close}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ArrayAction
|
||||
rowCount={rows.length}
|
||||
duplicateRow={() => duplicateRow(i, blockType)}
|
||||
addRow={() => setSelectorIndexOpen(i)}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
index={i}
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : undefined}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
fieldSchema={blockToRender.fields.map((field) => ({
|
||||
...field,
|
||||
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
This field requires at least
|
||||
{' '}
|
||||
{`${minRows || 1} ${minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural}`}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
This field has no
|
||||
{' '}
|
||||
{labels.plural}
|
||||
.
|
||||
</Banner>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Popup
|
||||
buttonType="custom"
|
||||
size="large"
|
||||
horizontalAlign="left"
|
||||
button={(
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
>
|
||||
{`Add ${labels.singular}`}
|
||||
</Button>
|
||||
)}
|
||||
render={({ close }) => (
|
||||
<BlockSelector
|
||||
blocks={blocks}
|
||||
addRow={addRow}
|
||||
addRowIndex={value}
|
||||
close={close}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default withCondition(Index);
|
||||
|
||||
Reference in New Issue
Block a user