Merge branch 'master' of github.com:trouble/payload
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useConfig } from '../../providers/Config';
|
||||
@@ -8,7 +9,6 @@ import MinimalTemplate from '../../templates/Minimal';
|
||||
import { useForm } from '../../forms/Form/context';
|
||||
import useTitle from '../../../hooks/useTitle';
|
||||
import { requests } from '../../../api';
|
||||
import { useStatusList } from '../Status';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -30,7 +30,6 @@ const DeleteDocument = (props) => {
|
||||
} = props;
|
||||
|
||||
const { serverURL, routes: { api, admin } } = useConfig();
|
||||
const { replaceStatus } = useStatusList();
|
||||
const { setModified } = useForm();
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const { closeAll, toggle } = useModal();
|
||||
@@ -41,11 +40,8 @@ const DeleteDocument = (props) => {
|
||||
const modalSlug = `delete-${id}`;
|
||||
|
||||
const addDefaultError = useCallback(() => {
|
||||
replaceStatus([{
|
||||
message: `There was an error while deleting ${title}. Please check your connection and try again.`,
|
||||
type: 'error',
|
||||
}]);
|
||||
}, [replaceStatus, title]);
|
||||
toast.error(`There was an error while deleting ${title}. Please check your connection and try again.`);
|
||||
}, [title]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
setDeleting(true);
|
||||
@@ -72,7 +68,7 @@ const DeleteDocument = (props) => {
|
||||
closeAll();
|
||||
|
||||
if (json.errors) {
|
||||
replaceStatus(json.errors);
|
||||
toast.error(json.errors);
|
||||
}
|
||||
addDefaultError();
|
||||
return false;
|
||||
@@ -80,7 +76,7 @@ const DeleteDocument = (props) => {
|
||||
return addDefaultError();
|
||||
}
|
||||
});
|
||||
}, [addDefaultError, closeAll, history, id, replaceStatus, singular, slug, title, admin, api, serverURL, setModified]);
|
||||
}, [addDefaultError, closeAll, history, id, singular, slug, title, admin, api, serverURL, setModified]);
|
||||
|
||||
if (id) {
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import Button from '../Button';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import { useStatusList } from '../Status';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -16,17 +16,13 @@ const GenerateConfirmation = (props) => {
|
||||
} = props;
|
||||
|
||||
const { toggle } = useModal();
|
||||
const { replaceStatus } = useStatusList();
|
||||
|
||||
const modalSlug = 'generate-confirmation';
|
||||
|
||||
const handleGenerate = () => {
|
||||
setKey();
|
||||
toggle(modalSlug);
|
||||
replaceStatus([{
|
||||
message: 'New API Key Generated.',
|
||||
type: 'success',
|
||||
}]);
|
||||
toast.success('New API Key Generated.', { autoClose: 3000 });
|
||||
highlightField(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, {
|
||||
useReducer, createContext, useContext, useEffect, useCallback,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import X from '../../icons/X';
|
||||
import reducer from './reducer';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'status-list';
|
||||
|
||||
const Context = createContext({});
|
||||
|
||||
const useStatusList = () => useContext(Context);
|
||||
|
||||
const StatusListProvider = ({ children }) => {
|
||||
const [statusList, dispatchStatus] = useReducer(reducer, []);
|
||||
const { pathname, state } = useLocation();
|
||||
|
||||
const removeStatus = useCallback((i) => dispatchStatus({ type: 'REMOVE', payload: i }), []);
|
||||
const addStatus = useCallback((status) => dispatchStatus({ type: 'ADD', payload: status }), []);
|
||||
const clearStatus = useCallback(() => dispatchStatus({ type: 'CLEAR' }), []);
|
||||
const replaceStatus = useCallback((status) => dispatchStatus({ type: 'REPLACE', payload: status }), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (state && state.status) {
|
||||
if (Array.isArray(state.status)) {
|
||||
replaceStatus(state.status);
|
||||
} else {
|
||||
replaceStatus([state.status]);
|
||||
}
|
||||
} else {
|
||||
clearStatus();
|
||||
}
|
||||
}, [addStatus, replaceStatus, clearStatus, state, pathname]);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{
|
||||
statusList,
|
||||
removeStatus,
|
||||
addStatus,
|
||||
clearStatus,
|
||||
replaceStatus,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
StatusListProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
const StatusList = () => {
|
||||
const { statusList, removeStatus } = useStatusList();
|
||||
|
||||
if (statusList.length > 0) {
|
||||
return (
|
||||
<ul className={baseClass}>
|
||||
{statusList.map((status, i) => {
|
||||
const classes = [
|
||||
`${baseClass}__status`,
|
||||
`${baseClass}__status--${status.type}`,
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classes}
|
||||
key={i}
|
||||
>
|
||||
{status.message}
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeStatus(i);
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export {
|
||||
StatusListProvider,
|
||||
useStatusList,
|
||||
};
|
||||
|
||||
export default StatusList;
|
||||
@@ -1,47 +0,0 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.status-list {
|
||||
position: relative;
|
||||
z-index: $z-status;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
background: $color-green;
|
||||
color: $color-dark-gray;
|
||||
padding: base(.5) $baseline;
|
||||
margin-bottom: 1px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
button {
|
||||
@extend %btn-reset;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li.status-list__status--error {
|
||||
background: $color-red;
|
||||
color: white;
|
||||
|
||||
button svg {
|
||||
@include color-svg(white);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
const statusReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'ADD': {
|
||||
const newState = [
|
||||
...state,
|
||||
action.payload,
|
||||
];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
|
||||
case 'REMOVE': {
|
||||
const statusList = [...state];
|
||||
statusList.splice(action.payload, 1);
|
||||
return statusList;
|
||||
}
|
||||
|
||||
case 'CLEAR': {
|
||||
return [];
|
||||
}
|
||||
|
||||
case 'REPLACE': {
|
||||
return action.payload;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default statusReducer;
|
||||
@@ -4,8 +4,8 @@ import React, {
|
||||
import { objectToFormData } from 'object-to-formdata';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { useStatusList } from '../../elements/Status';
|
||||
import { requests } from '../../../api';
|
||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||
import { useAuth } from '../../providers/Authentication';
|
||||
@@ -38,14 +38,12 @@ const Form = (props) => {
|
||||
disableSuccessStatus,
|
||||
initialState, // fully formed initial field state
|
||||
initialData, // values only, paths are required as key - form should build initial state as convenience
|
||||
disableScrollOnSuccess,
|
||||
waitForAutocomplete,
|
||||
log,
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
const locale = useLocale();
|
||||
const { replaceStatus, addStatus, clearStatus } = useStatusList();
|
||||
const { refreshCookie } = useAuth();
|
||||
|
||||
const [modified, setModified] = useState(false);
|
||||
@@ -111,17 +109,7 @@ const Form = (props) => {
|
||||
|
||||
// If not valid, prevent submission
|
||||
if (!isValid) {
|
||||
addStatus({
|
||||
message: 'Please correct the fields below.',
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
if (!disableScrollOnSuccess) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
toast.error('Please correct invalid fields.');
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -131,13 +119,6 @@ const Form = (props) => {
|
||||
return onSubmit(fields, reduceFieldsToValues(fields));
|
||||
}
|
||||
|
||||
if (!disableScrollOnSuccess) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
const formData = contextRef.current.createFormData();
|
||||
|
||||
try {
|
||||
@@ -151,7 +132,6 @@ const Form = (props) => {
|
||||
|
||||
|
||||
setProcessing(false);
|
||||
clearStatus();
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
const isJSON = contentType && contentType.indexOf('application/json') !== -1;
|
||||
@@ -182,20 +162,13 @@ const Form = (props) => {
|
||||
|
||||
history.push(destination);
|
||||
} else if (!disableSuccessStatus) {
|
||||
replaceStatus([{
|
||||
message: json.message || 'Submission successful.',
|
||||
type: 'success',
|
||||
disappear: 3000,
|
||||
}]);
|
||||
toast.success(json.message || 'Submission successful.', { autoClose: 3000 });
|
||||
}
|
||||
} else {
|
||||
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
|
||||
|
||||
if (json.message) {
|
||||
addStatus({
|
||||
message: json.message,
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(json.message);
|
||||
|
||||
return json;
|
||||
}
|
||||
@@ -218,10 +191,7 @@ const Form = (props) => {
|
||||
});
|
||||
|
||||
nonFieldErrors.forEach((err) => {
|
||||
addStatus({
|
||||
message: err.message || 'An unknown error occurred.',
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(err.message || 'An unknown error occurred.');
|
||||
});
|
||||
|
||||
return json;
|
||||
@@ -229,25 +199,17 @@ const Form = (props) => {
|
||||
|
||||
const message = errorMessages[res.status] || 'An unknown error occurrred.';
|
||||
|
||||
addStatus({
|
||||
message,
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(message);
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (err) {
|
||||
setProcessing(false);
|
||||
|
||||
return addStatus({
|
||||
message: err,
|
||||
type: 'error',
|
||||
});
|
||||
toast.error(err);
|
||||
}
|
||||
}, [
|
||||
action,
|
||||
addStatus,
|
||||
clearStatus,
|
||||
disableSuccessStatus,
|
||||
disabled,
|
||||
fields,
|
||||
@@ -257,8 +219,6 @@ const Form = (props) => {
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
redirect,
|
||||
replaceStatus,
|
||||
disableScrollOnSuccess,
|
||||
waitForAutocomplete,
|
||||
]);
|
||||
|
||||
@@ -366,7 +326,6 @@ Form.defaultProps = {
|
||||
disableSuccessStatus: false,
|
||||
disabled: false,
|
||||
initialState: undefined,
|
||||
disableScrollOnSuccess: false,
|
||||
waitForAutocomplete: false,
|
||||
initialData: undefined,
|
||||
log: false,
|
||||
@@ -387,7 +346,6 @@ Form.propTypes = {
|
||||
redirect: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
initialState: PropTypes.shape({}),
|
||||
disableScrollOnSuccess: PropTypes.bool,
|
||||
waitForAutocomplete: PropTypes.bool,
|
||||
initialData: PropTypes.shape({}),
|
||||
log: PropTypes.bool,
|
||||
|
||||
@@ -45,7 +45,6 @@ const AddUploadModal = (props) => {
|
||||
action={`${serverURL}${api}/${collection.slug}`}
|
||||
onSuccess={onSuccess}
|
||||
disableSuccessStatus
|
||||
disableScrollOnSuccess
|
||||
>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
|
||||
@@ -29,6 +29,7 @@ const DefaultAccount = (props) => {
|
||||
apiURL,
|
||||
initialState,
|
||||
isLoading,
|
||||
action,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@@ -42,7 +43,7 @@ const DefaultAccount = (props) => {
|
||||
auth,
|
||||
} = collection;
|
||||
|
||||
const { serverURL, routes: { api, admin } } = useConfig();
|
||||
const { routes: { admin } } = useConfig();
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -53,7 +54,7 @@ const DefaultAccount = (props) => {
|
||||
<Form
|
||||
className={`${baseClass}__form`}
|
||||
method="put"
|
||||
action={`${serverURL}${api}/${slug}/${data?.id}`}
|
||||
action={action}
|
||||
initialState={initialState}
|
||||
disabled={!hasSavePermission}
|
||||
>
|
||||
@@ -170,6 +171,7 @@ DefaultAccount.defaultProps = {
|
||||
DefaultAccount.propTypes = {
|
||||
hasSavePermission: PropTypes.bool.isRequired,
|
||||
apiURL: PropTypes.string.isRequired,
|
||||
action: PropTypes.string.isRequired,
|
||||
collection: PropTypes.shape({
|
||||
labels: PropTypes.shape({
|
||||
plural: PropTypes.string,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useConfig } from '../../providers/Config';
|
||||
import { useStepNav } from '../../elements/StepNav';
|
||||
import { useAuth } from '../../providers/Authentication';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import DefaultAccount from './Default';
|
||||
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
@@ -11,6 +12,7 @@ import { NegativeFieldGutterProvider } from '../../forms/FieldTypeGutter/context
|
||||
|
||||
const AccountView = () => {
|
||||
const { state: locationState } = useLocation();
|
||||
const locale = useLocale();
|
||||
const { setStepNav } = useStepNav();
|
||||
const { user, permissions } = useAuth();
|
||||
const [initialState, setInitialState] = useState({});
|
||||
@@ -40,6 +42,8 @@ const AccountView = () => {
|
||||
const dataToRender = locationState?.data || data;
|
||||
const apiURL = `${serverURL}${api}/${user.collection}/${data?.id}`;
|
||||
|
||||
const action = `${serverURL}${api}/${user.collection}/${data?.id}?locale=${locale}&depth=0`;
|
||||
|
||||
useEffect(() => {
|
||||
const nav = [{
|
||||
label: 'Account',
|
||||
@@ -63,6 +67,7 @@ const AccountView = () => {
|
||||
DefaultComponent={DefaultAccount}
|
||||
CustomComponent={CustomAccount}
|
||||
componentProps={{
|
||||
action,
|
||||
data,
|
||||
collection,
|
||||
permissions: collectionPermissions,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useConfig } from '../../providers/Config';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import StatusList, { useStatusList } from '../../elements/Status';
|
||||
import Form from '../../forms/Form';
|
||||
import Email from '../../forms/field-types/Email';
|
||||
import FormSubmit from '../../forms/Submit';
|
||||
@@ -15,7 +15,6 @@ import './index.scss';
|
||||
const baseClass = 'forgot-password';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const { addStatus } = useStatusList();
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
@@ -32,10 +31,7 @@ const ForgotPassword = () => {
|
||||
.then(() => {
|
||||
setHasSubmitted(true);
|
||||
}, () => {
|
||||
addStatus({
|
||||
type: 'error',
|
||||
message: 'The email provided is not valid.',
|
||||
});
|
||||
toast.error('The email provided is not valid.');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -81,7 +77,6 @@ const ForgotPassword = () => {
|
||||
|
||||
return (
|
||||
<MinimalTemplate className={baseClass}>
|
||||
<StatusList />
|
||||
<Form
|
||||
novalidate
|
||||
handleResponse={handleResponse}
|
||||
|
||||
@@ -21,7 +21,6 @@ const DefaultGlobalView = (props) => {
|
||||
} = props;
|
||||
|
||||
const {
|
||||
slug,
|
||||
fields,
|
||||
preview,
|
||||
label,
|
||||
@@ -122,7 +121,6 @@ DefaultGlobalView.defaultProps = {
|
||||
DefaultGlobalView.propTypes = {
|
||||
global: PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
slug: PropTypes.string,
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
preview: PropTypes.func,
|
||||
}).isRequired,
|
||||
|
||||
@@ -91,7 +91,7 @@ const GlobalView = (props) => {
|
||||
global,
|
||||
onSave,
|
||||
apiURL: `${serverURL}${api}/globals/${slug}?depth=0`,
|
||||
action: `${serverURL}${api}/globals/${slug}?locale=${locale}`,
|
||||
action: `${serverURL}${api}/globals/${slug}?locale=${locale}&depth=0`,
|
||||
}}
|
||||
/>
|
||||
</NegativeFieldGutterProvider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toast } from 'react-toastify';
|
||||
import Email from '../../../../forms/field-types/Email';
|
||||
import Password from '../../../../forms/field-types/Password';
|
||||
import Checkbox from '../../../../forms/field-types/Checkbox';
|
||||
@@ -7,7 +8,6 @@ import Button from '../../../../elements/Button';
|
||||
import ConfirmPassword from '../../../../forms/field-types/ConfirmPassword';
|
||||
import { useFormFields, useFormModified } from '../../../../forms/Form/context';
|
||||
import { useConfig } from '../../../../providers/Config';
|
||||
import { useStatusList } from '../../../../elements/Status';
|
||||
|
||||
import APIKey from './APIKey';
|
||||
|
||||
@@ -20,7 +20,6 @@ const Auth = (props) => {
|
||||
const [changingPassword, setChangingPassword] = useState(requirePassword);
|
||||
const { getField } = useFormFields();
|
||||
const modified = useFormModified();
|
||||
const { replaceStatus } = useStatusList();
|
||||
|
||||
const enableAPIKey = getField('enableAPIKey');
|
||||
|
||||
@@ -50,15 +49,9 @@ const Auth = (props) => {
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
replaceStatus([{
|
||||
message: 'Successfully unlocked',
|
||||
type: 'success',
|
||||
}]);
|
||||
toast.success('Successfully unlocked', { autoClose: 3000 });
|
||||
} else {
|
||||
replaceStatus([{
|
||||
message: 'Unable to unlock',
|
||||
type: 'error',
|
||||
}]);
|
||||
toast.error('Successfully unlocked');
|
||||
}
|
||||
}, [replaceStatus, serverURL, api, slug, email]);
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ const DefaultEditView = (props) => {
|
||||
admin: {
|
||||
useAsTitle,
|
||||
disableDuplicate,
|
||||
disableScrollOnSuccess,
|
||||
},
|
||||
timestamps,
|
||||
preview,
|
||||
@@ -68,7 +67,6 @@ const DefaultEditView = (props) => {
|
||||
onSuccess={onSave}
|
||||
disabled={!hasSavePermission}
|
||||
initialState={initialState}
|
||||
disableScrollOnSuccess={disableScrollOnSuccess}
|
||||
>
|
||||
<div className={`${baseClass}__main`}>
|
||||
<Meta
|
||||
@@ -232,7 +230,6 @@ DefaultEditView.propTypes = {
|
||||
admin: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
disableDuplicate: PropTypes.bool,
|
||||
disableScrollOnSuccess: PropTypes.bool,
|
||||
}),
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
preview: PropTypes.func,
|
||||
|
||||
@@ -10,6 +10,7 @@ import RenderCustomComponent from '../../../utilities/RenderCustomComponent';
|
||||
import DefaultEdit from './Default';
|
||||
import buildStateFromSchema from '../../../forms/Form/buildStateFromSchema';
|
||||
import { NegativeFieldGutterProvider } from '../../../forms/FieldTypeGutter/context';
|
||||
import { useLocale } from '../../../utilities/Locale';
|
||||
|
||||
const EditView = (props) => {
|
||||
const { collection, isEditing } = props;
|
||||
@@ -30,6 +31,7 @@ const EditView = (props) => {
|
||||
fields,
|
||||
} = collection;
|
||||
|
||||
const locale = useLocale();
|
||||
const { serverURL, routes: { admin, api } } = useConfig();
|
||||
const { params: { id } = {} } = useRouteMatch();
|
||||
const { state: locationState } = useLocation();
|
||||
@@ -92,11 +94,9 @@ const EditView = (props) => {
|
||||
const collectionPermissions = permissions?.[slug];
|
||||
|
||||
const apiURL = `${serverURL}${api}/${slug}/${id}`;
|
||||
let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}`;
|
||||
const action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}?locale=${locale}&depth=0`;
|
||||
const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission);
|
||||
|
||||
action += '?depth=0';
|
||||
|
||||
return (
|
||||
<NegativeFieldGutterProvider allow>
|
||||
<RenderCustomComponent
|
||||
|
||||
@@ -4,9 +4,9 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { ScrollInfoProvider } from '@faceless-ui/scroll-info';
|
||||
import { WindowInfoProvider } from '@faceless-ui/window-info';
|
||||
import { ModalProvider, ModalContainer } from '@faceless-ui/modal';
|
||||
import { ToastContainer, Slide } from 'react-toastify';
|
||||
import { SearchParamsProvider } from './components/utilities/SearchParams';
|
||||
import { LocaleProvider } from './components/utilities/Locale';
|
||||
import StatusList, { StatusListProvider } from './components/elements/Status';
|
||||
import { AuthenticationProvider } from './components/providers/Authentication';
|
||||
import Routes from './components/Routes';
|
||||
import getCSSVariable from '../utilities/getCSSVariable';
|
||||
@@ -25,30 +25,34 @@ const Index = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<WindowInfoProvider {...windowInfoProps}>
|
||||
<ScrollInfoProvider>
|
||||
<Router>
|
||||
<ModalProvider
|
||||
classPrefix="payload"
|
||||
zIndex={parseInt(getCSSVariable('z-modal'), 10)}
|
||||
>
|
||||
<AuthenticationProvider>
|
||||
<StatusListProvider>
|
||||
<React.Fragment>
|
||||
<ConfigProvider>
|
||||
<WindowInfoProvider {...windowInfoProps}>
|
||||
<ScrollInfoProvider>
|
||||
<Router>
|
||||
<ModalProvider
|
||||
classPrefix="payload"
|
||||
zIndex={parseInt(getCSSVariable('z-modal'), 10)}
|
||||
>
|
||||
<AuthenticationProvider>
|
||||
<SearchParamsProvider>
|
||||
<LocaleProvider>
|
||||
<StatusList />
|
||||
<Routes />
|
||||
</LocaleProvider>
|
||||
</SearchParamsProvider>
|
||||
</StatusListProvider>
|
||||
<ModalContainer />
|
||||
</AuthenticationProvider>
|
||||
</ModalProvider>
|
||||
</Router>
|
||||
</ScrollInfoProvider>
|
||||
</WindowInfoProvider>
|
||||
</ConfigProvider>
|
||||
<ModalContainer />
|
||||
</AuthenticationProvider>
|
||||
</ModalProvider>
|
||||
</Router>
|
||||
</ScrollInfoProvider>
|
||||
</WindowInfoProvider>
|
||||
</ConfigProvider>
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
transition={Slide}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import 'fonts';
|
||||
@import 'styles';
|
||||
@import './toastify.scss';
|
||||
|
||||
:root {
|
||||
--breakpoint-xs-width : #{$breakpoint-xs-width};
|
||||
|
||||
37
src/admin/scss/toastify.scss
Normal file
37
src/admin/scss/toastify.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
@import 'vars';
|
||||
@import '~react-toastify/dist/ReactToastify.css';
|
||||
|
||||
.Toastify {
|
||||
.Toastify__toast-container {
|
||||
left: base(5);
|
||||
transform: none;
|
||||
right: base(5);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.Toastify__toast {
|
||||
padding: base(.5);
|
||||
border-radius: $style-radius-m;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.Toastify__close-button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.Toastify__toast--success {
|
||||
color: $color-dark-gray;
|
||||
}
|
||||
|
||||
.Toastify__close-button--success {
|
||||
color: $color-dark-gray;
|
||||
}
|
||||
|
||||
.Toastify__toast--success {
|
||||
background: $color-green;
|
||||
}
|
||||
|
||||
.Toastify__toast--error {
|
||||
background: $color-red;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,13 @@ const mockHandler = require('./mockHandler');
|
||||
async function buildEmail() {
|
||||
if (!this.config.email.transport || this.config.email.transport === 'mock') {
|
||||
logger.info('E-mail configured with mock configuration');
|
||||
// TODO: Log mock e-mail credentials here as well?
|
||||
const mockAccount = await mockHandler(this.config.email);
|
||||
if (this.config.email.transport === 'mock') {
|
||||
const { account: { web, user, pass } } = mockAccount;
|
||||
logger.info(`Log into mock email provider at ${web}`);
|
||||
logger.info(`Mock email account username: ${user}`);
|
||||
logger.info(`Mock email account password: ${pass}`);
|
||||
}
|
||||
return mockAccount;
|
||||
}
|
||||
|
||||
|
||||
10
src/errors/InvalidConfiguration.js
Normal file
10
src/errors/InvalidConfiguration.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const httpStatus = require('http-status');
|
||||
const APIError = require('./APIError');
|
||||
|
||||
class InvalidConfiguration extends APIError {
|
||||
constructor(message, results) {
|
||||
super(message, httpStatus.INTERNAL_SERVER_ERROR, results);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InvalidConfiguration;
|
||||
@@ -7,6 +7,7 @@ const errorHandler = require('../express/middleware/errorHandler');
|
||||
const FileUploadError = require('./FileUploadError');
|
||||
const Forbidden = require('./Forbidden');
|
||||
const LockedAuth = require('./LockedAuth');
|
||||
const InvalidConfiguration = require('./InvalidConfiguration');
|
||||
const InvalidFieldRelationship = require('./InvalidFieldRelationship');
|
||||
const MissingCollectionLabel = require('./MissingCollectionLabel');
|
||||
const MissingFieldInputOptions = require('./MissingFieldInputOptions');
|
||||
@@ -26,6 +27,7 @@ module.exports = {
|
||||
FileUploadError,
|
||||
Forbidden,
|
||||
LockedAuth,
|
||||
InvalidConfiguration,
|
||||
InvalidFieldRelationship,
|
||||
MissingCollectionLabel,
|
||||
MissingFieldInputOptions,
|
||||
|
||||
@@ -16,10 +16,19 @@ const accessPromise = async ({
|
||||
}) => {
|
||||
const resultingData = data;
|
||||
|
||||
if (field.access && field.access[operation]) {
|
||||
const result = overrideAccess ? true : await field.access[operation]({ req, id });
|
||||
let accessOperation;
|
||||
|
||||
if (!result && operation === 'update' && originalDoc[field.name] !== undefined) {
|
||||
if (hook === 'afterRead') {
|
||||
accessOperation = 'read';
|
||||
} else if (hook === 'beforeValidate') {
|
||||
if (operation === 'update') accessOperation = 'update';
|
||||
if (operation === 'create') accessOperation = 'create';
|
||||
}
|
||||
|
||||
if (field.access && field.access[accessOperation]) {
|
||||
const result = overrideAccess ? true : await field.access[accessOperation]({ req, id });
|
||||
|
||||
if (!result && accessOperation === 'update' && originalDoc[field.name] !== undefined) {
|
||||
resultingData[field.name] = originalDoc[field.name];
|
||||
} else if (!result) {
|
||||
delete resultingData[field.name];
|
||||
|
||||
@@ -23,7 +23,7 @@ const hookPromise = async ({
|
||||
data: fullData,
|
||||
operation,
|
||||
req,
|
||||
});
|
||||
}) || data[field.name];
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
resultingData[field.name] = hookedValue;
|
||||
|
||||
@@ -6,7 +6,7 @@ const validationPromise = async ({
|
||||
field,
|
||||
path,
|
||||
}) => {
|
||||
if (hook === 'beforeValidate') return true;
|
||||
if (hook !== 'beforeChange') return true;
|
||||
|
||||
const hasCondition = field.admin && field.admin.condition;
|
||||
const shouldValidate = field.validate && !hasCondition;
|
||||
|
||||
@@ -35,6 +35,11 @@ const sanitizeConfig = (config) => {
|
||||
if (!sanitizedConfig.admin.user) {
|
||||
sanitizedConfig.admin.user = 'users';
|
||||
sanitizedConfig.collections.push(defaultUser);
|
||||
} else if (!sanitizedConfig.collections
|
||||
.filter((c) => c.auth !== undefined)
|
||||
.map((c) => c.slug)
|
||||
.includes(sanitizedConfig.admin.user)) {
|
||||
throw new InvalidConfiguration(`${sanitizedConfig.admin.user} is not a valid admin user collection`);
|
||||
}
|
||||
|
||||
sanitizedConfig.email = config.email || {};
|
||||
|
||||
Reference in New Issue
Block a user