implements initial form state

This commit is contained in:
James
2020-07-27 11:53:00 -04:00
parent d38b8e746c
commit cc4794e5ac
43 changed files with 733 additions and 625 deletions

View File

@@ -16,7 +16,7 @@ module.exports = {
read: () => true,
},
preview: (doc, token) => {
if (doc.title) {
if (doc && doc.title) {
return `http://localhost:3000/posts/${doc.title.value}?preview=true&token=${token}`;
}

View File

@@ -29,7 +29,7 @@
outline: 0;
padding: base(.5);
color: $color-dark-gray;
line-height: 1;
line-height: base(1);
&:hover:not(.clickable-arrow--is-disabled) {
background: $color-background-gray;

View File

@@ -8,7 +8,7 @@ import './index.scss';
const baseClass = 'editable-block-title';
const EditableBlockTitle = (props) => {
const { path, initialData } = props;
const { path } = props;
const inputRef = useRef(null);
const inputCloneRef = useRef(null);
const [inputWidth, setInputWidth] = useState(0);
@@ -18,7 +18,6 @@ const EditableBlockTitle = (props) => {
setValue,
} = useFieldType({
path,
initialData,
});
useEffect(() => {
@@ -31,7 +30,7 @@ const EditableBlockTitle = (props) => {
};
return (
<>
<React.Fragment>
<div className={baseClass}>
<input
ref={inputRef}
@@ -53,17 +52,12 @@ const EditableBlockTitle = (props) => {
>
{value || 'Untitled'}
</span>
</>
</React.Fragment>
);
};
EditableBlockTitle.defaultProps = {
initialData: undefined,
};
EditableBlockTitle.propTypes = {
path: PropTypes.string.isRequired,
initialData: PropTypes.string,
};
export default EditableBlockTitle;

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import AnimateHeight from 'react-animate-height';
import { Draggable } from 'react-beautiful-dnd';
@@ -22,17 +22,16 @@ const DraggableSection = (props) => {
rowCount,
parentPath,
fieldSchema,
initialData,
singularLabel,
blockType,
fieldTypes,
customComponentsPath,
isOpen,
toggleRowCollapse,
id,
positionPanelVerticalAlignment,
actionPanelVerticalAlignment,
toggleRowCollapse,
permissions,
isOpen,
} = props;
const [isHovered, setIsHovered] = useState(false);
@@ -73,7 +72,6 @@ const DraggableSection = (props) => {
<div className={`${baseClass}__section-header`}>
<SectionTitle
label={singularLabel}
initialData={initialData?.blockName}
path={`${parentPath}.${rowIndex}.blockName`}
/>
@@ -92,7 +90,6 @@ const DraggableSection = (props) => {
duration={0}
>
<RenderFields
initialData={initialData}
customComponentsPath={customComponentsPath}
fieldTypes={fieldTypes}
key={rowIndex}

View File

@@ -0,0 +1,101 @@
const buildValidationPromise = async (fieldState, field) => {
const validatedFieldState = fieldState;
validatedFieldState.valid = typeof field.validate === 'function' ? await field.validate(fieldState.value, field) : true;
if (typeof validatedFieldState.valid === 'string') {
validatedFieldState.errorMessage = validatedFieldState.valid;
validatedFieldState.valid = false;
}
};
const buildStateFromSchema = async (fieldSchema, fullData) => {
if (fieldSchema && fullData) {
const validationPromises = [];
const structureFieldState = (field, data = {}) => {
const value = data[field.name] || field.defaultValue;
const fieldState = {
value,
initialValue: value,
};
validationPromises.push(buildValidationPromise(fieldState, field));
return fieldState;
};
const iterateFields = (fields, data, path = '') => fields.reduce((state, field) => {
if (field.name && data[field.name]) {
if (Array.isArray(data[field.name])) {
if (field.type === 'array') {
return {
...state,
...data[field.name].reduce((rowState, row, i) => ({
...rowState,
...iterateFields(field.fields, row, `${path}${field.name}.${i}.`),
}), {}),
};
}
if (field.type === 'blocks') {
return {
...state,
...data[field.name].reduce((rowState, row, i) => {
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
const rowPath = `${path}${field.name}.${i}.`;
return {
...rowState,
[`${rowPath}blockType`]: {
value: row.blockType,
initialValue: row.blockType,
valid: true,
},
[`${rowPath}blockName`]: {
value: row.blockName,
initialValue: row.blockName,
valid: true,
},
...iterateFields(block.fields, row, rowPath),
};
}, {}),
};
}
}
if (field.fields) {
return {
...state,
...iterateFields(field.fields, data[field.name], `${path}${field.name}.`),
};
}
return {
...state,
[`${path}${field.name}`]: structureFieldState(field, data),
};
}
if (field.fields) {
return {
...state,
...iterateFields(field.fields, data, path),
};
}
return state;
}, {});
const resultingState = iterateFields(fieldSchema, fullData);
await Promise.all(validationPromises);
return resultingState;
}
return {};
};
module.exports = buildStateFromSchema;

View File

@@ -1,9 +1,43 @@
import { unflatten, flatten } from 'flatley';
import flattenFilters from './flattenFilters';
//
const unflattenRowsFromState = (state, path) => {
// Take a copy of state
const remainingFlattenedState = { ...state };
const rowsFromStateObject = {};
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
// Loop over all keys from state
// If the key begins with the name of the parent field,
// Add value to rowsFromStateObject and delete it from remaining state
Object.keys(state).forEach((key) => {
if (key.indexOf(`${path}.`) === 0) {
if (!state[key].ignoreWhileFlattening) {
const name = key.replace(pathPrefixToRemove, '');
rowsFromStateObject[name] = state[key];
rowsFromStateObject[name].initialValue = rowsFromStateObject[name].value;
}
delete remainingFlattenedState[key];
}
});
const unflattenedRows = unflatten(rowsFromStateObject);
return {
unflattenedRows: unflattenedRows[path.replace(pathPrefixToRemove, '')] || [],
remainingFlattenedState,
};
};
function fieldReducer(state, action) {
switch (action.type) {
case 'REPLACE_ALL':
return {
...action.value,
};
case 'REPLACE_STATE': {
return action.state;
}
case 'REMOVE': {
const newState = { ...state };
@@ -11,15 +45,112 @@ function fieldReducer(state, action) {
return newState;
}
case 'REMOVE_ROW': {
const { rowIndex, path } = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
unflattenedRows.splice(rowIndex, 1);
const flattenedRowState = unflattenedRows.length > 0 ? flatten({ [path]: unflattenedRows }, { filters: flattenFilters }) : {};
return {
...remainingFlattenedState,
...flattenedRowState,
};
}
case 'ADD_ROW': {
const {
rowIndex, path, fieldSchema, blockType,
} = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
// Get paths of sub fields
const subFields = fieldSchema.reduce((acc, field) => {
if (field.type === 'flexible' || field.type === 'repeater') {
return acc;
}
if (field.name) {
return {
...acc,
[field.name]: {
value: null,
valid: !field.required,
},
};
}
if (field.fields) {
return {
...acc,
...(field.fields.reduce((fields, subField) => ({
...fields,
[subField.name]: {
value: null,
valid: !field.required,
},
}), {})),
};
}
return acc;
}, {});
if (blockType) {
subFields.blockType = {
value: blockType,
initialValue: blockType,
valid: true,
};
subFields.blockName = {
value: null,
initialValue: null,
valid: true,
};
}
// Add new object containing subfield names to unflattenedRows array
unflattenedRows.splice(rowIndex + 1, 0, subFields);
const newState = {
...remainingFlattenedState,
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
};
return newState;
}
case 'MOVE_ROW': {
const { moveFromIndex, moveToIndex, path } = action;
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
// copy the row to move
const copyOfMovingRow = unflattenedRows[moveFromIndex];
// delete the row by index
unflattenedRows.splice(moveFromIndex, 1);
// insert row copyOfMovingRow back in
unflattenedRows.splice(moveToIndex, 0, copyOfMovingRow);
const newState = {
...remainingFlattenedState,
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
};
return newState;
}
default: {
const newField = {
value: action.value,
valid: action.valid,
errorMessage: action.errorMessage,
disableFormData: action.disableFormData,
ignoreWhileFlattening: action.ignoreWhileFlattening,
initialValue: action.initialValue,
};
if (action.disableFormData) newField.disableFormData = action.disableFormData;
return {
...state,
[action.path]: newField,

View File

@@ -0,0 +1,10 @@
const flattenFilters = [{
test: (_, value) => {
const hasValidProperty = Object.prototype.hasOwnProperty.call(value, 'valid');
const hasValueProperty = Object.prototype.hasOwnProperty.call(value, 'value');
return (hasValidProperty && hasValueProperty);
},
}];
export default flattenFilters;

View File

@@ -1,11 +1,10 @@
import React, {
useReducer, useEffect, useRef, useState,
useReducer, useEffect, useRef, useState, useCallback,
} from 'react';
import { objectToFormData } from 'object-to-formdata';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { unflatten } from 'flatley';
import HiddenInput from '../field-types/HiddenInput';
import { useLocale } from '../../utilities/Locale';
import { useStatusList } from '../../elements/Status';
import { requests } from '../../../api';
@@ -40,7 +39,6 @@ const Form = (props) => {
const {
disabled,
onSubmit,
ajax,
method,
action,
handleResponse,
@@ -49,6 +47,7 @@ const Form = (props) => {
className,
redirect,
disableSuccessStatus,
initialState,
} = props;
const history = useHistory();
@@ -62,11 +61,12 @@ const Form = (props) => {
const contextRef = useRef({ ...initContextState });
contextRef.current.initialState = initialState;
const [fields, dispatchFields] = useReducer(fieldReducer, {});
contextRef.current.fields = fields;
contextRef.current.dispatchFields = dispatchFields;
contextRef.current.submit = (e) => {
const submit = useCallback((e) => {
if (disabled) {
e.preventDefault();
return false;
@@ -100,121 +100,129 @@ const Form = (props) => {
return onSubmit(fields);
}
// If form is AJAX, fetch data
if (ajax !== false) {
e.preventDefault();
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth',
});
window.scrollTo({
top: 0,
behavior: 'smooth',
});
const formData = contextRef.current.createFormData();
setProcessing(true);
const formData = contextRef.current.createFormData();
setProcessing(true);
// Make the API call from the action
return requests[method.toLowerCase()](action, {
body: formData,
}).then((res) => {
setModified(false);
if (typeof handleResponse === 'function') return handleResponse(res);
// Make the API call from the action
return requests[method.toLowerCase()](action, {
body: formData,
}).then((res) => {
setModified(false);
if (typeof handleResponse === 'function') return handleResponse(res);
return res.json().then((json) => {
setProcessing(false);
clearStatus();
return res.json().then((json) => {
setProcessing(false);
clearStatus();
if (res.status < 400) {
if (typeof onSuccess === 'function') onSuccess(json);
if (res.status < 400) {
if (typeof onSuccess === 'function') onSuccess(json);
if (redirect) {
const destination = {
pathname: redirect,
if (redirect) {
const destination = {
pathname: redirect,
};
if (json.message && !disableSuccessStatus) {
destination.state = {
status: [
{
message: json.message,
type: 'success',
},
],
};
if (json.message && !disableSuccessStatus) {
destination.state = {
status: [
{
message: json.message,
type: 'success',
},
],
};
}
history.push(destination);
} else if (!disableSuccessStatus) {
replaceStatus([{
message: json.message,
type: 'success',
disappear: 3000,
}]);
}
} else {
if (json.message) {
addStatus({
message: json.message,
type: 'error',
});
return json;
}
if (Array.isArray(json.errors)) {
const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]);
fieldErrors.forEach((err) => {
dispatchFields({
valid: false,
errorMessage: err.message,
path: err.field,
value: contextRef.current.fields?.[err.field]?.value,
});
});
nonFieldErrors.forEach((err) => {
addStatus({
message: err.message || 'An unknown error occurred.',
type: 'error',
});
});
if (fieldErrors.length > 0 && nonFieldErrors.length === 0) {
addStatus({
message: 'Please correct the fields below.',
type: 'error',
});
}
return json;
}
history.push(destination);
} else if (!disableSuccessStatus) {
replaceStatus([{
message: json.message,
type: 'success',
disappear: 3000,
}]);
}
} else {
if (json.message) {
addStatus({
message: 'An unknown error occurred.',
message: json.message,
type: 'error',
});
return json;
}
return json;
});
}).catch((err) => {
addStatus({
message: err,
type: 'error',
});
if (Array.isArray(json.errors)) {
const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]);
fieldErrors.forEach((err) => {
dispatchFields({
...(contextRef.current?.fields?.[err.field] || {}),
valid: false,
errorMessage: err.message,
path: err.field,
});
});
nonFieldErrors.forEach((err) => {
addStatus({
message: err.message || 'An unknown error occurred.',
type: 'error',
});
});
if (fieldErrors.length > 0 && nonFieldErrors.length === 0) {
addStatus({
message: 'Please correct the fields below.',
type: 'error',
});
}
return json;
}
addStatus({
message: 'An unknown error occurred.',
type: 'error',
});
}
return json;
});
}
}).catch((err) => {
addStatus({
message: err,
type: 'error',
});
});
}, [
action,
addStatus,
clearStatus,
disableSuccessStatus,
disabled,
fields,
handleResponse,
history,
method,
onSubmit,
onSuccess,
redirect,
replaceStatus,
]);
return true;
};
contextRef.current.getFields = () => contextRef.current.fields;
const getFields = useCallback(() => contextRef.current.fields, [contextRef]);
const getField = useCallback((path) => contextRef.current.fields[path], [contextRef]);
const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), [contextRef]);
contextRef.current.getField = (path) => contextRef.current.fields[path];
contextRef.current.getData = () => reduceFieldsToValues(contextRef.current.fields, true);
contextRef.current.getSiblingData = (path) => {
const getSiblingData = useCallback((path) => {
let siblingFields = contextRef.current.fields;
// If this field is nested
@@ -234,9 +242,9 @@ const Form = (props) => {
}
return reduceFieldsToValues(siblingFields, true);
};
}, [contextRef]);
contextRef.current.getDataByPath = (path) => {
const getDataByPath = useCallback((path) => {
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
const name = path.split('.').pop();
@@ -254,21 +262,35 @@ const Form = (props) => {
const values = reduceFieldsToValues(data, true);
const unflattenedData = unflatten(values);
return unflattenedData?.[name];
};
}, [contextRef]);
contextRef.current.getUnflattenedValues = () => reduceFieldsToValues(contextRef.current.fields);
const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]);
contextRef.current.validateForm = () => !Object.values(contextRef.current.fields).some((field) => field.valid === false);
const validateForm = useCallback(() => !Object.values(contextRef.current.fields).some((field) => field.valid === false), [contextRef]);
contextRef.current.createFormData = () => {
const createFormData = useCallback(() => {
const data = reduceFieldsToValues(contextRef.current.fields);
return objectToFormData(data, { indices: true });
};
}, [contextRef]);
contextRef.current.dispatchFields = dispatchFields;
contextRef.current.submit = submit;
contextRef.current.getFields = getFields;
contextRef.current.getField = getField;
contextRef.current.getData = getData;
contextRef.current.getSiblingData = getSiblingData;
contextRef.current.getDataByPath = getDataByPath;
contextRef.current.getUnflattenedValues = getUnflattenedValues;
contextRef.current.validateForm = validateForm;
contextRef.current.createFormData = createFormData;
contextRef.current.setModified = setModified;
contextRef.current.setProcessing = setProcessing;
contextRef.current.setSubmitted = setSubmitted;
useEffect(() => {
dispatchFields({ type: 'REPLACE_STATE', state: initialState });
}, [initialState]);
useThrottledEffect(() => {
refreshCookie();
}, 15000, [fields]);
@@ -299,10 +321,6 @@ const Form = (props) => {
<SubmittedContext.Provider value={submitted}>
<ProcessingContext.Provider value={processing}>
<ModifiedContext.Provider value={modified}>
<HiddenInput
path="locale"
defaultValue={locale}
/>
{children}
</ModifiedContext.Provider>
</ProcessingContext.Provider>
@@ -317,7 +335,6 @@ const Form = (props) => {
Form.defaultProps = {
redirect: '',
onSubmit: null,
ajax: true,
method: 'POST',
action: '',
handleResponse: null,
@@ -325,12 +342,12 @@ Form.defaultProps = {
className: '',
disableSuccessStatus: false,
disabled: false,
initialState: {},
};
Form.propTypes = {
disableSuccessStatus: PropTypes.bool,
onSubmit: PropTypes.func,
ajax: PropTypes.bool,
method: PropTypes.oneOf(['post', 'POST', 'get', 'GET', 'put', 'PUT', 'delete', 'DELETE']),
action: PropTypes.string,
handleResponse: PropTypes.func,
@@ -342,6 +359,7 @@ Form.propTypes = {
className: PropTypes.string,
redirect: PropTypes.string,
disabled: PropTypes.bool,
initialState: PropTypes.shape({}),
};
export default Form;

View File

@@ -10,4 +10,6 @@ export default {
submit: () => { },
dispatchFields: () => { },
setModified: () => { },
initialState: {},
reset: 0,
};

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import useIntersect from '../../../hooks/useIntersect';
import './index.scss';
const baseClass = 'render-fields';
const intersectionObserverOptions = {
rootMargin: '1000px',
@@ -16,13 +16,13 @@ export const useRenderedFields = () => useContext(RenderedFieldContext);
const RenderFields = (props) => {
const {
fieldSchema,
initialData,
customComponentsPath: customComponentsPathFromProps,
fieldTypes,
filter,
permissions,
readOnly: readOnlyOverride,
operation: operationFromProps,
className,
} = props;
const [hasIntersected, setHasIntersected] = useState(false);
@@ -52,9 +52,17 @@ const RenderFields = (props) => {
}
}, [isIntersecting, hasIntersected]);
const classes = [
baseClass,
className,
].filter(Boolean).join(' ');
if (fieldSchema) {
return (
<div ref={intersectionRef}>
<div
ref={intersectionRef}
className={classes}
>
{hasIntersected && (
<RenderedFieldContext.Provider value={contextValue}>
{fieldSchema.map((field, i) => {
@@ -62,14 +70,10 @@ const RenderFields = (props) => {
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
const FieldComponent = field?.admin?.hidden ? fieldTypes.hidden : fieldTypes[field.type];
let initialFieldData;
let fieldPermissions = permissions[field.name];
if (!field.name) {
initialFieldData = initialData;
fieldPermissions = permissions;
} else if (initialData?.[field.name] !== undefined) {
initialFieldData = initialData[field.name];
}
let { admin: { readOnly } = {} } = field;
@@ -84,14 +88,13 @@ const RenderFields = (props) => {
if (FieldComponent) {
return (
<RenderCustomComponent
key={field.name || `field-${i}`}
key={i}
path={`${customComponentsPath}${field.name ? `${field.name}.field` : ''}`}
DefaultComponent={FieldComponent}
componentProps={{
...field,
path: field.path || field.name,
fieldTypes,
initialData: initialFieldData,
admin: {
...(field.admin || {}),
readOnly,
@@ -132,19 +135,18 @@ const RenderFields = (props) => {
};
RenderFields.defaultProps = {
initialData: {},
customComponentsPath: '',
filter: null,
readOnly: false,
permissions: {},
operation: undefined,
className: undefined,
};
RenderFields.propTypes = {
fieldSchema: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,
initialData: PropTypes.shape({}),
customComponentsPath: PropTypes.string,
fieldTypes: PropTypes.shape({
hidden: PropTypes.function,
@@ -153,6 +155,7 @@ RenderFields.propTypes = {
permissions: PropTypes.shape({}),
readOnly: PropTypes.bool,
operation: PropTypes.string,
className: PropTypes.string,
};
export default RenderFields;

View File

@@ -1,3 +0,0 @@
.missing-field {
}

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useReducer, useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { v4 as uuidv4 } from 'uuid';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
@@ -23,8 +22,6 @@ const ArrayFieldType = (props) => {
name,
path: pathFromProps,
fields,
defaultValue,
initialData,
fieldTypes,
validate,
required,
@@ -34,10 +31,9 @@ const ArrayFieldType = (props) => {
permissions,
} = props;
const dataToInitialize = initialData || defaultValue;
const [rows, dispatchRows] = useReducer(reducer, []);
const { customComponentsPath } = useRenderedFields();
const { getDataByPath } = useForm();
const { getDataByPath, initialState, dispatchFields } = useForm();
const path = pathFromProps || name;
@@ -57,40 +53,25 @@ const ArrayFieldType = (props) => {
path,
validate: memoizedValidate,
disableFormData,
initialData: initialData?.length,
defaultValue: defaultValue?.length,
ignoreWhileFlattening: true,
required,
});
const addRow = useCallback((rowIndex) => {
const data = getDataByPath(path);
dispatchRows({
type: 'ADD', index: rowIndex, data,
});
dispatchRows({ type: 'ADD', rowIndex });
dispatchFields({ type: 'ADD_ROW', rowIndex, fieldSchema: fields, path });
setValue(value + 1);
}, [dispatchRows, getDataByPath, path, setValue, value]);
}, [dispatchRows, dispatchFields, fields, path, setValue, value]);
const removeRow = useCallback((rowIndex) => {
const data = getDataByPath(path);
dispatchRows({
type: 'REMOVE',
index: rowIndex,
data,
});
setValue(value - 1);
}, [dispatchRows, path, getDataByPath, setValue, value]);
dispatchRows({ type: 'REMOVE', rowIndex });
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
}, [dispatchRows, dispatchFields, path]);
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
const data = getDataByPath(path);
dispatchRows({
type: 'MOVE', index: moveFromIndex, moveToIndex, data,
});
}, [dispatchRows, getDataByPath, path]);
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
}, [dispatchRows, dispatchFields, path]);
const onDragEnd = useCallback((result) => {
if (!result.destination) return;
@@ -100,28 +81,19 @@ const ArrayFieldType = (props) => {
}, [moveRow]);
useEffect(() => {
dispatchRows({
type: 'SET_ALL',
rows: dataToInitialize.reduce((acc, data) => ([
...acc,
{
key: uuidv4(),
open: true,
data,
},
]), []),
});
}, [dataToInitialize]);
const data = getDataByPath(path);
dispatchRows({ type: 'SET_ALL', data });
}, [initialState, getDataByPath, path]);
useEffect(() => {
if (value === 0 && dataToInitialize.length > 0 && disableFormData) {
setValue(rows?.length || 0);
if (rows?.length === 0) {
setDisableFormData(false);
setValue(value);
} else if (value > 0 && !disableFormData) {
} else {
setDisableFormData(true);
setValue(value);
}
}, [value, setValue, disableFormData, dataToInitialize]);
}, [rows, setValue]);
return (
<RenderArray
@@ -147,8 +119,6 @@ const ArrayFieldType = (props) => {
ArrayFieldType.defaultProps = {
label: '',
defaultValue: [],
initialData: [],
validate: array,
required: false,
maxRows: undefined,
@@ -158,12 +128,6 @@ ArrayFieldType.defaultProps = {
};
ArrayFieldType.propTypes = {
defaultValue: PropTypes.arrayOf(
PropTypes.shape({}),
),
initialData: PropTypes.arrayOf(
PropTypes.shape({}),
),
fields: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,
@@ -230,7 +194,6 @@ const RenderArray = React.memo((props) => {
removeRow={() => removeRow(i)}
moveRow={moveRow}
parentPath={path}
initialData={row.data}
initNull={row.initNull}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
fieldTypes={fieldTypes}
@@ -259,4 +222,40 @@ const RenderArray = React.memo((props) => {
);
});
RenderArray.defaultProps = {
label: undefined,
showError: false,
errorMessage: undefined,
rows: [],
singularLabel: 'Row',
path: '',
customComponentsPath: undefined,
value: undefined,
};
RenderArray.propTypes = {
label: PropTypes.string,
showError: PropTypes.bool,
errorMessage: PropTypes.string,
rows: PropTypes.arrayOf(
PropTypes.shape({}),
),
singularLabel: PropTypes.string,
path: PropTypes.string,
customComponentsPath: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.number,
onDragEnd: PropTypes.func.isRequired,
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
moveRow: PropTypes.func.isRequired,
fieldTypes: PropTypes.shape({}).isRequired,
fields: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,
permissions: PropTypes.shape({
fields: PropTypes.shape({}),
}).isRequired,
};
export default withCondition(ArrayFieldType);

View File

@@ -22,10 +22,10 @@ const BlockSelection = (props) => {
};
return (
<div
<button
className={baseClass}
role="button"
tabIndex={0}
type="button"
onClick={handleBlockSelection}
>
<div className={`${baseClass}__image`}>
@@ -36,11 +36,10 @@ const BlockSelection = (props) => {
alt={blockImageAltText}
/>
)
: <DefaultBlockImage />
}
: <DefaultBlockImage />}
</div>
<div className={`${baseClass}__label`}>{labels.singular}</div>
</div>
</button>
);
};

View File

@@ -8,6 +8,10 @@
padding: base(.75) base(.5);
cursor: pointer;
align-items: center;
background: none;
border-radius: 0;
box-shadow: 0;
border: 0;
&:hover {
background-color: $color-background-gray;

View File

@@ -3,7 +3,6 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { v4 as uuidv4 } from 'uuid';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
@@ -27,8 +26,6 @@ const Blocks = (props) => {
name,
path: pathFromProps,
blocks,
defaultValue,
initialData,
singularLabel,
fieldTypes,
maxRows,
@@ -40,6 +37,10 @@ const Blocks = (props) => {
const path = pathFromProps || name;
const [rows, dispatchRows] = useReducer(reducer, []);
const { customComponentsPath } = useRenderedFields();
const { getDataByPath, initialState, dispatchFields } = useForm();
const memoizedValidate = useCallback((value) => {
const validationResult = validate(
value,
@@ -61,51 +62,32 @@ const Blocks = (props) => {
path,
validate: memoizedValidate,
disableFormData,
initialData: initialData?.length,
defaultValue: defaultValue?.length,
ignoreWhileFlattening: true,
required,
});
const dataToInitialize = initialData || defaultValue;
const [rows, dispatchRows] = useReducer(reducer, []);
const { customComponentsPath } = useRenderedFields();
const { getDataByPath } = useForm();
const addRow = useCallback((index, blockType) => {
const data = getDataByPath(path);
dispatchRows({
type: 'ADD', index, data, initialRowData: { blockType },
});
const addRow = useCallback((rowIndex, blockType) => {
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
dispatchRows({ type: 'ADD', rowIndex, blockType });
dispatchFields({ type: 'ADD_ROW', rowIndex, fieldSchema: block.fields, path, blockType });
setValue(value + 1);
}, [getDataByPath, path, setValue, value]);
const removeRow = useCallback((index) => {
const data = getDataByPath(path);
dispatchRows({
type: 'REMOVE',
index,
data,
});
}, [path, setValue, value, blocks, dispatchFields]);
const removeRow = useCallback((rowIndex) => {
dispatchRows({ type: 'REMOVE', rowIndex });
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
setValue(value - 1);
}, [getDataByPath, path, setValue, value]);
}, [path, setValue, value, dispatchFields]);
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
const data = getDataByPath(path);
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
}, [dispatchRows, dispatchFields, path]);
dispatchRows({
type: 'MOVE', index: moveFromIndex, moveToIndex, data,
});
}, [getDataByPath, path]);
const toggleCollapse = useCallback((index) => {
dispatchRows({
type: 'TOGGLE_COLLAPSE', index, rows,
});
}, [rows]);
const toggleCollapse = useCallback((rowIndex) => {
dispatchRows({ type: 'TOGGLE_COLLAPSE', rowIndex });
}, []);
const onDragEnd = useCallback((result) => {
if (!result.destination) return;
@@ -115,31 +97,22 @@ const Blocks = (props) => {
}, [moveRow]);
useEffect(() => {
dispatchRows({
type: 'SET_ALL',
rows: dataToInitialize.reduce((acc, data) => ([
...acc,
{
key: uuidv4(),
open: true,
data,
},
]), []),
});
}, [dataToInitialize]);
const data = getDataByPath(path);
dispatchRows({ type: 'SET_ALL', data });
}, [initialState, getDataByPath, path]);
useEffect(() => {
if (value === 0 && dataToInitialize.length > 0 && disableFormData) {
setValue(rows?.length || 0);
if (rows?.length === 0) {
setDisableFormData(false);
setValue(value);
} else if (value > 0 && !disableFormData) {
} else {
setDisableFormData(true);
setValue(value);
}
}, [value, setValue, disableFormData, dataToInitialize]);
}, [rows, setValue]);
return (
<RenderBlock
<RenderBlocks
onDragEnd={onDragEnd}
label={label}
showError={showError}
@@ -156,7 +129,6 @@ const Blocks = (props) => {
toggleCollapse={toggleCollapse}
permissions={permissions}
value={value}
dataToInitialize={dataToInitialize}
blocks={blocks}
/>
);
@@ -164,8 +136,6 @@ const Blocks = (props) => {
Blocks.defaultProps = {
label: '',
defaultValue: [],
initialData: [],
singularLabel: 'Block',
validate: blocksValidator,
required: false,
@@ -178,12 +148,6 @@ Blocks.propTypes = {
blocks: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,
defaultValue: PropTypes.arrayOf(
PropTypes.shape({}),
),
initialData: PropTypes.arrayOf(
PropTypes.shape({}),
),
label: PropTypes.string,
singularLabel: PropTypes.string,
name: PropTypes.string.isRequired,
@@ -198,7 +162,7 @@ Blocks.propTypes = {
}),
};
const RenderBlock = React.memo((props) => {
const RenderBlocks = React.memo((props) => {
const {
onDragEnd,
label,
@@ -216,7 +180,6 @@ const RenderBlock = React.memo((props) => {
permissions,
value,
toggleCollapse,
dataToInitialize,
blocks,
} = props;
@@ -239,12 +202,7 @@ const RenderBlock = React.memo((props) => {
{...provided.droppableProps}
>
{rows.length > 0 && rows.map((row, i) => {
let { blockType } = row.data;
if (!blockType) {
blockType = dataToInitialize?.[i]?.blockType;
}
const { blockType } = row;
const blockToRender = blocks.find((block) => block.slug === blockType);
if (blockToRender) {
@@ -263,7 +221,6 @@ const RenderBlock = React.memo((props) => {
moveRow={moveRow}
toggleRowCollapse={() => toggleCollapse(i)}
parentPath={path}
initialData={row.data}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
fieldTypes={fieldTypes}
permissions={permissions.fields}
@@ -318,4 +275,41 @@ const RenderBlock = React.memo((props) => {
);
});
RenderBlocks.defaultProps = {
label: undefined,
showError: false,
errorMessage: undefined,
rows: [],
singularLabel: 'Row',
path: '',
customComponentsPath: undefined,
value: undefined,
};
RenderBlocks.propTypes = {
label: PropTypes.string,
showError: PropTypes.bool,
errorMessage: PropTypes.string,
rows: PropTypes.arrayOf(
PropTypes.shape({}),
),
singularLabel: PropTypes.string,
path: PropTypes.string,
customComponentsPath: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.number,
onDragEnd: PropTypes.func.isRequired,
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
moveRow: PropTypes.func.isRequired,
fieldTypes: PropTypes.shape({}).isRequired,
permissions: PropTypes.shape({
fields: PropTypes.shape({}),
}).isRequired,
blocks: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,
toggleCollapse: PropTypes.func.isRequired,
};
export default withCondition(Blocks);

View File

@@ -15,8 +15,6 @@ const Checkbox = (props) => {
name,
path: pathFromProps,
required,
defaultValue,
initialData,
validate,
label,
onChange,
@@ -43,8 +41,6 @@ const Checkbox = (props) => {
} = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
disableFormData,
});
@@ -98,8 +94,6 @@ Checkbox.defaultProps = {
label: null,
required: false,
admin: {},
defaultValue: false,
initialData: false,
validate: checkbox,
path: '',
onChange: undefined,
@@ -115,8 +109,6 @@ Checkbox.propTypes = {
width: PropTypes.string,
}),
required: PropTypes.bool,
defaultValue: PropTypes.bool,
initialData: PropTypes.bool,
validate: PropTypes.func,
label: PropTypes.string,
onChange: PropTypes.func,

View File

@@ -19,8 +19,6 @@ const DateTime = (props) => {
path: pathFromProps,
name,
required,
defaultValue,
initialData,
validate,
errorMessage,
label,
@@ -45,8 +43,6 @@ const DateTime = (props) => {
} = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
});
@@ -88,8 +84,6 @@ const DateTime = (props) => {
DateTime.defaultProps = {
label: null,
required: false,
defaultValue: undefined,
initialData: undefined,
validate: date,
errorMessage: defaultError,
admin: {},
@@ -101,8 +95,6 @@ DateTime.propTypes = {
path: PropTypes.string,
label: PropTypes.string,
required: PropTypes.bool,
defaultValue: PropTypes.string,
initialData: PropTypes.string,
validate: PropTypes.func,
errorMessage: PropTypes.string,
admin: PropTypes.shape({

View File

@@ -13,8 +13,6 @@ const Email = (props) => {
name,
path: pathFromProps,
required,
defaultValue,
initialData,
validate,
admin: {
readOnly,
@@ -35,9 +33,6 @@ const Email = (props) => {
const fieldType = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
enableDebouncedValue: true,
});

View File

@@ -7,7 +7,7 @@ import './index.scss';
const Group = (props) => {
const {
label, fields, name, path: pathFromProps, fieldTypes, initialData,
label, fields, name, path: pathFromProps, fieldTypes,
} = props;
const path = pathFromProps || name;
@@ -18,7 +18,6 @@ const Group = (props) => {
<div className="field-type group">
<h3>{label}</h3>
<RenderFields
initialData={initialData}
fieldTypes={fieldTypes}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
fieldSchema={fields.map((subField) => ({
@@ -32,12 +31,10 @@ const Group = (props) => {
Group.defaultProps = {
label: '',
initialData: {},
path: '',
};
Group.propTypes = {
initialData: PropTypes.shape({}),
fields: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,

View File

@@ -8,8 +8,6 @@ const HiddenInput = (props) => {
name,
path: pathFromProps,
required,
defaultValue,
initialData,
} = props;
const path = pathFromProps || name;
@@ -17,8 +15,6 @@ const HiddenInput = (props) => {
const { value, setValue } = useFieldType({
path,
required,
initialData,
defaultValue,
});
return (
@@ -33,8 +29,6 @@ const HiddenInput = (props) => {
HiddenInput.defaultProps = {
required: false,
defaultValue: undefined,
initialData: undefined,
path: '',
};
@@ -42,8 +36,6 @@ HiddenInput.propTypes = {
name: PropTypes.string.isRequired,
path: PropTypes.string,
required: PropTypes.bool,
defaultValue: PropTypes.string,
initialData: PropTypes.string,
};
export default withCondition(HiddenInput);

View File

@@ -13,8 +13,6 @@ const NumberField = (props) => {
name,
path: pathFromProps,
required,
defaultValue,
initialData,
validate,
label,
placeholder,
@@ -41,9 +39,6 @@ const NumberField = (props) => {
errorMessage,
} = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
enableDebouncedValue: true,
});
@@ -95,8 +90,6 @@ NumberField.defaultProps = {
label: null,
path: undefined,
required: false,
defaultValue: undefined,
initialData: undefined,
placeholder: undefined,
max: undefined,
min: undefined,
@@ -109,8 +102,6 @@ NumberField.propTypes = {
path: PropTypes.string,
required: PropTypes.bool,
placeholder: PropTypes.string,
defaultValue: PropTypes.number,
initialData: PropTypes.number,
validate: PropTypes.func,
admin: PropTypes.shape({
readOnly: PropTypes.bool,

View File

@@ -13,8 +13,6 @@ const Password = (props) => {
path: pathFromProps,
name,
required,
defaultValue,
initialData,
validate,
style,
width,
@@ -38,8 +36,6 @@ const Password = (props) => {
} = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
enableDebouncedValue: true,
});
@@ -82,8 +78,6 @@ const Password = (props) => {
Password.defaultProps = {
required: false,
initialData: undefined,
defaultValue: undefined,
validate: password,
width: undefined,
style: {},
@@ -95,8 +89,6 @@ Password.propTypes = {
name: PropTypes.string.isRequired,
path: PropTypes.string,
required: PropTypes.bool,
initialData: PropTypes.string,
defaultValue: PropTypes.string,
width: PropTypes.string,
style: PropTypes.shape({}),
label: PropTypes.string.isRequired,

View File

@@ -15,8 +15,6 @@ const RadioGroup = (props) => {
name,
path: pathFromProps,
required,
defaultValue,
initialData,
validate,
label,
admin: {
@@ -42,8 +40,6 @@ const RadioGroup = (props) => {
} = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
});
@@ -72,7 +68,7 @@ const RadioGroup = (props) => {
required={required}
/>
{options?.map((option) => {
const isSelected = !value ? (option.value === defaultValue) : (option.value === value);
const isSelected = option.value === value;
return (
<RadioInput
@@ -90,8 +86,6 @@ const RadioGroup = (props) => {
RadioGroup.defaultProps = {
label: null,
required: false,
defaultValue: null,
initialData: undefined,
validate: radio,
admin: {},
path: '',
@@ -101,8 +95,6 @@ RadioGroup.propTypes = {
path: PropTypes.string,
name: PropTypes.string.isRequired,
required: PropTypes.bool,
defaultValue: PropTypes.string,
initialData: PropTypes.string,
validate: PropTypes.func,
admin: PropTypes.shape({
readOnly: PropTypes.bool,

View File

@@ -1,5 +1,5 @@
import React, {
Component, useState, useEffect, useCallback,
Component, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import some from 'async-some';
@@ -90,7 +90,7 @@ class Relationship extends Component {
error = 'You do not have permission to load options for this field.';
}
this.setState({
return this.setState({
errorLoading: error,
});
}, (lastPage, nextPage) => {
@@ -245,10 +245,6 @@ class Relationship extends Component {
const valueToRender = this.findValueInOptions(options, value);
// ///////////////////////////////////////////
// TODO: simplify formatValue pattern seen below with react select
// ///////////////////////////////////////////
return (
<div
className={classes}
@@ -330,14 +326,11 @@ Relationship.propTypes = {
};
const RelationshipFieldType = (props) => {
const [formattedInitialData, setFormattedInitialData] = useState(undefined);
const {
defaultValue, relationTo, hasMany, validate, path, name, initialData, required,
relationTo, validate, path, name, required,
} = props;
const hasMultipleRelations = Array.isArray(relationTo);
const dataToInitialize = initialData || defaultValue;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
@@ -348,41 +341,10 @@ const RelationshipFieldType = (props) => {
const fieldType = useFieldType({
...props,
path: path || name,
initialData: formattedInitialData,
defaultValue,
validate: memoizedValidate,
required,
});
useEffect(() => {
const formatInitialData = (valueToFormat) => {
if (hasMultipleRelations) {
const id = valueToFormat?.value?.id || valueToFormat?.value;
return {
...valueToFormat,
value: id,
};
}
return valueToFormat?.id || valueToFormat;
};
if (dataToInitialize) {
if (hasMany && Array.isArray(dataToInitialize)) {
const newFormattedInitialData = [];
dataToInitialize.forEach((individualValue) => {
newFormattedInitialData.push(formatInitialData(individualValue));
});
setFormattedInitialData(newFormattedInitialData);
} else {
setFormattedInitialData(formatInitialData(dataToInitialize));
}
}
}, [dataToInitialize, hasMany, hasMultipleRelations]);
return (
<Relationship
{...props}

View File

@@ -74,8 +74,6 @@ const RichText = (props) => {
path: pathFromProps,
name,
required,
defaultValue,
initialData,
validate,
label,
placeholder,
@@ -93,8 +91,6 @@ const RichText = (props) => {
const fieldType = useFieldType({
path,
required,
initialData,
defaultValue,
validate,
});
@@ -164,8 +160,6 @@ const RichText = (props) => {
RichText.defaultProps = {
label: null,
required: false,
defaultValue: undefined,
initialData: undefined,
placeholder: undefined,
admin: {},
validate: richText,
@@ -177,8 +171,6 @@ RichText.propTypes = {
path: PropTypes.string,
required: PropTypes.bool,
placeholder: PropTypes.string,
defaultValue: PropTypes.string,
initialData: PropTypes.arrayOf(PropTypes.shape({})),
validate: PropTypes.func,
admin: PropTypes.shape({
readOnly: PropTypes.bool,

View File

@@ -7,29 +7,24 @@ import './index.scss';
const Row = (props) => {
const {
fields, fieldTypes, initialData, path, permissions,
fields, fieldTypes, path, permissions,
} = props;
return (
<div className="field-type row">
<RenderFields
permissions={permissions}
initialData={initialData}
fieldTypes={fieldTypes}
fieldSchema={fields.map((field) => {
return {
...field,
path: `${path ? `${path}.` : ''}${field.name}`,
};
})}
/>
</div>
<RenderFields
className="field-type row"
permissions={permissions}
fieldTypes={fieldTypes}
fieldSchema={fields.map((field) => ({
...field,
path: `${path ? `${path}.` : ''}${field.name}`,
}))}
/>
);
};
Row.defaultProps = {
path: '',
initialData: undefined,
permissions: {},
};
@@ -39,7 +34,6 @@ Row.propTypes = {
).isRequired,
fieldTypes: PropTypes.shape({}).isRequired,
path: PropTypes.string,
initialData: PropTypes.shape({}),
permissions: PropTypes.shape({}),
};

View File

@@ -65,8 +65,6 @@ const Select = (props) => {
path: pathFromProps,
name,
required,
defaultValue,
initialData,
validate,
label,
options,
@@ -94,8 +92,6 @@ const Select = (props) => {
path,
label,
required,
initialData,
defaultValue,
validate: memoizedValidate,
});
@@ -142,8 +138,6 @@ Select.defaultProps = {
admin: {},
required: false,
validate: select,
defaultValue: undefined,
initialData: undefined,
hasMany: false,
path: '',
};
@@ -156,14 +150,6 @@ Select.propTypes = {
width: PropTypes.string,
}),
label: PropTypes.string.isRequired,
defaultValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array,
]),
initialData: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array,
]),
validate: PropTypes.func,
name: PropTypes.string.isRequired,
path: PropTypes.string,

View File

@@ -13,8 +13,6 @@ const Text = (props) => {
path: pathFromProps,
name,
required,
defaultValue,
initialData,
validate,
label,
placeholder,
@@ -36,9 +34,6 @@ const Text = (props) => {
const fieldType = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
enableDebouncedValue: true,
});
@@ -91,8 +86,6 @@ Text.defaultProps = {
label: null,
required: false,
admin: {},
defaultValue: undefined,
initialData: undefined,
placeholder: undefined,
validate: text,
path: '',
@@ -105,8 +98,6 @@ Text.propTypes = {
path: PropTypes.string,
required: PropTypes.bool,
placeholder: PropTypes.string,
defaultValue: PropTypes.string,
initialData: PropTypes.string,
validate: PropTypes.func,
admin: PropTypes.shape({
readOnly: PropTypes.bool,

View File

@@ -13,14 +13,14 @@ const Textarea = (props) => {
path: pathFromProps,
name,
required,
defaultValue,
initialData,
validate,
style,
width,
admin: {
readOnly,
style,
width,
} = {},
label,
placeholder,
readOnly,
minLength,
maxLength,
rows,
@@ -40,9 +40,6 @@ const Textarea = (props) => {
errorMessage,
} = useFieldType({
path,
required,
initialData,
defaultValue,
validate: memoizedValidate,
enableDebouncedValue: true,
});
@@ -87,8 +84,6 @@ const Textarea = (props) => {
Textarea.defaultProps = {
required: false,
label: null,
defaultValue: undefined,
initialData: undefined,
validate: textarea,
placeholder: null,
path: '',
@@ -102,8 +97,6 @@ Textarea.propTypes = {
name: PropTypes.string.isRequired,
path: PropTypes.string,
required: PropTypes.bool,
defaultValue: PropTypes.string,
initialData: PropTypes.string,
validate: PropTypes.func,
admin: PropTypes.shape({
readOnly: PropTypes.bool,

View File

@@ -26,8 +26,6 @@ const Upload = (props) => {
path: pathFromProps,
name,
required,
defaultValue,
initialData,
admin: {
readOnly,
style,
@@ -45,8 +43,6 @@ const Upload = (props) => {
const addModalSlug = `${path}-add`;
const selectExistingModalSlug = `${path}-select-existing`;
const dataToInitialize = (typeof initialData === 'object' && initialData.id) ? initialData.id : initialData;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
return validationResult;
@@ -55,8 +51,6 @@ const Upload = (props) => {
const fieldType = useFieldType({
path,
required,
initialData: dataToInitialize,
defaultValue,
validate: memoizedValidate,
});
@@ -75,13 +69,9 @@ const Upload = (props) => {
].filter(Boolean).join(' ');
useEffect(() => {
if (typeof initialData === 'object' && initialData?.id) {
setInternalValue(initialData);
}
if (typeof initialData === 'string') {
if (typeof value === 'string') {
const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}/${initialData}`);
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`);
if (response.ok) {
const json = await response.json();
@@ -91,7 +81,7 @@ const Upload = (props) => {
fetchFile();
}
}, [initialData, setInternalValue, relationTo]);
}, [value, setInternalValue, relationTo]);
return (
<div
@@ -173,8 +163,6 @@ const Upload = (props) => {
Upload.defaultProps = {
label: null,
required: false,
defaultValue: undefined,
initialData: undefined,
admin: {},
validate: upload,
path: '',
@@ -184,13 +172,6 @@ Upload.propTypes = {
name: PropTypes.string.isRequired,
path: PropTypes.string,
required: PropTypes.bool,
defaultValue: PropTypes.string,
initialData: PropTypes.oneOfType([
PropTypes.shape({
id: PropTypes.string,
}),
PropTypes.string,
]),
validate: PropTypes.func,
admin: PropTypes.shape({
readOnly: PropTypes.bool,

View File

@@ -1,6 +1,3 @@
export { default as auth } from './Auth';
export { default as file } from './File';
export { default as email } from './Email';
export { default as hidden } from './HiddenInput';
export { default as text } from './Text';

View File

@@ -2,59 +2,57 @@ import { v4 as uuidv4 } from 'uuid';
const reducer = (currentState, action) => {
const {
type, index, moveToIndex, rows, data = [], initialRowData = {},
type, rowIndex, moveFromIndex, moveToIndex, data, blockType,
} = action;
const stateCopy = [...currentState];
switch (type) {
case 'SET_ALL':
return rows;
case 'SET_ALL': {
if (Array.isArray(data)) {
return data.map((dataRow) => {
const row = {
key: uuidv4(),
open: true,
};
if (dataRow.blockType) {
row.blockType = dataRow.blockType;
}
return row;
});
}
return [];
}
case 'TOGGLE_COLLAPSE':
stateCopy[index].open = !stateCopy[index].open;
stateCopy[rowIndex].open = !stateCopy[rowIndex].open;
return stateCopy;
case 'ADD': {
stateCopy.splice(index + 1, 0, {
const newRow = {
open: true,
key: uuidv4(),
data,
});
};
data.splice(index + 1, 0, initialRowData);
if (blockType) newRow.blockType = blockType;
const result = stateCopy.map((row, i) => {
return {
...row,
data: {
...(data[i] || {}),
},
};
});
stateCopy.splice(rowIndex + 1, 0, newRow);
return result;
return stateCopy;
}
case 'REMOVE':
stateCopy.splice(index, 1);
stateCopy.splice(rowIndex, 1);
return stateCopy;
case 'MOVE': {
const stateCopyWithNewData = stateCopy.map((row, i) => {
return {
...row,
data: {
...(data[i] || {}),
},
};
});
const movingRowState = { ...stateCopyWithNewData[index] };
stateCopyWithNewData.splice(index, 1);
stateCopyWithNewData.splice(moveToIndex, 0, movingRowState);
return stateCopyWithNewData;
const movingRowState = { ...stateCopy[moveFromIndex] };
stateCopy.splice(moveFromIndex, 1);
stateCopy.splice(moveToIndex, 0, movingRowState);
return stateCopy;
}
default:

View File

@@ -3,43 +3,36 @@ import {
} from 'react';
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context';
import useDebounce from '../../../hooks/useDebounce';
import useUnmountEffect from '../../../hooks/useUnmountEffect';
import './index.scss';
const useFieldType = (options) => {
const {
path,
initialData: data,
defaultValue,
validate,
disableFormData,
enableDebouncedValue,
disableFormData,
ignoreWhileFlattening,
} = options;
// Determine what the initial data is to be used
// If initialData is defined, that means that data has been provided
// via the API and should override any default values present.
// If no initialData, use default value
const initialData = data !== undefined ? data : defaultValue;
const formContext = useForm();
const submitted = useFormSubmitted();
const processing = useFormProcessing();
const modified = useFormModified();
const {
dispatchFields, getField, setModified,
dispatchFields, getField, setModified, reset,
} = formContext;
const [internalValue, setInternalValue] = useState(initialData);
const [internalValue, setInternalValue] = useState(undefined);
// Debounce internal values to update form state only every 60ms
const debouncedValue = useDebounce(internalValue, 120);
// Get field by path
const field = getField(path);
const fieldExists = Boolean(field);
const initialValue = field?.initialValue;
// Valid could be a string equal to an error message
const valid = (field && typeof field.valid === 'boolean') ? field.valid : true;
@@ -57,48 +50,39 @@ const useFieldType = (options) => {
fieldToDispatch.valid = false;
}
if (disableFormData) {
fieldToDispatch.disableFormData = true;
}
fieldToDispatch.disableFormData = disableFormData;
fieldToDispatch.ignoreWhileFlattening = ignoreWhileFlattening;
fieldToDispatch.initialValue = initialValue;
dispatchFields(fieldToDispatch);
}, [path, dispatchFields, validate, disableFormData]);
}, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue]);
// Method to return from `useFieldType`, used to
// update internal field values from field component(s)
// as fast as they arrive. NOTE - this method is NOT debounced
const setValue = useCallback((e) => {
const value = (e && e.target) ? e.target.value : e;
const val = (e && e.target) ? e.target.value : e;
if (!modified) setModified(true);
setInternalValue(value);
setInternalValue(val);
}, [setModified, modified]);
// Remove field from state on "unmount"
// This is mostly used for array / flex content row modifications
useUnmountEffect(() => {
formContext.dispatchFields({ path, type: 'REMOVE' });
});
useEffect(() => {
setInternalValue(initialValue);
}, [initialValue]);
// The only time that the FORM value should be updated
// is when the debounced value updates. So, when the debounced value updates,
// send it up to the form
const formValue = enableDebouncedValue ? debouncedValue : internalValue;
const valueToSend = enableDebouncedValue ? debouncedValue : internalValue;
useEffect(() => {
if (!fieldExists || formValue !== undefined) {
sendField(formValue);
if (valueToSend !== undefined) {
sendField(valueToSend);
}
}, [formValue, sendField, fieldExists]);
useEffect(() => {
if (initialData !== undefined) {
setInternalValue(initialData);
}
}, [initialData]);
}, [valueToSend, sendField]);
return {
...options,

View File

@@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import format from 'date-fns/format';
import config from 'payload/config';
import Eyebrow from '../../elements/Eyebrow';
import Form from '../../forms/Form';
import PreviewButton from '../../elements/PreviewButton';
@@ -13,13 +12,11 @@ import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving';
import './index.scss';
const { serverURL, routes: { api } } = config;
const baseClass = 'global-edit';
const DefaultGlobalView = (props) => {
const {
global, data, onSave, permissions,
global, data, onSave, permissions, action, apiURL,
} = props;
const {
@@ -29,8 +26,6 @@ const DefaultGlobalView = (props) => {
label,
} = global;
const apiURL = `${serverURL}${api}/globals/${slug}`;
const action = `${serverURL}${api}/globals/${slug}`;
const hasSavePermission = permissions?.update?.permission;
return (
@@ -57,7 +52,7 @@ const DefaultGlobalView = (props) => {
operation="update"
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={field => (!field.position || (field.position && field.position !== 'sidebar'))}
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
@@ -93,7 +88,7 @@ const DefaultGlobalView = (props) => {
operation="update"
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={field => field.position === 'sidebar'}
filter={(field) => field.position === 'sidebar'}
position="sidebar"
fieldTypes={fieldTypes}
fieldSchema={fields}
@@ -138,6 +133,8 @@ DefaultGlobalView.propTypes = {
}),
fields: PropTypes.shape({}),
}).isRequired,
action: PropTypes.string.isRequired,
apiURL: PropTypes.string.isRequired,
};
export default DefaultGlobalView;

View File

@@ -5,6 +5,7 @@ import config from 'payload/config';
import { useStepNav } from '../../elements/StepNav';
import usePayloadAPI from '../../../hooks/usePayloadAPI';
import { useUser } from '../../data/User';
import { useLocale } from '../../utilities/Locale';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import DefaultGlobal from './Default';
@@ -14,6 +15,7 @@ const { serverURL, routes: { admin, api } } = config;
const GlobalView = (props) => {
const { state: locationState } = useLocation();
const history = useHistory();
const locale = useLocale();
const { setStepNav } = useStepNav();
const { permissions } = useUser();
@@ -60,6 +62,8 @@ const GlobalView = (props) => {
permissions: globalPermissions,
global,
onSave,
apiURL: `${serverURL}${api}/globals/${slug}?depth=0`,
action: `${serverURL}${api}/globals/${slug}?locale=${locale}`,
}}
/>
);

View File

@@ -1,12 +1,11 @@
import React, { useMemo, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Button from '../../../elements/Button';
import CopyToClipboard from '../../../elements/CopyToClipboard';
import { text } from '../../../../../fields/validations';
import { useFormFields } from '../../Form/context';
import useFieldType from '../../../../forms/useFieldType';
import Label from '../../../../forms/Label';
import Button from '../../../../elements/Button';
import CopyToClipboard from '../../../../elements/CopyToClipboard';
import { text } from '../../../../../../fields/validations';
import { useFormFields } from '../../../../forms/Form/context';
import './index.scss';
@@ -14,11 +13,7 @@ const path = 'apiKey';
const baseClass = 'api-key';
const validate = (val) => text(val, { minLength: 24, maxLength: 48 });
const APIKey = (props) => {
const {
initialData,
} = props;
const APIKey = () => {
const [initialAPIKey, setInitialAPIKey] = useState(null);
const { getField } = useFormFields();
@@ -38,7 +33,6 @@ const APIKey = (props) => {
const fieldType = useFieldType({
path: 'apiKey',
initialData: initialData || initialAPIKey,
validate,
});
@@ -51,6 +45,12 @@ const APIKey = (props) => {
setInitialAPIKey(uuidv4());
}, []);
useEffect(() => {
if (!apiKeyValue) {
setValue(initialAPIKey);
}
}, [apiKeyValue, setValue, initialAPIKey]);
const classes = [
'field-type',
'api-key',
@@ -83,12 +83,4 @@ const APIKey = (props) => {
);
};
APIKey.defaultProps = {
initialData: undefined,
};
APIKey.propTypes = {
initialData: PropTypes.string,
};
export default APIKey;

View File

@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Email from '../Email';
import Password from '../Password';
import Checkbox from '../Checkbox';
import Button from '../../../elements/Button';
import ConfirmPassword from '../ConfirmPassword';
import { useFormFields } from '../../Form/context';
import Email from '../../../../forms/field-types/Email';
import Password from '../../../../forms/field-types/Password';
import Checkbox from '../../../../forms/field-types/Checkbox';
import Button from '../../../../elements/Button';
import ConfirmPassword from '../../../../forms/field-types/ConfirmPassword';
import { useFormFields, useFormModified } from '../../../../forms/Form/context';
import APIKey from './APIKey';
import './index.scss';
@@ -13,19 +13,25 @@ import './index.scss';
const baseClass = 'auth-fields';
const Auth = (props) => {
const { initialData, useAPIKey, requirePassword } = props;
const { useAPIKey, requirePassword } = props;
const [changingPassword, setChangingPassword] = useState(requirePassword);
const { getField } = useFormFields();
const modified = useFormModified();
const enableAPIKey = getField('enableAPIKey');
useEffect(() => {
if (!modified) {
setChangingPassword(false);
}
}, [modified]);
return (
<div className={baseClass}>
<Email
required
name="email"
label="Email"
initialData={initialData?.email}
autoComplete="email"
/>
{changingPassword && (
@@ -60,12 +66,11 @@ const Auth = (props) => {
{useAPIKey && (
<div className={`${baseClass}__api-key`}>
<Checkbox
initialData={initialData?.enableAPIKey}
label="Enable API Key"
name="enableAPIKey"
/>
{enableAPIKey?.value && (
<APIKey initialData={initialData?.apiKey} />
<APIKey />
)}
</div>
)}
@@ -74,18 +79,11 @@ const Auth = (props) => {
};
Auth.defaultProps = {
initialData: undefined,
useAPIKey: false,
requirePassword: false,
};
Auth.propTypes = {
fieldTypes: PropTypes.shape({}).isRequired,
initialData: PropTypes.shape({
enableAPIKey: PropTypes.bool,
apiKey: PropTypes.string,
email: PropTypes.string,
}),
useAPIKey: PropTypes.bool,
requirePassword: PropTypes.bool,
};

View File

@@ -1,5 +1,5 @@
@import '../../../../scss/styles.scss';
@import '../shared.scss';
@import '../../../../../scss/styles.scss';
@import '../../../../forms/field-types/shared.scss';
.auth-fields {
margin: base(1.5) 0 base(2);

View File

@@ -15,10 +15,12 @@ import DeleteDocument from '../../../elements/DeleteDocument';
import * as fieldTypes from '../../../forms/field-types';
import RenderTitle from '../../../elements/RenderTitle';
import LeaveWithoutSaving from '../../../modals/LeaveWithoutSaving';
import Auth from './Auth';
import Upload from './Upload';
import './index.scss';
const { serverURL, routes: { api, admin } } = config;
const { routes: { admin } } = config;
const baseClass = 'collection-edit';
@@ -26,7 +28,16 @@ const DefaultEditView = (props) => {
const { params: { id } = {} } = useRouteMatch();
const {
collection, isEditing, data, onSave, permissions, isLoading,
collection,
isEditing,
data,
onSave,
permissions,
isLoading,
initialState,
apiURL,
action,
hasSavePermission,
} = props;
const {
@@ -38,16 +49,9 @@ const DefaultEditView = (props) => {
timestamps,
preview,
auth,
upload,
} = collection;
const apiURL = `${serverURL}${api}/${slug}/${id}`;
let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}`;
const hasSavePermission = (isEditing && permissions?.update?.permission) || (!isEditing && permissions?.create?.permission);
if (auth && !isEditing) {
action = `${action}/register`;
}
const classes = [
baseClass,
isEditing && `${baseClass}--is-editing`,
@@ -61,6 +65,7 @@ const DefaultEditView = (props) => {
action={action}
onSuccess={onSave}
disabled={!hasSavePermission}
initialState={initialState}
>
<div className={`${baseClass}__main`}>
<Eyebrow />
@@ -76,6 +81,19 @@ const DefaultEditView = (props) => {
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
</h1>
</header>
{auth && (
<Auth
useAPIKey={auth.useAPIKey}
requirePassword={!isEditing}
/>
)}
{upload && (
<Upload
data={data}
{...upload}
fieldTypes={fieldTypes}
/>
)}
<RenderFields
operation={isEditing ? 'update' : 'create'}
readOnly={!hasSavePermission}
@@ -83,7 +101,6 @@ const DefaultEditView = (props) => {
filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</React.Fragment>
@@ -145,7 +162,6 @@ const DefaultEditView = (props) => {
position="sidebar"
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</div>
@@ -187,9 +203,14 @@ DefaultEditView.defaultProps = {
isEditing: false,
isLoading: true,
data: undefined,
initialState: undefined,
apiURL: undefined,
};
DefaultEditView.propTypes = {
hasSavePermission: PropTypes.bool.isRequired,
action: PropTypes.string.isRequired,
apiURL: PropTypes.string,
isLoading: PropTypes.bool,
collection: PropTypes.shape({
labels: PropTypes.shape({
@@ -203,7 +224,10 @@ DefaultEditView.propTypes = {
fields: PropTypes.arrayOf(PropTypes.shape({})),
preview: PropTypes.func,
timestamps: PropTypes.bool,
auth: PropTypes.shape({}),
auth: PropTypes.shape({
useAPIKey: PropTypes.bool,
}),
upload: PropTypes.shape({}),
}).isRequired,
isEditing: PropTypes.bool,
data: PropTypes.shape({
@@ -223,6 +247,7 @@ DefaultEditView.propTypes = {
}),
fields: PropTypes.shape({}),
}).isRequired,
initialState: PropTypes.shape({}),
};
export default DefaultEditView;

View File

@@ -2,10 +2,10 @@ import React, {
useState, useRef, useEffect, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import useFieldType from '../../useFieldType';
import Button from '../../../elements/Button';
import FileDetails from '../../../elements/FileDetails';
import Error from '../../Error';
import useFieldType from '../../../../forms/useFieldType';
import Button from '../../../../elements/Button';
import FileDetails from '../../../../elements/FileDetails';
import Error from '../../../../forms/Error';
import './index.scss';
@@ -34,10 +34,10 @@ const File = (props) => {
const [replacingFile, setReplacingFile] = useState(false);
const {
initialData = {}, adminThumbnail, staticURL,
data = {}, adminThumbnail, staticURL,
} = props;
const { filename } = initialData;
const { filename } = data;
const {
value,
@@ -122,7 +122,7 @@ const File = (props) => {
useEffect(() => {
setReplacingFile(false);
}, [initialData]);
}, [data]);
const classes = [
baseClass,
@@ -138,7 +138,7 @@ const File = (props) => {
/>
{(filename && !replacingFile) && (
<FileDetails
{...initialData}
{...data}
staticURL={staticURL}
adminThumbnail={adminThumbnail}
handleRemove={() => {
@@ -167,7 +167,7 @@ const File = (props) => {
</div>
)}
{!value && (
<>
<React.Fragment>
<div
className={`${baseClass}__drop-zone`}
ref={dropRef}
@@ -181,7 +181,7 @@ const File = (props) => {
</Button>
<span className={`${baseClass}__drag-label`}>or drag and drop a file here</span>
</div>
</>
</React.Fragment>
)}
<input
ref={inputRef}
@@ -195,13 +195,13 @@ const File = (props) => {
};
File.defaultProps = {
initialData: undefined,
data: undefined,
adminThumbnail: undefined,
};
File.propTypes = {
fieldTypes: PropTypes.shape({}).isRequired,
initialData: PropTypes.shape({
data: PropTypes.shape({
filename: PropTypes.string,
mimeType: PropTypes.string,
filesize: PropTypes.number,

View File

@@ -1,4 +1,4 @@
@import '../../../../scss/styles.scss';
@import '../../../../../scss/styles.scss';
.file-field {
position: relative;

View File

@@ -5,21 +5,14 @@ import config from 'payload/config';
import { useStepNav } from '../../../elements/StepNav';
import usePayloadAPI from '../../../../hooks/usePayloadAPI';
import { useUser } from '../../../data/User';
import formatFields from './formatFields';
import RenderCustomComponent from '../../../utilities/RenderCustomComponent';
import DefaultEdit from './Default';
import buildStateFromSchema from '../../../forms/Form/buildStateFromSchema';
const { serverURL, routes: { admin, api } } = config;
const EditView = (props) => {
const { params: { id } = {} } = useRouteMatch();
const { state: locationState } = useLocation();
const history = useHistory();
const { setStepNav } = useStepNav();
const [fields, setFields] = useState([]);
const { permissions } = useUser();
const { collection, isEditing } = props;
const {
@@ -30,8 +23,17 @@ const EditView = (props) => {
admin: {
useAsTitle,
},
fields,
auth,
} = collection;
const { params: { id } = {} } = useRouteMatch();
const { state: locationState } = useLocation();
const history = useHistory();
const { setStepNav } = useStepNav();
const [initialState, setInitialState] = useState({});
const { permissions } = useUser();
const onSave = (json) => {
history.push(`${admin}/collections/${collection.slug}/${json?.doc?.id}`, {
status: {
@@ -69,11 +71,24 @@ const EditView = (props) => {
}, [setStepNav, isEditing, pluralLabel, dataToRender, slug, useAsTitle]);
useEffect(() => {
setFields(formatFields(collection, isEditing));
}, [collection, isEditing]);
const awaitInitialState = async () => {
const state = await buildStateFromSchema(fields, dataToRender);
setInitialState(state);
};
awaitInitialState();
}, [dataToRender, fields]);
const collectionPermissions = permissions?.[slug];
const apiURL = `${serverURL}${api}/${slug}/${id}`;
let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}?depth=0`;
const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission);
if (auth && !isEditing) {
action = `${action}/register`;
}
return (
<RenderCustomComponent
DefaultComponent={DefaultEdit}
@@ -81,10 +96,14 @@ const EditView = (props) => {
componentProps={{
isLoading,
data: dataToRender,
collection: { ...collection, fields },
collection,
permissions: collectionPermissions,
isEditing,
onSave,
initialState,
hasSavePermission,
apiURL,
action,
}}
/>
);
@@ -106,6 +125,7 @@ EditView.propTypes = {
}),
fields: PropTypes.arrayOf(PropTypes.shape({})),
preview: PropTypes.func,
auth: PropTypes.shape({}),
}).isRequired,
isEditing: PropTypes.bool,
};

View File

@@ -47,7 +47,9 @@ const DefaultList = (props) => {
const [{ data }, { setParams }] = usePayloadAPI(apiURL, { initialParams: { depth: 0 } });
useEffect(() => {
const params = {};
const params = {
depth: 0,
};
if (page) params.page = page;
if (sort) params.sort = sort;