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", "name": "@payloadcms/payload",
"version": "0.0.22", "version": "0.0.23",
"description": "CMS and Application Framework", "description": "CMS and Application Framework",
"license": "ISC", "license": "ISC",
"author": "Payload CMS LLC", "author": "Payload CMS LLC",

View File

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

View File

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

View File

@@ -1,281 +1,21 @@
import React, { import React from '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 PropTypes from 'prop-types';
import { unflatten } from 'flatley'; import { useFormFields } from '../FormProvider/context';
import HiddenInput from '../field-types/HiddenInput'; import HiddenInput from '../field-types/HiddenInput';
import { useLocale } from '../../utilities/Locale'; 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'; import './index.scss';
const baseClass = 'form'; 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 Form = (props) => {
const { const {
disabled,
onSubmit,
ajax,
method,
action,
handleResponse,
onSuccess,
children, children,
className, className,
redirect,
disableSuccessStatus,
} = props; } = props;
const history = useHistory();
const locale = useLocale(); const locale = useLocale();
const { replaceStatus, addStatus, clearStatus } = useStatusList(); const { submit } = useFormFields();
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 classes = [ const classes = [
className, className,
@@ -285,63 +25,28 @@ const Form = (props) => {
return ( return (
<form <form
noValidate noValidate
onSubmit={contextRef.current.submit} onSubmit={submit}
method={method}
action={action}
className={classes} className={classes}
> >
<FormContext.Provider value={contextRef.current}> <HiddenInput
<FieldContext.Provider value={{ path="locale"
fields, defaultValue={locale}
...contextRef.current, />
}} {children}
>
<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>
</form> </form>
); );
}; };
Form.defaultProps = { Form.defaultProps = {
redirect: '',
onSubmit: null,
ajax: true,
method: 'POST',
action: '',
handleResponse: null,
onSuccess: null,
className: '', className: '',
disableSuccessStatus: false,
disabled: false,
}; };
Form.propTypes = { 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([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
PropTypes.node, PropTypes.node,
]).isRequired, ]).isRequired,
className: PropTypes.string, className: PropTypes.string,
redirect: PropTypes.string,
disabled: PropTypes.bool,
}; };
export default Form; export default Form;

View File

@@ -5,6 +5,26 @@ function fieldReducer(state, action) {
...action.value, ...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': { case 'REMOVE': {
const newState = { ...state }; const newState = { ...state };
delete newState[action.path]; 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 React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useFormProcessing } from '../Form/context'; import { useFormProcessing } from '../FormProvider/context';
import Button from '../../elements/Button'; import Button from '../../elements/Button';
import './index.scss'; import './index.scss';

View File

@@ -8,10 +8,11 @@ import Button from '../../../elements/Button';
import DraggableSection from '../../DraggableSection'; import DraggableSection from '../../DraggableSection';
import reducer from '../rowReducer'; import reducer from '../rowReducer';
import { useRenderedFields } from '../../RenderFields'; import { useRenderedFields } from '../../RenderFields';
import { useForm } from '../../Form/context'; import { useForm } from '../../FormProvider/context';
import useFieldType from '../../useFieldType'; import useFieldType from '../../useFieldType';
import Error from '../../Error'; import Error from '../../Error';
import { array } from '../../../../../fields/validations'; import { array } from '../../../../../fields/validations';
import SubForm from '../../SubForm';
import './index.scss'; import './index.scss';
@@ -191,6 +192,7 @@ ArrayFieldType.defaultProps = {
minRows: undefined, minRows: undefined,
singularLabel: 'Row', singularLabel: 'Row',
permissions: {}, permissions: {},
path: '',
}; };
ArrayFieldType.propTypes = { ArrayFieldType.propTypes = {
@@ -206,7 +208,7 @@ ArrayFieldType.propTypes = {
label: PropTypes.string, label: PropTypes.string,
singularLabel: PropTypes.string, singularLabel: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string,
fieldTypes: PropTypes.shape({}).isRequired, fieldTypes: PropTypes.shape({}).isRequired,
validate: PropTypes.func, validate: PropTypes.func,
required: PropTypes.bool, 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 Button from '../../../elements/Button';
import CopyToClipboard from '../../../elements/CopyToClipboard'; import CopyToClipboard from '../../../elements/CopyToClipboard';
import { text } from '../../../../../fields/validations'; import { text } from '../../../../../fields/validations';
import { useFormFields } from '../../Form/context'; import { useFormFields } from '../../FormProvider/context';
import './index.scss'; import './index.scss';

View File

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

View File

@@ -8,14 +8,15 @@ import { v4 as uuidv4 } from 'uuid';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import Button from '../../../elements/Button'; import Button from '../../../elements/Button';
import reducer from '../rowReducer'; import reducer from '../rowReducer';
import { useForm } from '../../Form/context'; import { useForm } from '../../FormProvider/context';
import DraggableSection from '../../DraggableSection'; import DraggableSection from '../../DraggableSection';
import { useRenderedFields } from '../../RenderFields'; import { useRenderedFields } from '../../RenderFields';
import Error from '../../Error'; import Error from '../../Error';
import useFieldType from '../../useFieldType'; import useFieldType from '../../useFieldType';
import Popup from '../../../elements/Popup'; import Popup from '../../../elements/Popup';
import BlockSelector from './BlockSelector'; import BlockSelector from './BlockSelector';
import { blocks } from '../../../../../fields/validations'; import { blocks as blockValidator } from '../../../../../fields/validations';
import SubForm from '../../SubForm';
import './index.scss'; import './index.scss';
@@ -242,7 +243,7 @@ Blocks.defaultProps = {
defaultValue: [], defaultValue: [],
initialData: [], initialData: [],
singularLabel: 'Block', singularLabel: 'Block',
validate: blocks, validate: blockValidator,
required: false, required: false,
maxRows: undefined, maxRows: undefined,
minRows: 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 useFieldType from '../../useFieldType';
import Label from '../../Label'; import Label from '../../Label';
import Error from '../../Error'; import Error from '../../Error';
import { useFormFields } from '../../Form/context'; import { useFormFields } from '../../FormProvider/context';
import './index.scss'; import './index.scss';

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import RenderFields, { useRenderedFields } from '../../RenderFields'; import RenderFields, { useRenderedFields } from '../../RenderFields';
import withCondition from '../../withCondition'; import withCondition from '../../withCondition';
import SubForm from '../../SubForm';
import './index.scss'; import './index.scss';
@@ -47,4 +48,23 @@ Group.propTypes = {
fieldTypes: PropTypes.shape({}).isRequired, 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 config from '../../../../../config';
import MinimalTemplate from '../../../../templates/Minimal'; import MinimalTemplate from '../../../../templates/Minimal';
import Form from '../../../Form'; import Form from '../../../Form';
import FormProvider from '../../../FormProvider';
import Button from '../../../../elements/Button'; import Button from '../../../../elements/Button';
import formatFields from '../../../../views/collections/Edit/formatFields'; import formatFields from '../../../../views/collections/Edit/formatFields';
import RenderFields from '../../../RenderFields'; import RenderFields from '../../../RenderFields';
@@ -45,34 +46,36 @@ const AddUploadModal = (props) => {
slug={slug} slug={slug}
> >
<MinimalTemplate width="wide"> <MinimalTemplate width="wide">
<Form <FormProvider
method="post" method="post"
action={`${serverURL}${api}/${collection.slug}`} action={`${serverURL}${api}/${collection.slug}`}
onSuccess={onSuccess} onSuccess={onSuccess}
disableSuccessStatus disableSuccessStatus
> >
<header className={`${baseClass}__header`}> <Form>
<h1> <header className={`${baseClass}__header`}>
New <h1>
{' '} New
{collection.labels.singular} {' '}
</h1> {collection.labels.singular}
<FormSubmit>Save</FormSubmit> </h1>
<Button <FormSubmit>Save</FormSubmit>
icon="x" <Button
round icon="x"
buttonStyle="icon-label" round
iconStyle="with-border" buttonStyle="icon-label"
onClick={closeAll} 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> </Form>
<RenderFields </FormProvider>
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldTypes={fieldTypes}
fieldSchema={fields}
customComponentsPath={`${collection.slug}.fields.`}
/>
</Form>
</MinimalTemplate> </MinimalTemplate>
</Modal> </Modal>
); );

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import NavigationPrompt from 'react-router-navigation-prompt'; 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 MinimalTemplate from '../../templates/Minimal';
import Button from '../../elements/Button'; import Button from '../../elements/Button';

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import format from 'date-fns/format'; import format from 'date-fns/format';
import config from 'payload/config'; import config from 'payload/config';
import Eyebrow from '../../elements/Eyebrow'; import Eyebrow from '../../elements/Eyebrow';
import FormProvider from '../../forms/FormProvider';
import Form from '../../forms/Form'; import Form from '../../forms/Form';
import PreviewButton from '../../elements/PreviewButton'; import PreviewButton from '../../elements/PreviewButton';
import FormSubmit from '../../forms/Submit'; import FormSubmit from '../../forms/Submit';
@@ -35,84 +36,85 @@ const DefaultGlobalView = (props) => {
return ( return (
<div className={baseClass}> <div className={baseClass}>
<Form <FormProvider
className={`${baseClass}__form`}
method="post" method="post"
action={action} action={action}
onSuccess={onSave} onSuccess={onSave}
disabled={!hasSavePermission} disabled={!hasSavePermission}
> >
<div className={`${baseClass}__main`}> <Form className={`${baseClass}__form`}>
<Eyebrow /> <div className={`${baseClass}__main`}>
<LeaveWithoutSaving /> <Eyebrow />
<div className={`${baseClass}__edit`}> <LeaveWithoutSaving />
<header className={`${baseClass}__header`}> <div className={`${baseClass}__edit`}>
<h1> <header className={`${baseClass}__header`}>
Edit <h1>
{' '} Edit
{label} {' '}
</h1> {label}
</header> </h1>
<RenderFields </header>
operation="update" <RenderFields
readOnly={!hasSavePermission} operation="update"
permissions={permissions.fields} readOnly={!hasSavePermission}
filter={field => (!field.position || (field.position && field.position !== 'sidebar'))} permissions={permissions.fields}
fieldTypes={fieldTypes} filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
fieldSchema={fields} fieldTypes={fieldTypes}
initialData={data} fieldSchema={fields}
customComponentsPath={`${slug}.fields.`} initialData={data}
/> customComponentsPath={`${slug}.fields.`}
/>
</div>
</div> </div>
</div> <div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar`}> <div className={`${baseClass}__document-actions${preview ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
<div className={`${baseClass}__document-actions${preview ? ` ${baseClass}__document-actions--with-preview` : ''}`}> <PreviewButton generatePreviewURL={preview} />
<PreviewButton generatePreviewURL={preview} /> {hasSavePermission && (
{hasSavePermission && ( <FormSubmit>Save</FormSubmit>
<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> </div>
{data && ( </Form>
<div className={`${baseClass}__api-url`}> </FormProvider>
<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>
</div> </div>
); );
}; };

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { Link, useRouteMatch } from 'react-router-dom';
import format from 'date-fns/format'; import format from 'date-fns/format';
import config from 'payload/config'; import config from 'payload/config';
import Eyebrow from '../../../elements/Eyebrow'; import Eyebrow from '../../../elements/Eyebrow';
import FormProvider from '../../../forms/FormProvider';
import Form from '../../../forms/Form'; import Form from '../../../forms/Form';
import Loading from '../../../elements/Loading'; import Loading from '../../../elements/Loading';
import PreviewButton from '../../../elements/PreviewButton'; import PreviewButton from '../../../elements/PreviewButton';
@@ -55,130 +56,131 @@ const DefaultEditView = (props) => {
return ( return (
<div className={classes}> <div className={classes}>
<Form <FormProvider
className={`${baseClass}__form`}
method={id ? 'put' : 'post'} method={id ? 'put' : 'post'}
action={action} action={action}
onSuccess={onSave} onSuccess={onSave}
disabled={!hasSavePermission} disabled={!hasSavePermission}
> >
<div className={`${baseClass}__main`}> <Form className={`${baseClass}__form`}>
<Eyebrow /> <div className={`${baseClass}__main`}>
<LeaveWithoutSaving /> <Eyebrow />
<div className={`${baseClass}__edit`}> <LeaveWithoutSaving />
{isLoading && ( <div className={`${baseClass}__edit`}>
<Loading /> {isLoading && (
)} <Loading />
{!isLoading && ( )}
<React.Fragment> {!isLoading && (
<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 && (
<React.Fragment> <React.Fragment>
<li><Link to={`${admin}/collections/${slug}/create`}>Create New</Link></li> <header className={`${baseClass}__header`}>
<li><DuplicateDocument slug={slug} /></li> <h1>
</React.Fragment> <RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
)} </h1>
{permissions?.delete?.permission && ( </header>
<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 <RenderFields
operation={isEditing ? 'update' : 'create'} operation={isEditing ? 'update' : 'create'}
readOnly={!hasSavePermission} readOnly={!hasSavePermission}
permissions={permissions.fields} permissions={permissions.fields}
filter={(field) => field?.admin?.position === 'sidebar'} filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))}
position="sidebar"
fieldTypes={fieldTypes} fieldTypes={fieldTypes}
fieldSchema={fields} fieldSchema={fields}
initialData={data} initialData={data}
customComponentsPath={`${slug}.fields.`} customComponentsPath={`${slug}.fields.`}
/> />
</div> </React.Fragment>
{isEditing && ( )}
<ul className={`${baseClass}__meta`}> </div>
<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>
</div> <div className={`${baseClass}__sidebar`}>
</Form> {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> </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 useTitle = (useAsTitle) => {
const { getField } = useFormFields(); const { getField } = useFormFields();