Compare commits

...

3 Commits

Author SHA1 Message Date
James
9aaadfa115 0.0.23 2020-07-22 22:40:59 -04:00
James
fe25c1920a implements nested forms for Array, Block, and Group field types 2020-07-22 22:40:45 -04:00
James
0f1ba6b315 breaks out html form from FormProvider 2020-07-22 21:58:07 -04:00
28 changed files with 827 additions and 655 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload",
"version": "0.0.22",
"version": "0.0.23",
"description": "CMS and Application Framework",
"license": "ISC",
"author": "Payload CMS LLC",

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import config from 'payload/config';
import Button from '../Button';
import { useForm } from '../../forms/Form/context';
import { useForm } from '../../forms/FormProvider/context';
import './index.scss';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useForm } from '../../forms/Form/context';
import { useForm } from '../../forms/FormProvider/context';
import { useUser } from '../../data/User';
import Button from '../Button';

View File

@@ -1,281 +1,21 @@
import React, {
useReducer, useEffect, useRef, useState,
} from 'react';
import { objectToFormData } from 'object-to-formdata';
import { useHistory } from 'react-router-dom';
import React from 'react';
import PropTypes from 'prop-types';
import { unflatten } from 'flatley';
import { useFormFields } from '../FormProvider/context';
import HiddenInput from '../field-types/HiddenInput';
import { useLocale } from '../../utilities/Locale';
import { useStatusList } from '../../elements/Status';
import { requests } from '../../../api';
import useThrottledEffect from '../../../hooks/useThrottledEffect';
import { useUser } from '../../data/User';
import fieldReducer from './fieldReducer';
import initContextState from './initContextState';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FieldContext } from './context';
import './index.scss';
const baseClass = 'form';
const reduceFieldsToValues = (fields, flatten) => {
const data = {};
Object.keys(fields).forEach((key) => {
if (!fields[key].disableFormData && fields[key].value !== undefined) {
data[key] = fields[key].value;
}
});
if (flatten) {
return unflatten(data, { safe: true });
}
return data;
};
const Form = (props) => {
const {
disabled,
onSubmit,
ajax,
method,
action,
handleResponse,
onSuccess,
children,
className,
redirect,
disableSuccessStatus,
} = props;
const history = useHistory();
const locale = useLocale();
const { replaceStatus, addStatus, clearStatus } = useStatusList();
const { refreshCookie } = useUser();
const [modified, setModified] = useState(false);
const [processing, setProcessing] = useState(false);
const [submitted, setSubmitted] = useState(false);
const contextRef = useRef({ ...initContextState });
const [fields, dispatchFields] = useReducer(fieldReducer, {});
contextRef.current.fields = fields;
contextRef.current.dispatchFields = dispatchFields;
contextRef.current.submit = (e) => {
if (disabled) {
e.preventDefault();
return false;
}
e.stopPropagation();
setSubmitted(true);
const isValid = contextRef.current.validateForm();
// If not valid, prevent submission
if (!isValid) {
e.preventDefault();
addStatus({
message: 'Please correct the fields below.',
type: 'error',
});
window.scrollTo({
top: 0,
behavior: 'smooth',
});
return false;
}
// If submit handler comes through via props, run that
if (onSubmit) {
e.preventDefault();
return onSubmit(fields);
}
// If form is AJAX, fetch data
if (ajax !== false) {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth',
});
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);
return res.json().then((json) => {
setProcessing(false);
clearStatus();
if (res.status < 400) {
if (typeof onSuccess === 'function') onSuccess(json);
if (redirect) {
const destination = {
pathname: redirect,
};
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;
}
addStatus({
message: 'An unknown error occurred.',
type: 'error',
});
}
return json;
});
}).catch((err) => {
addStatus({
message: err,
type: 'error',
});
});
}
return true;
};
contextRef.current.getFields = () => contextRef.current.fields;
contextRef.current.getField = (path) => contextRef.current.fields[path];
contextRef.current.getData = () => reduceFieldsToValues(contextRef.current.fields, true);
contextRef.current.getSiblingData = (path) => {
let siblingFields = contextRef.current.fields;
// If this field is nested
// We can provide a list of sibling fields
if (path.indexOf('.') > 0) {
const parentFieldPath = path.substring(0, path.lastIndexOf('.') + 1);
siblingFields = Object.keys(contextRef.current.fields).reduce((siblings, fieldKey) => {
if (fieldKey.indexOf(parentFieldPath) === 0) {
return {
...siblings,
[fieldKey.replace(parentFieldPath, '')]: contextRef.current.fields[fieldKey],
};
}
return siblings;
}, {});
}
return reduceFieldsToValues(siblingFields, true);
};
contextRef.current.getDataByPath = (path) => {
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
const name = path.split('.').pop();
const data = Object.keys(contextRef.current.fields).reduce((matchedData, key) => {
if (key.indexOf(`${path}.`) === 0) {
return {
...matchedData,
[key.replace(pathPrefixToRemove, '')]: contextRef.current.fields[key],
};
}
return matchedData;
}, {});
const values = reduceFieldsToValues(data, true);
const unflattenedData = unflatten(values);
return unflattenedData?.[name];
};
contextRef.current.getUnflattenedValues = () => reduceFieldsToValues(contextRef.current.fields);
contextRef.current.validateForm = () => !Object.values(contextRef.current.fields).some((field) => field.valid === false);
contextRef.current.createFormData = () => {
const data = reduceFieldsToValues(contextRef.current.fields);
return objectToFormData(data, { indices: true });
};
contextRef.current.setModified = setModified;
contextRef.current.setProcessing = setProcessing;
contextRef.current.setSubmitted = setSubmitted;
useThrottledEffect(() => {
refreshCookie();
}, 15000, [fields]);
useEffect(() => {
setModified(false);
}, [locale]);
const { submit } = useFormFields();
const classes = [
className,
@@ -285,63 +25,28 @@ const Form = (props) => {
return (
<form
noValidate
onSubmit={contextRef.current.submit}
method={method}
action={action}
onSubmit={submit}
className={classes}
>
<FormContext.Provider value={contextRef.current}>
<FieldContext.Provider value={{
fields,
...contextRef.current,
}}
>
<SubmittedContext.Provider value={submitted}>
<ProcessingContext.Provider value={processing}>
<ModifiedContext.Provider value={modified}>
<HiddenInput
path="locale"
defaultValue={locale}
/>
{children}
</ModifiedContext.Provider>
</ProcessingContext.Provider>
</SubmittedContext.Provider>
</FieldContext.Provider>
</FormContext.Provider>
<HiddenInput
path="locale"
defaultValue={locale}
/>
{children}
</form>
);
};
Form.defaultProps = {
redirect: '',
onSubmit: null,
ajax: true,
method: 'POST',
action: '',
handleResponse: null,
onSuccess: null,
className: '',
disableSuccessStatus: false,
disabled: false,
};
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,
onSuccess: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
className: PropTypes.string,
redirect: PropTypes.string,
disabled: PropTypes.bool,
};
export default Form;

View File

@@ -5,6 +5,26 @@ function fieldReducer(state, action) {
...action.value,
};
case 'REPLACE_ALL_BY_PATH': {
const { path, value = {} } = action;
const newState = Object.entries(state).reduce((reducedState, [key, val]) => {
if (key.indexOf(`${path}`) === 0) {
return reducedState;
}
return {
...reducedState,
[key]: val,
};
}, {});
return {
...newState,
...value,
};
}
case 'REMOVE': {
const newState = { ...state };
delete newState[action.path];

View File

@@ -0,0 +1,315 @@
import React, {
useReducer, useEffect, useRef, useState,
} from 'react';
import { objectToFormData } from 'object-to-formdata';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { unflatten } from 'flatley';
import { useLocale } from '../../utilities/Locale';
import { useStatusList } from '../../elements/Status';
import { requests } from '../../../api';
import useThrottledEffect from '../../../hooks/useThrottledEffect';
import { useUser } from '../../data/User';
import fieldReducer from './fieldReducer';
import initContextState from './initContextState';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FieldContext } from './context';
const reduceFieldsToValues = (fields, flatten) => {
const data = {};
Object.keys(fields).forEach((key) => {
if (!fields[key].disableFormData && fields[key].value !== undefined) {
data[key] = fields[key].value;
}
});
if (flatten) {
return unflatten(data, { safe: true });
}
return data;
};
const FormProvider = (props) => {
const {
disabled,
onSubmit,
method,
action,
handleResponse,
onSuccess,
children,
redirect,
disableSuccessStatus,
} = props;
const history = useHistory();
const locale = useLocale();
const { replaceStatus, addStatus, clearStatus } = useStatusList();
const { refreshCookie } = useUser();
const [modified, setModified] = useState(false);
const [processing, setProcessing] = useState(false);
const [submitted, setSubmitted] = useState(false);
const contextRef = useRef({ ...initContextState });
const [fields, dispatchFields] = useReducer(fieldReducer, {});
contextRef.current.fields = fields;
contextRef.current.dispatchFields = dispatchFields;
contextRef.current.submit = (e) => {
if (disabled) {
e.preventDefault();
return false;
}
e.stopPropagation();
setSubmitted(true);
const isValid = contextRef.current.validateForm();
// If not valid, prevent submission
if (!isValid) {
e.preventDefault();
addStatus({
message: 'Please correct the fields below.',
type: 'error',
});
window.scrollTo({
top: 0,
behavior: 'smooth',
});
return false;
}
// If submit handler comes through via props, run that
if (onSubmit) {
e.preventDefault();
return onSubmit(fields);
}
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth',
});
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);
return res.json().then((json) => {
setProcessing(false);
clearStatus();
if (res.status < 400) {
if (typeof onSuccess === 'function') onSuccess(json);
if (redirect) {
const destination = {
pathname: redirect,
};
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;
}
addStatus({
message: 'An unknown error occurred.',
type: 'error',
});
}
return json;
});
}).catch((err) => {
addStatus({
message: err,
type: 'error',
});
});
};
contextRef.current.getFields = () => contextRef.current.fields;
contextRef.current.getField = (path) => contextRef.current.fields[path];
contextRef.current.getData = () => reduceFieldsToValues(contextRef.current.fields, true);
contextRef.current.getSiblingData = (path) => {
let siblingFields = contextRef.current.fields;
// If this field is nested
// We can provide a list of sibling fields
if (path.indexOf('.') > 0) {
const parentFieldPath = path.substring(0, path.lastIndexOf('.') + 1);
siblingFields = Object.keys(contextRef.current.fields).reduce((siblings, fieldKey) => {
if (fieldKey.indexOf(parentFieldPath) === 0) {
return {
...siblings,
[fieldKey.replace(parentFieldPath, '')]: contextRef.current.fields[fieldKey],
};
}
return siblings;
}, {});
}
return reduceFieldsToValues(siblingFields, true);
};
contextRef.current.getDataByPath = (path) => {
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
const name = path.split('.').pop();
const data = Object.keys(contextRef.current.fields).reduce((matchedData, key) => {
if (key.indexOf(`${path}.`) === 0) {
return {
...matchedData,
[key.replace(pathPrefixToRemove, '')]: contextRef.current.fields[key],
};
}
return matchedData;
}, {});
const values = reduceFieldsToValues(data, true);
const unflattenedData = unflatten(values);
return unflattenedData?.[name];
};
contextRef.current.getUnflattenedValues = () => reduceFieldsToValues(contextRef.current.fields);
contextRef.current.validateForm = () => !Object.values(contextRef.current.fields).some((field) => field.valid === false);
contextRef.current.createFormData = () => {
const data = reduceFieldsToValues(contextRef.current.fields);
return objectToFormData(data, { indices: true });
};
contextRef.current.setModified = setModified;
contextRef.current.setProcessing = setProcessing;
contextRef.current.setSubmitted = setSubmitted;
useThrottledEffect(() => {
refreshCookie();
}, 15000, [fields]);
useEffect(() => {
setModified(false);
}, [locale]);
return (
<FormContext.Provider value={contextRef.current}>
<FieldContext.Provider value={{
fields,
...contextRef.current,
}}
>
<SubmittedContext.Provider value={submitted}>
<ProcessingContext.Provider value={processing}>
<ModifiedContext.Provider value={modified}>
{children}
</ModifiedContext.Provider>
</ProcessingContext.Provider>
</SubmittedContext.Provider>
</FieldContext.Provider>
</FormContext.Provider>
);
};
FormProvider.defaultProps = {
redirect: '',
onSubmit: null,
method: 'POST',
action: '',
handleResponse: null,
onSuccess: null,
className: '',
disableSuccessStatus: false,
disabled: false,
};
FormProvider.propTypes = {
disableSuccessStatus: PropTypes.bool,
onSubmit: PropTypes.func,
method: PropTypes.oneOf(['post', 'POST', 'get', 'GET', 'put', 'PUT', 'delete', 'DELETE']),
action: PropTypes.string,
handleResponse: PropTypes.func,
onSuccess: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
className: PropTypes.string,
redirect: PropTypes.string,
disabled: PropTypes.bool,
};
export default FormProvider;

View File

@@ -0,0 +1,49 @@
import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import FormProvider from '../FormProvider';
import { useFormFields } from '../FormProvider/context';
import useDebounce from '../../../hooks/useDebounce';
const SendValue = ({ sendValuesToParent }) => {
const { getFields } = useFormFields();
const fields = getFields();
const debouncedFields = useDebounce(fields, 500);
useEffect(() => {
sendValuesToParent(debouncedFields);
}, [sendValuesToParent, debouncedFields]);
return null;
};
const SubForm = (props) => {
const { path: pathFromProps, name, children } = props;
const path = pathFromProps || name;
const { dispatchFields } = useFormFields();
const sendValuesToParent = useCallback((fields) => {
dispatchFields({ type: 'REPLACE_ALL_BY_PATH', value: fields, path });
}, [dispatchFields, path]);
return (
<FormProvider>
{children}
<SendValue sendValuesToParent={sendValuesToParent} />
</FormProvider>
);
};
SubForm.defaultProps = {
path: '',
};
SubForm.propTypes = {
path: PropTypes.string,
name: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
export default SubForm;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useFormProcessing } from '../Form/context';
import { useFormProcessing } from '../FormProvider/context';
import Button from '../../elements/Button';
import './index.scss';

View File

@@ -8,10 +8,11 @@ import Button from '../../../elements/Button';
import DraggableSection from '../../DraggableSection';
import reducer from '../rowReducer';
import { useRenderedFields } from '../../RenderFields';
import { useForm } from '../../Form/context';
import { useForm } from '../../FormProvider/context';
import useFieldType from '../../useFieldType';
import Error from '../../Error';
import { array } from '../../../../../fields/validations';
import SubForm from '../../SubForm';
import './index.scss';
@@ -191,6 +192,7 @@ ArrayFieldType.defaultProps = {
minRows: undefined,
singularLabel: 'Row',
permissions: {},
path: '',
};
ArrayFieldType.propTypes = {
@@ -206,7 +208,7 @@ ArrayFieldType.propTypes = {
label: PropTypes.string,
singularLabel: PropTypes.string,
name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
path: PropTypes.string,
fieldTypes: PropTypes.shape({}).isRequired,
validate: PropTypes.func,
required: PropTypes.bool,
@@ -217,4 +219,23 @@ ArrayFieldType.propTypes = {
}),
};
export default withCondition(ArrayFieldType);
const ArrayForm = (props) => {
const { name, path } = props;
return (
<SubForm {...{ name, path }}>
<ArrayFieldType {...props} />
</SubForm>
);
};
ArrayForm.defaultProps = {
path: '',
};
ArrayForm.propTypes = {
path: PropTypes.string,
name: PropTypes.string.isRequired,
};
export default withCondition(ArrayForm);

View File

@@ -6,7 +6,7 @@ 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 { useFormFields } from '../../FormProvider/context';
import './index.scss';

View File

@@ -5,7 +5,7 @@ import Password from '../Password';
import Checkbox from '../Checkbox';
import Button from '../../../elements/Button';
import ConfirmPassword from '../ConfirmPassword';
import { useFormFields } from '../../Form/context';
import { useFormFields } from '../../FormProvider/context';
import APIKey from './APIKey';
import './index.scss';

View File

@@ -8,14 +8,15 @@ import { v4 as uuidv4 } from 'uuid';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
import reducer from '../rowReducer';
import { useForm } from '../../Form/context';
import { useForm } from '../../FormProvider/context';
import DraggableSection from '../../DraggableSection';
import { useRenderedFields } from '../../RenderFields';
import Error from '../../Error';
import useFieldType from '../../useFieldType';
import Popup from '../../../elements/Popup';
import BlockSelector from './BlockSelector';
import { blocks } from '../../../../../fields/validations';
import { blocks as blockValidator } from '../../../../../fields/validations';
import SubForm from '../../SubForm';
import './index.scss';
@@ -242,7 +243,7 @@ Blocks.defaultProps = {
defaultValue: [],
initialData: [],
singularLabel: 'Block',
validate: blocks,
validate: blockValidator,
required: false,
maxRows: undefined,
minRows: undefined,
@@ -276,4 +277,23 @@ Blocks.propTypes = {
}),
};
export default withCondition(Blocks);
const BlocksForm = (props) => {
const { name, path } = props;
return (
<SubForm {...{ name, path }}>
<Blocks {...props} />
</SubForm>
);
};
BlocksForm.defaultProps = {
path: '',
};
BlocksForm.propTypes = {
path: PropTypes.string,
name: PropTypes.string.isRequired,
};
export default withCondition(BlocksForm);

View File

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import { useFormFields } from '../../Form/context';
import { useFormFields } from '../../FormProvider/context';
import './index.scss';

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import RenderFields, { useRenderedFields } from '../../RenderFields';
import withCondition from '../../withCondition';
import SubForm from '../../SubForm';
import './index.scss';
@@ -47,4 +48,23 @@ Group.propTypes = {
fieldTypes: PropTypes.shape({}).isRequired,
};
export default withCondition(Group);
const GroupForm = (props) => {
const { name, path } = props;
return (
<SubForm {...{ name, path }}>
<Group {...props} />
</SubForm>
);
};
GroupForm.defaultProps = {
path: '',
};
GroupForm.propTypes = {
path: PropTypes.string,
name: PropTypes.string.isRequired,
};
export default withCondition(GroupForm);

View File

@@ -4,6 +4,7 @@ import { Modal, useModal } from '@faceless-ui/modal';
import config from '../../../../../config';
import MinimalTemplate from '../../../../templates/Minimal';
import Form from '../../../Form';
import FormProvider from '../../../FormProvider';
import Button from '../../../../elements/Button';
import formatFields from '../../../../views/collections/Edit/formatFields';
import RenderFields from '../../../RenderFields';
@@ -45,34 +46,36 @@ const AddUploadModal = (props) => {
slug={slug}
>
<MinimalTemplate width="wide">
<Form
<FormProvider
method="post"
action={`${serverURL}${api}/${collection.slug}`}
onSuccess={onSuccess}
disableSuccessStatus
>
<header className={`${baseClass}__header`}>
<h1>
New
{' '}
{collection.labels.singular}
</h1>
<FormSubmit>Save</FormSubmit>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={closeAll}
<Form>
<header className={`${baseClass}__header`}>
<h1>
New
{' '}
{collection.labels.singular}
</h1>
<FormSubmit>Save</FormSubmit>
<Button
icon="x"
round
buttonStyle="icon-label"
iconStyle="with-border"
onClick={closeAll}
/>
</header>
<RenderFields
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
customComponentsPath={`${collection.slug}.fields.`}
/>
</header>
<RenderFields
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
customComponentsPath={`${collection.slug}.fields.`}
/>
</Form>
</Form>
</FormProvider>
</MinimalTemplate>
</Modal>
);

View File

@@ -1,7 +1,7 @@
import {
useCallback, useEffect, useState,
} from 'react';
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context';
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../FormProvider/context';
import useDebounce from '../../../hooks/useDebounce';
import useUnmountEffect from '../../../hooks/useUnmountEffect';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useFormFields } from '../Form/context';
import { useFormFields } from '../FormProvider/context';
const withCondition = (Field) => {
const CheckForCondition = (props) => {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import NavigationPrompt from 'react-router-navigation-prompt';
import { useForm } from '../../forms/Form/context';
import { useForm } from '../../forms/FormProvider/context';
import MinimalTemplate from '../../templates/Minimal';
import Button from '../../elements/Button';

View File

@@ -5,6 +5,7 @@ import format from 'date-fns/format';
import config from 'payload/config';
import Eyebrow from '../../elements/Eyebrow';
import Form from '../../forms/Form';
import FormProvider from '../../forms/FormProvider';
import PreviewButton from '../../elements/PreviewButton';
import FormSubmit from '../../forms/Submit';
import RenderFields from '../../forms/RenderFields';
@@ -48,93 +49,95 @@ const DefaultAccount = (props) => {
return (
<div className={classes}>
<Form
<FormProvider
className={`${baseClass}__form`}
method="put"
action={`${serverURL}${api}/${slug}/${data?.id}`}
>
<div className={`${baseClass}__main`}>
<Eyebrow />
<LeaveWithoutSaving />
<div className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
<h1>
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
</h1>
</header>
<RenderFields
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={dataToRender}
customComponentsPath={`${slug}.fields.`}
/>
</div>
</div>
<div className={`${baseClass}__sidebar`}>
<ul className={`${baseClass}__collection-actions`}>
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
<li><DuplicateDocument slug={slug} /></li>
<li>
<DeleteDocument
collection={collection}
id={data?.id}
<Form>
<div className={`${baseClass}__main`}>
<Eyebrow />
<LeaveWithoutSaving />
<div className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
<h1>
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
</h1>
</header>
<RenderFields
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={dataToRender}
customComponentsPath={`${slug}.fields.`}
/>
</li>
</ul>
<div className={`${baseClass}__document-actions${preview ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
<PreviewButton generatePreviewURL={preview} />
<FormSubmit>Save</FormSubmit>
</div>
</div>
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL
{' '}
<CopyToClipboard value={apiURL} />
</span>
<a
href={apiURL}
target="_blank"
rel="noopener noreferrer"
>
{apiURL}
</a>
</div>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
filter={(field) => field.position === 'sidebar'}
position="sidebar"
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={dataToRender}
customComponentsPath={`${slug}.fields.`}
/>
</div>
<ul className={`${baseClass}__meta`}>
<li>
<div className={`${baseClass}__label`}>ID</div>
<div>{data?.id}</div>
</li>
{timestamps && (
<React.Fragment>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>Last Modified</div>
<div>{format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
{data.createdAt && (
<li>
<div className={`${baseClass}__label`}>Created</div>
<div>{format(new Date(data.createdAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
</React.Fragment>
)}
<div className={`${baseClass}__sidebar`}>
<ul className={`${baseClass}__collection-actions`}>
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
<li><DuplicateDocument slug={slug} /></li>
<li>
<DeleteDocument
collection={collection}
id={data?.id}
/>
</li>
</ul>
<div className={`${baseClass}__document-actions${preview ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
<PreviewButton generatePreviewURL={preview} />
<FormSubmit>Save</FormSubmit>
</div>
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL
{' '}
<CopyToClipboard value={apiURL} />
</span>
<a
href={apiURL}
target="_blank"
rel="noopener noreferrer"
>
{apiURL}
</a>
</div>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
filter={(field) => field.position === 'sidebar'}
position="sidebar"
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={dataToRender}
customComponentsPath={`${slug}.fields.`}
/>
</div>
<ul className={`${baseClass}__meta`}>
<li>
<div className={`${baseClass}__label`}>ID</div>
<div>{data?.id}</div>
</li>
{timestamps && (
<React.Fragment>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>Last Modified</div>
<div>{format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
{data.createdAt && (
<li>
<div className={`${baseClass}__label`}>Created</div>
<div>{format(new Date(data.createdAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
</React.Fragment>
)}
</ul>
</div>
</Form>
</ul>
</div>
</Form>
</FormProvider>
</div>
);
};
@@ -163,6 +166,7 @@ DefaultAccount.propTypes = {
data: PropTypes.shape({
updatedAt: PropTypes.string,
createdAt: PropTypes.string,
id: PropTypes.string,
}),
onSave: PropTypes.func,
};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import config from 'payload/config';
import MinimalTemplate from '../../templates/Minimal';
import FormProvider from '../../forms/FormProvider';
import Form from '../../forms/Form';
import RenderFields from '../../forms/RenderFields';
import * as fieldTypes from '../../forms/field-types';
@@ -48,21 +49,23 @@ const CreateFirstUser = (props) => {
<MinimalTemplate className={baseClass}>
<h1>Welcome to Payload</h1>
<p>To begin, create your first user.</p>
<Form
<FormProvider
onSuccess={onSuccess}
method="POST"
redirect={admin}
action={`${serverURL}${api}/${userSlug}/first-register`}
>
<RenderFields
fieldSchema={[
...fields,
...userConfig.fields,
]}
fieldTypes={fieldTypes}
/>
<FormSubmit>Create</FormSubmit>
</Form>
<Form>
<RenderFields
fieldSchema={[
...fields,
...userConfig.fields,
]}
fieldTypes={fieldTypes}
/>
<FormSubmit>Create</FormSubmit>
</Form>
</FormProvider>
</MinimalTemplate>
);
};

View File

@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import config from 'payload/config';
import MinimalTemplate from '../../templates/Minimal';
import StatusList, { useStatusList } from '../../elements/Status';
import FormProvider from '../../forms/FormProvider';
import Form from '../../forms/Form';
import Email from '../../forms/field-types/Email';
import FormSubmit from '../../forms/Submit';
@@ -84,21 +85,22 @@ const ForgotPassword = () => {
return (
<MinimalTemplate className={baseClass}>
<StatusList />
<Form
novalidate
<FormProvider
handleResponse={handleResponse}
method="POST"
action={`${serverURL}${api}/${userSlug}/forgot-password`}
>
<h1>Forgot Password</h1>
<p>Please enter your email below. You will receive an email message with instructions on how to reset your password.</p>
<Email
label="Email Address"
name="email"
required
/>
<FormSubmit>Submit</FormSubmit>
</Form>
<Form>
<h1>Forgot Password</h1>
<p>Please enter your email below. You will receive an email message with instructions on how to reset your password.</p>
<Email
label="Email Address"
name="email"
required
/>
<FormSubmit>Submit</FormSubmit>
</Form>
</FormProvider>
<Link to={`${admin}/login`}>Back to login</Link>
</MinimalTemplate>
);

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import format from 'date-fns/format';
import config from 'payload/config';
import Eyebrow from '../../elements/Eyebrow';
import FormProvider from '../../forms/FormProvider';
import Form from '../../forms/Form';
import PreviewButton from '../../elements/PreviewButton';
import FormSubmit from '../../forms/Submit';
@@ -35,84 +36,85 @@ const DefaultGlobalView = (props) => {
return (
<div className={baseClass}>
<Form
className={`${baseClass}__form`}
<FormProvider
method="post"
action={action}
onSuccess={onSave}
disabled={!hasSavePermission}
>
<div className={`${baseClass}__main`}>
<Eyebrow />
<LeaveWithoutSaving />
<div className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
<h1>
Edit
{' '}
{label}
</h1>
</header>
<RenderFields
operation="update"
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={field => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
<Form className={`${baseClass}__form`}>
<div className={`${baseClass}__main`}>
<Eyebrow />
<LeaveWithoutSaving />
<div className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
<h1>
Edit
{' '}
{label}
</h1>
</header>
<RenderFields
operation="update"
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</div>
</div>
</div>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__document-actions${preview ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
<PreviewButton generatePreviewURL={preview} />
{hasSavePermission && (
<FormSubmit>Save</FormSubmit>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__document-actions${preview ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
<PreviewButton generatePreviewURL={preview} />
{hasSavePermission && (
<FormSubmit>Save</FormSubmit>
)}
</div>
{data && (
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL
{' '}
<CopyToClipboard value={apiURL} />
</span>
<a
href={apiURL}
target="_blank"
rel="noopener noreferrer"
>
{apiURL}
</a>
</div>
)}
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
operation="update"
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={(field) => field.position === 'sidebar'}
position="sidebar"
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</div>
{data && (
<ul className={`${baseClass}__meta`}>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>Last Modified</div>
<div>{format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
</ul>
)}
</div>
{data && (
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL
{' '}
<CopyToClipboard value={apiURL} />
</span>
<a
href={apiURL}
target="_blank"
rel="noopener noreferrer"
>
{apiURL}
</a>
</div>
)}
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
operation="update"
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={field => field.position === 'sidebar'}
position="sidebar"
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</div>
{data && (
<ul className={`${baseClass}__meta`}>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>Last Modified</div>
<div>{format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
</ul>
)}
</div>
</Form>
</Form>
</FormProvider>
</div>
);
};

View File

@@ -3,6 +3,7 @@ import { Link, useHistory } from 'react-router-dom';
import config from 'payload/config';
import Logo from '../../graphics/Logo';
import MinimalTemplate from '../../templates/Minimal';
import FormProvider from '../../forms/FormProvider';
import Form from '../../forms/Form';
import Email from '../../forms/field-types/Email';
import Password from '../../forms/field-types/Password';
@@ -57,29 +58,31 @@ const Login = () => {
<div className={`${baseClass}__brand`}>
<Logo />
</div>
<Form
<FormProvider
disableSuccessStatus
onSuccess={onSuccess}
method="POST"
action={`${serverURL}${api}/${userSlug}/login`}
>
<Email
label="Email Address"
name="email"
autoComplete="email"
required
/>
<Password
error="password"
label="Password"
name="password"
required
/>
<Link to={`${admin}/forgot`}>
Forgot password?
</Link>
<FormSubmit>Login</FormSubmit>
</Form>
<Form>
<Email
label="Email Address"
name="email"
autoComplete="email"
required
/>
<Password
error="password"
label="Password"
name="password"
required
/>
<Link to={`${admin}/forgot`}>
Forgot password?
</Link>
<FormSubmit>Login</FormSubmit>
</Form>
</FormProvider>
</MinimalTemplate>
);
};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { Link, useHistory, useParams } from 'react-router-dom';
import config from 'payload/config';
import StatusList from '../../elements/Status';
import FormProvider from '../../forms/FormProvider';
import Form from '../../forms/Form';
import Password from '../../forms/field-types/Password';
import FormSubmit from '../../forms/Submit';
@@ -59,24 +60,26 @@ const ResetPassword = () => {
<div className={baseClass}>
<div className={`${baseClass}__wrap`}>
<StatusList />
<Form
<FormProvider
handleResponse={handleResponse}
method="POST"
action={`${serverURL}${api}/${userSlug}/reset-password`}
redirect={admin}
>
<Password
error="password"
label="Password"
name="password"
required
/>
<HiddenInput
name="token"
defaultValue={token}
/>
<FormSubmit>Reset Password</FormSubmit>
</Form>
<Form>
<Password
error="password"
label="Password"
name="password"
required
/>
<HiddenInput
name="token"
defaultValue={token}
/>
<FormSubmit>Reset Password</FormSubmit>
</Form>
</FormProvider>
</div>
</div>
);

View File

@@ -4,6 +4,7 @@ import { Link, useRouteMatch } from 'react-router-dom';
import format from 'date-fns/format';
import config from 'payload/config';
import Eyebrow from '../../../elements/Eyebrow';
import FormProvider from '../../../forms/FormProvider';
import Form from '../../../forms/Form';
import Loading from '../../../elements/Loading';
import PreviewButton from '../../../elements/PreviewButton';
@@ -55,130 +56,131 @@ const DefaultEditView = (props) => {
return (
<div className={classes}>
<Form
className={`${baseClass}__form`}
<FormProvider
method={id ? 'put' : 'post'}
action={action}
onSuccess={onSave}
disabled={!hasSavePermission}
>
<div className={`${baseClass}__main`}>
<Eyebrow />
<LeaveWithoutSaving />
<div className={`${baseClass}__edit`}>
{isLoading && (
<Loading />
)}
{!isLoading && (
<React.Fragment>
<header className={`${baseClass}__header`}>
<h1>
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
</h1>
</header>
<RenderFields
operation={isEditing ? 'update' : 'create'}
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</React.Fragment>
)}
</div>
</div>
<div className={`${baseClass}__sidebar`}>
{isEditing ? (
<ul className={`${baseClass}__collection-actions`}>
{permissions?.create?.permission && (
<Form className={`${baseClass}__form`}>
<div className={`${baseClass}__main`}>
<Eyebrow />
<LeaveWithoutSaving />
<div className={`${baseClass}__edit`}>
{isLoading && (
<Loading />
)}
{!isLoading && (
<React.Fragment>
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
<li><DuplicateDocument slug={slug} /></li>
</React.Fragment>
)}
{permissions?.delete?.permission && (
<li>
<DeleteDocument
collection={collection}
id={id}
/>
</li>
)}
</ul>
) : undefined}
<div className={`${baseClass}__sidebar-sticky`}>
<div className={`${baseClass}__document-actions${(preview && isEditing) ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
{isEditing && (
<PreviewButton generatePreviewURL={preview} />
)}
{hasSavePermission && (
<FormSubmit>Save</FormSubmit>
)}
</div>
{isEditing && (
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL
{' '}
<CopyToClipboard value={apiURL} />
</span>
<a
href={apiURL}
target="_blank"
rel="noopener noreferrer"
>
{apiURL}
</a>
</div>
)}
{!isLoading && (
<React.Fragment>
<div className={`${baseClass}__sidebar-fields`}>
<header className={`${baseClass}__header`}>
<h1>
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
</h1>
</header>
<RenderFields
operation={isEditing ? 'update' : 'create'}
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={(field) => field?.admin?.position === 'sidebar'}
position="sidebar"
filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</div>
{isEditing && (
<ul className={`${baseClass}__meta`}>
<li>
<div className={`${baseClass}__label`}>ID</div>
<div>{id}</div>
</li>
{timestamps && (
<React.Fragment>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>Last Modified</div>
<div>{format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
{data.createdAt && (
<li>
<div className={`${baseClass}__label`}>Created</div>
<div>{format(new Date(data.createdAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
</React.Fragment>
)}
</ul>
)}
</React.Fragment>
)}
</React.Fragment>
)}
</div>
</div>
</div>
</Form>
<div className={`${baseClass}__sidebar`}>
{isEditing ? (
<ul className={`${baseClass}__collection-actions`}>
{permissions?.create?.permission && (
<React.Fragment>
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li>
<li><DuplicateDocument slug={slug} /></li>
</React.Fragment>
)}
{permissions?.delete?.permission && (
<li>
<DeleteDocument
collection={collection}
id={id}
/>
</li>
)}
</ul>
) : undefined}
<div className={`${baseClass}__sidebar-sticky`}>
<div className={`${baseClass}__document-actions${(preview && isEditing) ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
{isEditing && (
<PreviewButton generatePreviewURL={preview} />
)}
{hasSavePermission && (
<FormSubmit>Save</FormSubmit>
)}
</div>
{isEditing && (
<div className={`${baseClass}__api-url`}>
<span className={`${baseClass}__label`}>
API URL
{' '}
<CopyToClipboard value={apiURL} />
</span>
<a
href={apiURL}
target="_blank"
rel="noopener noreferrer"
>
{apiURL}
</a>
</div>
)}
{!isLoading && (
<React.Fragment>
<div className={`${baseClass}__sidebar-fields`}>
<RenderFields
operation={isEditing ? 'update' : 'create'}
readOnly={!hasSavePermission}
permissions={permissions.fields}
filter={(field) => field?.admin?.position === 'sidebar'}
position="sidebar"
fieldTypes={fieldTypes}
fieldSchema={fields}
initialData={data}
customComponentsPath={`${slug}.fields.`}
/>
</div>
{isEditing && (
<ul className={`${baseClass}__meta`}>
<li>
<div className={`${baseClass}__label`}>ID</div>
<div>{id}</div>
</li>
{timestamps && (
<React.Fragment>
{data.updatedAt && (
<li>
<div className={`${baseClass}__label`}>Last Modified</div>
<div>{format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
{data.createdAt && (
<li>
<div className={`${baseClass}__label`}>Created</div>
<div>{format(new Date(data.createdAt), 'MMMM do yyyy, h:mma')}</div>
</li>
)}
</React.Fragment>
)}
</ul>
)}
</React.Fragment>
)}
</div>
</div>
</Form>
</FormProvider>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import { useFormFields } from '../components/forms/Form/context';
import { useFormFields } from '../components/forms/FormProvider/context';
const useTitle = (useAsTitle) => {
const { getField } = useFormFields();