merges with master
This commit is contained in:
@@ -1,24 +1,9 @@
|
||||
import Cookies from 'universal-cookie';
|
||||
import qs from 'qs';
|
||||
import config from 'payload/config';
|
||||
|
||||
const { cookiePrefix } = config;
|
||||
const cookieTokenName = `${cookiePrefix}-token`;
|
||||
|
||||
export const getJWTHeader = () => {
|
||||
const cookies = new Cookies();
|
||||
const jwt = cookies.get(cookieTokenName);
|
||||
return jwt ? { Authorization: `JWT ${jwt}` } : {};
|
||||
};
|
||||
|
||||
export const requests = {
|
||||
get: (url, params) => {
|
||||
const query = qs.stringify(params, { addQueryPrefix: true, depth: 10 });
|
||||
return fetch(`${url}${query}`, {
|
||||
headers: {
|
||||
...getJWTHeader(),
|
||||
},
|
||||
});
|
||||
return fetch(`${url}${query}`);
|
||||
},
|
||||
|
||||
post: (url, options = {}) => {
|
||||
@@ -29,7 +14,6 @@ export const requests = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
...headers,
|
||||
...getJWTHeader(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,7 +28,6 @@ export const requests = {
|
||||
method: 'put',
|
||||
headers: {
|
||||
...headers,
|
||||
...getJWTHeader(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -58,7 +41,6 @@ export const requests = {
|
||||
method: 'delete',
|
||||
headers: {
|
||||
...headers,
|
||||
...getJWTHeader(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Route, Switch, withRouter, Redirect,
|
||||
Route, Switch, withRouter, Redirect, useHistory,
|
||||
} from 'react-router-dom';
|
||||
import config from 'payload/config';
|
||||
import List from './views/collections/List';
|
||||
@@ -25,17 +25,22 @@ const {
|
||||
} = config;
|
||||
|
||||
const Routes = () => {
|
||||
const history = useHistory();
|
||||
const [initialized, setInitialized] = useState(null);
|
||||
const { user, permissions, permissions: { canAccessAdmin } } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
requests.get(`${routes.api}/${userSlug}/init`).then(res => res.json().then((data) => {
|
||||
requests.get(`${routes.api}/${userSlug}/init`).then((res) => res.json().then((data) => {
|
||||
if (data && 'initialized' in data) {
|
||||
setInitialized(data.initialized);
|
||||
}
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
history.replace();
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<Route
|
||||
path={routes.admin}
|
||||
@@ -62,6 +67,9 @@ const Routes = () => {
|
||||
<Route path={`${match.url}/logout`}>
|
||||
<Logout />
|
||||
</Route>
|
||||
<Route path={`${match.url}/logout-inactivity`}>
|
||||
<Logout inactivity />
|
||||
</Route>
|
||||
<Route path={`${match.url}/forgot`}>
|
||||
<ForgotPassword />
|
||||
</Route>
|
||||
@@ -94,14 +102,12 @@ const Routes = () => {
|
||||
key={`${collection.slug}-list`}
|
||||
path={`${match.url}/collections/${collection.slug}`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<List
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
render={(routeProps) => (
|
||||
<List
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -116,14 +122,12 @@ const Routes = () => {
|
||||
key={`${collection.slug}-create`}
|
||||
path={`${match.url}/collections/${collection.slug}/create`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<Edit
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
render={(routeProps) => (
|
||||
<Edit
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -138,15 +142,13 @@ const Routes = () => {
|
||||
key={`${collection.slug}-edit`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<Edit
|
||||
isEditing
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
render={(routeProps) => (
|
||||
<Edit
|
||||
isEditing
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -161,14 +163,12 @@ const Routes = () => {
|
||||
key={`${global.slug}`}
|
||||
path={`${match.url}/globals/${global.slug}`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<EditGlobal
|
||||
{...routeProps}
|
||||
global={global}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
render={(routeProps) => (
|
||||
<EditGlobal
|
||||
{...routeProps}
|
||||
global={global}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -189,6 +189,10 @@ const Routes = () => {
|
||||
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (user === undefined) {
|
||||
return <Loading />;
|
||||
}
|
||||
return <Redirect to={`${match.url}/login`} />;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -24,9 +24,9 @@ function recursivelyAddFieldComponents(fields) {
|
||||
};
|
||||
}
|
||||
|
||||
if (field.components || field.fields) {
|
||||
if (field.admin.components || field.fields) {
|
||||
const fieldComponents = {
|
||||
...(field.components || {}),
|
||||
...(field.admin.components || {}),
|
||||
};
|
||||
|
||||
if (field.fields) {
|
||||
@@ -56,7 +56,7 @@ function customComponents(config) {
|
||||
|
||||
newComponents[collection.slug] = {
|
||||
fields: recursivelyAddFieldComponents(collection.fields),
|
||||
...(collection.components || {}),
|
||||
...(collection.admin.components || {}),
|
||||
};
|
||||
|
||||
return newComponents;
|
||||
@@ -67,7 +67,7 @@ function customComponents(config) {
|
||||
|
||||
newComponents[global.slug] = {
|
||||
fields: recursivelyAddFieldComponents(global.fields),
|
||||
...(global.components || {}),
|
||||
...(global.admin.components || {}),
|
||||
};
|
||||
|
||||
return newComponents;
|
||||
@@ -76,7 +76,7 @@ function customComponents(config) {
|
||||
const string = stringify({
|
||||
...(allCollectionComponents || {}),
|
||||
...(allGlobalComponents || {}),
|
||||
...(config.components || {}),
|
||||
...(config.admin.components || {}),
|
||||
}).replace(/\\/g, '\\\\');
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, {
|
||||
useState, createContext, useContext, useEffect, useCallback,
|
||||
} from 'react';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import Cookies from 'universal-cookie';
|
||||
import config from 'payload/config';
|
||||
import { useModal } from '@trbl/react-modal';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { requests } from '../../api';
|
||||
import StayLoggedInModal from '../modals/StayLoggedIn';
|
||||
import useDebounce from '../../hooks/useDebounce';
|
||||
|
||||
const {
|
||||
cookiePrefix,
|
||||
admin: {
|
||||
user: userSlug,
|
||||
},
|
||||
@@ -23,16 +21,12 @@ const {
|
||||
},
|
||||
} = config;
|
||||
|
||||
const cookieTokenName = `${cookiePrefix}-token`;
|
||||
|
||||
const cookies = new Cookies();
|
||||
const Context = createContext({});
|
||||
|
||||
const isNotExpired = decodedJWT => (decodedJWT?.exp || 0) > Date.now() / 1000;
|
||||
|
||||
const UserProvider = ({ children }) => {
|
||||
const [token, setToken] = useState('');
|
||||
const [user, setUser] = useState(null);
|
||||
const [user, setUser] = useState(undefined);
|
||||
const [tokenInMemory, setTokenInMemory] = useState(null);
|
||||
const exp = user?.exp;
|
||||
|
||||
const [permissions, setPermissions] = useState({ canAccessAdmin: null });
|
||||
|
||||
@@ -42,64 +36,72 @@ const UserProvider = ({ children }) => {
|
||||
const [lastLocationChange, setLastLocationChange] = useState(0);
|
||||
const debouncedLocationChange = useDebounce(lastLocationChange, 10000);
|
||||
|
||||
const exp = user?.exp || 0;
|
||||
const email = user?.email;
|
||||
|
||||
const refreshToken = useCallback(() => {
|
||||
// Need to retrieve token straight from cookie so as to keep this function
|
||||
// with no dependencies and to make sure we have the exact token that will be used
|
||||
// in the request to the /refresh route
|
||||
const tokenFromCookie = cookies.get(cookieTokenName);
|
||||
const decodedToken = jwt.decode(tokenFromCookie);
|
||||
const refreshCookie = useCallback(() => {
|
||||
const now = Math.round((new Date()).getTime() / 1000);
|
||||
const remainingTime = (exp || 0) - now;
|
||||
|
||||
if (decodedToken?.exp > (Date.now() / 1000)) {
|
||||
if (exp && remainingTime < 120) {
|
||||
setTimeout(async () => {
|
||||
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`);
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json();
|
||||
setToken(json.refreshedToken);
|
||||
setUser(json.user);
|
||||
} else {
|
||||
history.push(`${admin}/logout-inactivity`);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}, [setToken]);
|
||||
}, [setUser, history, exp]);
|
||||
|
||||
const setToken = useCallback((token) => {
|
||||
const decoded = jwt.decode(token);
|
||||
setUser(decoded);
|
||||
setTokenInMemory(token);
|
||||
}, []);
|
||||
|
||||
const logOut = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
cookies.remove(cookieTokenName, { path: '/' });
|
||||
setTokenInMemory(null);
|
||||
requests.get(`${serverURL}${api}/${userSlug}/logout`);
|
||||
};
|
||||
|
||||
// On mount, get cookie and set as token
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
const cookieToken = cookies.get(cookieTokenName);
|
||||
if (cookieToken) setToken(cookieToken);
|
||||
}, []);
|
||||
const fetchMe = async () => {
|
||||
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`);
|
||||
|
||||
// When location changes, refresh token
|
||||
if (request.status === 200) {
|
||||
const json = await request.json();
|
||||
|
||||
setUser(json?.user || null);
|
||||
|
||||
if (json?.token) {
|
||||
setToken(json.token);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchMe();
|
||||
}, [setToken]);
|
||||
|
||||
// When location changes, refresh cookie
|
||||
useEffect(() => {
|
||||
refreshToken();
|
||||
}, [debouncedLocationChange, refreshToken]);
|
||||
if (email) {
|
||||
refreshCookie();
|
||||
}
|
||||
}, [debouncedLocationChange, refreshCookie, email]);
|
||||
|
||||
useEffect(() => {
|
||||
setLastLocationChange(Date.now());
|
||||
}, [pathname]);
|
||||
|
||||
// When token changes, set cookie, decode and set user
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
const decoded = jwt.decode(token);
|
||||
if (isNotExpired(decoded)) {
|
||||
setUser(decoded);
|
||||
cookies.set(cookieTokenName, token, { path: '/' });
|
||||
}
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
// When user changes, get new policies
|
||||
// When user changes, get new access
|
||||
useEffect(() => {
|
||||
async function getPermissions() {
|
||||
const request = await requests.get(`${serverURL}${api}/policies`);
|
||||
const request = await requests.get(`${serverURL}${api}/access`);
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json();
|
||||
@@ -135,7 +137,6 @@ const UserProvider = ({ children }) => {
|
||||
|
||||
if (remainingTime > 0) {
|
||||
forceLogOut = setTimeout(() => {
|
||||
logOut();
|
||||
history.push(`${admin}/logout`);
|
||||
closeAllModals();
|
||||
}, remainingTime * 1000);
|
||||
@@ -149,15 +150,15 @@ const UserProvider = ({ children }) => {
|
||||
return (
|
||||
<Context.Provider value={{
|
||||
user,
|
||||
setToken,
|
||||
logOut,
|
||||
refreshToken,
|
||||
token,
|
||||
refreshCookie,
|
||||
permissions,
|
||||
setToken,
|
||||
token: tokenInMemory,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<StayLoggedInModal refreshToken={refreshToken} />
|
||||
<StayLoggedInModal refreshCookie={refreshCookie} />
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
49
src/client/components/elements/Card/index.js
Normal file
49
src/client/components/elements/Card/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '../Button';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'card';
|
||||
|
||||
const Card = (props) => {
|
||||
const { title, actions, onClick } = props;
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
onClick && `${baseClass}--has-onclick`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<h5>
|
||||
{title}
|
||||
</h5>
|
||||
{actions && (
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{onClick && (
|
||||
<Button
|
||||
className={`${baseClass}__click`}
|
||||
buttonStyle="none"
|
||||
onClick={onClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.defaultProps = {
|
||||
actions: null,
|
||||
onClick: undefined,
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
actions: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Card;
|
||||
41
src/client/components/elements/Card/index.scss
Normal file
41
src/client/components/elements/Card/index.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.card {
|
||||
background: $color-background-gray;
|
||||
padding: base(1.25) $baseline;
|
||||
position: relative;
|
||||
|
||||
h5 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: base(.5);
|
||||
display: inline-flex;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--has-onclick {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: darken($color-background-gray, 3%);
|
||||
}
|
||||
}
|
||||
|
||||
&__click {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
const getInitialColumnState = (fields, useAsTitle, defaultColumns) => {
|
||||
let initialColumns = [];
|
||||
|
||||
const hasThumbnail = fields.find(field => field.type === 'thumbnail');
|
||||
const hasThumbnail = fields.find((field) => field.type === 'thumbnail');
|
||||
|
||||
if (Array.isArray(defaultColumns)) {
|
||||
initialColumns = defaultColumns;
|
||||
if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) {
|
||||
return {
|
||||
columns: defaultColumns,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasThumbnail) {
|
||||
@@ -15,11 +17,8 @@ const getInitialColumnState = (fields, useAsTitle, defaultColumns) => {
|
||||
initialColumns.push(useAsTitle);
|
||||
}
|
||||
|
||||
const remainingColumns = fields.filter((field) => {
|
||||
return field.name !== useAsTitle && field.type !== 'thumbnail';
|
||||
}).slice(0, 3 - initialColumns.length).map((field) => {
|
||||
return field.name;
|
||||
});
|
||||
const remainingColumns = fields.filter((field) => field.name !== useAsTitle && field.type !== 'thumbnail')
|
||||
.slice(0, 3 - initialColumns.length).map((field) => field.name);
|
||||
|
||||
initialColumns = initialColumns.concat(remainingColumns);
|
||||
|
||||
|
||||
@@ -23,17 +23,19 @@ const reducer = (state, { type, payload }) => {
|
||||
];
|
||||
}
|
||||
|
||||
return state.filter(remainingColumn => remainingColumn !== payload);
|
||||
return state.filter((remainingColumn) => remainingColumn !== payload);
|
||||
};
|
||||
|
||||
const ColumnSelector = (props) => {
|
||||
const {
|
||||
collection: {
|
||||
fields,
|
||||
useAsTitle,
|
||||
admin: {
|
||||
useAsTitle,
|
||||
defaultColumns,
|
||||
},
|
||||
},
|
||||
handleChange,
|
||||
defaultColumns,
|
||||
} = props;
|
||||
|
||||
const [initialColumns, setInitialColumns] = useState([]);
|
||||
@@ -55,7 +57,7 @@ const ColumnSelector = (props) => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{fields && fields.map((field, i) => {
|
||||
const isEnabled = columns.find(column => column === field.name);
|
||||
const isEnabled = columns.find((column) => column === field.name);
|
||||
return (
|
||||
<Pill
|
||||
onClick={() => dispatchColumns({ payload: field.name, type: isEnabled ? 'disable' : 'enable' })}
|
||||
@@ -73,20 +75,18 @@ const ColumnSelector = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
ColumnSelector.defaultProps = {
|
||||
defaultColumns: undefined,
|
||||
};
|
||||
|
||||
ColumnSelector.propTypes = {
|
||||
collection: PropTypes.shape({
|
||||
fields: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
useAsTitle: PropTypes.string,
|
||||
admin: PropTypes.shape({
|
||||
defaultColumns: PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
),
|
||||
useAsTitle: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
defaultColumns: PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
),
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import config from 'payload/config';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Modal, useModal } from '@trbl/react-modal';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import Button from '../Button';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import useTitle from '../../../hooks/useTitle';
|
||||
@@ -20,7 +20,9 @@ const DeleteDocument = (props) => {
|
||||
title: titleFromProps,
|
||||
id,
|
||||
collection: {
|
||||
useAsTitle,
|
||||
admin: {
|
||||
useAsTitle,
|
||||
},
|
||||
slug,
|
||||
labels: {
|
||||
singular,
|
||||
@@ -80,7 +82,7 @@ const DeleteDocument = (props) => {
|
||||
|
||||
if (id) {
|
||||
return (
|
||||
<>
|
||||
<React.Fragment>
|
||||
<button
|
||||
type="button"
|
||||
slug={modalSlug}
|
||||
@@ -127,7 +129,7 @@ const DeleteDocument = (props) => {
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,7 +143,9 @@ DeleteDocument.defaultProps = {
|
||||
|
||||
DeleteDocument.propTypes = {
|
||||
collection: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
admin: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
}),
|
||||
slug: PropTypes.string,
|
||||
labels: PropTypes.shape({
|
||||
singular: PropTypes.string,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import config from 'payload/config';
|
||||
import useForm from '../../forms/Form/useForm';
|
||||
import Button from '../Button';
|
||||
import { useForm } from '../../forms/Form/context';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -11,21 +12,28 @@ const { routes: { admin } } = config;
|
||||
const baseClass = 'duplicate';
|
||||
|
||||
const Duplicate = ({ slug }) => {
|
||||
const { push } = useHistory();
|
||||
const { getData } = useForm();
|
||||
const data = getData();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
const data = getData();
|
||||
|
||||
push({
|
||||
pathname: `${admin}/collections/${slug}/create`,
|
||||
state: {
|
||||
data,
|
||||
},
|
||||
});
|
||||
}, [push, getData, slug]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
className={baseClass}
|
||||
to={{
|
||||
pathname: `${admin}/collections/${slug}/create`,
|
||||
state: {
|
||||
data,
|
||||
},
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Duplicate
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
&__main-detail {
|
||||
border-top: $style-stroke-width-m solid white;
|
||||
order: 3;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@ const ListControls = (props) => {
|
||||
const {
|
||||
handleChange,
|
||||
collection,
|
||||
enableColumns,
|
||||
collection: {
|
||||
fields,
|
||||
useAsTitle,
|
||||
defaultColumns,
|
||||
admin: {
|
||||
useAsTitle,
|
||||
defaultColumns,
|
||||
},
|
||||
},
|
||||
} = props;
|
||||
|
||||
@@ -29,7 +32,7 @@ const ListControls = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (useAsTitle) {
|
||||
const foundTitleField = fields.find(field => field.name === useAsTitle);
|
||||
const foundTitleField = fields.find((field) => field.name === useAsTitle);
|
||||
|
||||
if (foundTitleField) {
|
||||
setTitleField(foundTitleField);
|
||||
@@ -71,35 +74,43 @@ const ListControls = (props) => {
|
||||
fieldName={titleField ? titleField.name : undefined}
|
||||
fieldLabel={titleField ? titleField.label : undefined}
|
||||
/>
|
||||
<Button
|
||||
className={`${baseClass}__toggle-columns`}
|
||||
buttonStyle={visibleDrawer === 'columns' ? undefined : 'secondary'}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : false)}
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
Columns
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__toggle-where`}
|
||||
buttonStyle={visibleDrawer === 'where' ? undefined : 'secondary'}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : false)}
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
<div className={`${baseClass}__buttons`}>
|
||||
<div className={`${baseClass}__buttons-wrap`}>
|
||||
{enableColumns && (
|
||||
<Button
|
||||
className={`${baseClass}__toggle-columns`}
|
||||
buttonStyle={visibleDrawer === 'columns' ? undefined : 'secondary'}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : false)}
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
Columns
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={`${baseClass}__toggle-where`}
|
||||
buttonStyle={visibleDrawer === 'where' ? undefined : 'secondary'}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : false)}
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__columns`}
|
||||
height={visibleDrawer === 'columns' ? 'auto' : 0}
|
||||
>
|
||||
<ColumnSelector
|
||||
collection={collection}
|
||||
defaultColumns={defaultColumns}
|
||||
handleChange={setColumns}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
{enableColumns && (
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__columns`}
|
||||
height={visibleDrawer === 'columns' ? 'auto' : 0}
|
||||
>
|
||||
<ColumnSelector
|
||||
collection={collection}
|
||||
defaultColumns={defaultColumns}
|
||||
handleChange={setColumns}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
)}
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__where`}
|
||||
height={visibleDrawer === 'where' ? 'auto' : 0}
|
||||
@@ -113,13 +124,20 @@ const ListControls = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
ListControls.defaultProps = {
|
||||
enableColumns: true,
|
||||
};
|
||||
|
||||
ListControls.propTypes = {
|
||||
enableColumns: PropTypes.bool,
|
||||
collection: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
admin: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
defaultColumns: PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
),
|
||||
}),
|
||||
fields: PropTypes.arrayOf(PropTypes.shape),
|
||||
defaultColumns: PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
),
|
||||
}).isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -15,9 +15,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin-left: $baseline;
|
||||
}
|
||||
|
||||
&__buttons-wrap {
|
||||
display: flex;
|
||||
margin-left: - base(.5);
|
||||
margin-right: - base(.5);
|
||||
width: calc(100% + #{$baseline});
|
||||
}
|
||||
|
||||
&__toggle-columns,
|
||||
&__toggle-where {
|
||||
margin: 0 0 0 $baseline;
|
||||
margin: 0 base(.5);
|
||||
min-width: 140px;
|
||||
|
||||
&.btn--style-primary {
|
||||
@@ -33,9 +44,8 @@
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__toggle-columns,
|
||||
&__toggle-where {
|
||||
margin: 0 0 0 base(.5);
|
||||
&__buttons {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,17 +59,17 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__toggle-columns,
|
||||
&__toggle-where {
|
||||
width: calc(50% - #{base(.25)});
|
||||
}
|
||||
|
||||
&__toggle-columns {
|
||||
margin: 0 base(.25) 0 0;
|
||||
}
|
||||
|
||||
&__toggle-where {
|
||||
margin: 0 0 0 base(.25);
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { NavLink, Link } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { NavLink, Link, useHistory } from 'react-router-dom';
|
||||
import config from 'payload/config';
|
||||
import { useUser } from '../../data/User';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
@@ -25,12 +25,17 @@ const {
|
||||
const Nav = () => {
|
||||
const { permissions } = useUser();
|
||||
const [menuActive, setMenuActive] = useState(false);
|
||||
const history = useHistory();
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
menuActive && `${baseClass}--menu-active`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
useEffect(() => history.listen(() => {
|
||||
setMenuActive(false);
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<aside className={classes}>
|
||||
<div className={`${baseClass}__scroll`}>
|
||||
@@ -76,27 +81,31 @@ const Nav = () => {
|
||||
return null;
|
||||
})}
|
||||
</nav>
|
||||
<span className={`${baseClass}__label`}>Globals</span>
|
||||
<nav>
|
||||
{globals && globals.map((global, i) => {
|
||||
const href = `${admin}/globals/${global.slug}`;
|
||||
{(globals && globals.length > 0) && (
|
||||
<React.Fragment>
|
||||
<span className={`${baseClass}__label`}>Globals</span>
|
||||
<nav>
|
||||
{globals.map((global, i) => {
|
||||
const href = `${admin}/globals/${global.slug}`;
|
||||
|
||||
if (permissions?.[global.slug].read.permission) {
|
||||
return (
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
key={i}
|
||||
to={href}
|
||||
>
|
||||
<Chevron />
|
||||
{global.label}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
if (permissions?.[global.slug].read.permission) {
|
||||
return (
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
key={i}
|
||||
to={href}
|
||||
>
|
||||
<Chevron />
|
||||
{global.label}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</nav>
|
||||
return null;
|
||||
})}
|
||||
</nav>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Localizer />
|
||||
<Link
|
||||
|
||||
@@ -159,5 +159,11 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
nav a {
|
||||
font-size: base(.875);
|
||||
line-height: base(1.25);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
outline: 0;
|
||||
padding: base(.5);
|
||||
color: $color-dark-gray;
|
||||
line-height: 1;
|
||||
line-height: base(1);
|
||||
|
||||
&:hover:not(.clickable-arrow--is-disabled) {
|
||||
background: $color-background-gray;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useWindowInfo } from '@trbl/react-window-info';
|
||||
import { useScrollInfo } from '@trbl/react-scroll-info';
|
||||
import { useWindowInfo } from '@faceless-ui/window-info';
|
||||
import { useScrollInfo } from '@faceless-ui/scroll-info';
|
||||
|
||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||
import PopupButton from './PopupButton';
|
||||
@@ -115,8 +115,7 @@ const Popup = (props) => {
|
||||
setActive={setActive}
|
||||
active={active}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useForm from '../../forms/Form/useForm';
|
||||
import { useForm } from '../../forms/Form/context';
|
||||
import { useUser } from '../../data/User';
|
||||
import Button from '../Button';
|
||||
|
||||
@@ -11,9 +11,9 @@ const PreviewButton = ({ generatePreviewURL }) => {
|
||||
const { getFields } = useForm();
|
||||
const fields = getFields();
|
||||
|
||||
const previewURL = (generatePreviewURL && typeof generatePreviewURL === 'function') ? generatePreviewURL(fields, token) : null;
|
||||
if (generatePreviewURL && typeof generatePreviewURL === 'function') {
|
||||
const previewURL = generatePreviewURL(fields, token);
|
||||
|
||||
if (previewURL) {
|
||||
return (
|
||||
<Button
|
||||
el="anchor"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useTitle from '../../../hooks/useTitle';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'render-title';
|
||||
|
||||
const RenderTitle = (props) => {
|
||||
const {
|
||||
useAsTitle, title: titleFromProps, data, fallback,
|
||||
@@ -12,11 +16,27 @@ const RenderTitle = (props) => {
|
||||
|
||||
let title = titleFromData;
|
||||
if (!title) title = titleFromForm;
|
||||
if (!title) title = data.id;
|
||||
if (!title) title = data?.id;
|
||||
if (!title) title = fallback;
|
||||
title = titleFromProps || title;
|
||||
|
||||
return <>{title}</>;
|
||||
const idAsTitle = title === data?.id;
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
idAsTitle && `${baseClass}--id-as-title`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<span className={classes}>
|
||||
{idAsTitle && (
|
||||
<Fragment>
|
||||
ID:
|
||||
</Fragment>
|
||||
)}
|
||||
{title}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
RenderTitle.defaultProps = {
|
||||
|
||||
12
src/client/components/elements/RenderTitle/index.scss
Normal file
12
src/client/components/elements/RenderTitle/index.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.render-title {
|
||||
&--id-as-title {
|
||||
font-size: base(.75);
|
||||
font-weight: normal;
|
||||
color: $color-gray;
|
||||
background: $color-background-gray;
|
||||
padding: base(.25) base(.5);
|
||||
border-radius: $style-radius-m;
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ 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 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 }), []);
|
||||
const replaceStatus = useCallback((status) => dispatchStatus({ type: 'REPLACE', payload: status }), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (state && state.status) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import Button from '../../../elements/Button';
|
||||
import Popup from '../../../elements/Popup';
|
||||
import BlockSelector from '../../field-types/Flexible/BlockSelector';
|
||||
import BlockSelector from '../../field-types/Blocks/BlockSelector';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -52,7 +52,7 @@ const ActionPanel = (props) => {
|
||||
{singularLabel}
|
||||
</Popup>
|
||||
|
||||
{blockType === 'flexible'
|
||||
{blockType === 'blocks'
|
||||
? (
|
||||
<Popup
|
||||
buttonType="custom"
|
||||
@@ -102,8 +102,7 @@ const ActionPanel = (props) => {
|
||||
Add
|
||||
{singularLabel}
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,13 +114,17 @@ ActionPanel.defaultProps = {
|
||||
verticalAlignment: 'center',
|
||||
blockType: null,
|
||||
isHovered: false,
|
||||
blocks: [],
|
||||
};
|
||||
|
||||
ActionPanel.propTypes = {
|
||||
singularLabel: PropTypes.string,
|
||||
addRow: PropTypes.func.isRequired,
|
||||
removeRow: PropTypes.func.isRequired,
|
||||
blockType: PropTypes.oneOf(['flexible', 'repeater']),
|
||||
blockType: PropTypes.oneOf(['blocks', 'array']),
|
||||
blocks: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
verticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
|
||||
isHovered: PropTypes.bool,
|
||||
rowIndex: PropTypes.number.isRequired,
|
||||
|
||||
@@ -61,7 +61,7 @@ $controls-top-adjustment: base(.1);
|
||||
|
||||
|
||||
// External scopes
|
||||
.field-type.flexible {
|
||||
.field-type.blocks {
|
||||
.position-panel {
|
||||
&__controls-container {
|
||||
min-height: calc(100% + #{$controls-top-adjustment});
|
||||
|
||||
@@ -8,7 +8,7 @@ import './index.scss';
|
||||
const baseClass = 'editable-block-title';
|
||||
|
||||
const EditableBlockTitle = (props) => {
|
||||
const { path, initialData } = props;
|
||||
const { path } = props;
|
||||
const inputRef = useRef(null);
|
||||
const inputCloneRef = useRef(null);
|
||||
const [inputWidth, setInputWidth] = useState(0);
|
||||
@@ -18,7 +18,6 @@ const EditableBlockTitle = (props) => {
|
||||
setValue,
|
||||
} = useFieldType({
|
||||
path,
|
||||
initialData,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,7 +30,7 @@ const EditableBlockTitle = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<React.Fragment>
|
||||
<div className={baseClass}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -53,17 +52,12 @@ const EditableBlockTitle = (props) => {
|
||||
>
|
||||
{value || 'Untitled'}
|
||||
</span>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
EditableBlockTitle.defaultProps = {
|
||||
initialData: undefined,
|
||||
};
|
||||
|
||||
EditableBlockTitle.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
initialData: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EditableBlockTitle;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
@@ -22,17 +22,16 @@ const DraggableSection = (props) => {
|
||||
rowCount,
|
||||
parentPath,
|
||||
fieldSchema,
|
||||
initialData,
|
||||
singularLabel,
|
||||
blockType,
|
||||
fieldTypes,
|
||||
customComponentsPath,
|
||||
isOpen,
|
||||
toggleRowCollapse,
|
||||
id,
|
||||
positionPanelVerticalAlignment,
|
||||
actionPanelVerticalAlignment,
|
||||
toggleRowCollapse,
|
||||
permissions,
|
||||
isOpen,
|
||||
} = props;
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
@@ -48,79 +47,73 @@ const DraggableSection = (props) => {
|
||||
draggableId={id}
|
||||
index={rowIndex}
|
||||
>
|
||||
{(providedDrag) => {
|
||||
return (
|
||||
<div
|
||||
ref={providedDrag.innerRef}
|
||||
className={classes}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onMouseOver={() => setIsHovered(true)}
|
||||
onFocus={() => setIsHovered(true)}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
{(providedDrag) => (
|
||||
<div
|
||||
ref={providedDrag.innerRef}
|
||||
className={classes}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onMouseOver={() => setIsHovered(true)}
|
||||
onFocus={() => setIsHovered(true)}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
|
||||
<div className={`${baseClass}__content-wrapper`}>
|
||||
<PositionPanel
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
moveRow={moveRow}
|
||||
rowCount={rowCount}
|
||||
positionIndex={rowIndex}
|
||||
verticalAlignment={positionPanelVerticalAlignment}
|
||||
/>
|
||||
<div className={`${baseClass}__content-wrapper`}>
|
||||
<PositionPanel
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
moveRow={moveRow}
|
||||
rowCount={rowCount}
|
||||
positionIndex={rowIndex}
|
||||
verticalAlignment={positionPanelVerticalAlignment}
|
||||
/>
|
||||
|
||||
<div className={`${baseClass}__render-fields-wrapper`}>
|
||||
<div className={`${baseClass}__render-fields-wrapper`}>
|
||||
|
||||
{blockType === 'flexible' && (
|
||||
<div className={`${baseClass}__section-header`}>
|
||||
<SectionTitle
|
||||
label={singularLabel}
|
||||
initialData={initialData?.blockName}
|
||||
path={`${parentPath}.${rowIndex}.blockName`}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon="chevron"
|
||||
onClick={toggleRowCollapse}
|
||||
buttonStyle="icon-label"
|
||||
className={`toggle-collapse toggle-collapse--is-${isOpen ? 'open' : 'closed'}`}
|
||||
round
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimateHeight
|
||||
height={isOpen ? 'auto' : 0}
|
||||
duration={0}
|
||||
>
|
||||
<RenderFields
|
||||
initialData={initialData}
|
||||
customComponentsPath={customComponentsPath}
|
||||
fieldTypes={fieldTypes}
|
||||
key={rowIndex}
|
||||
permissions={permissions}
|
||||
fieldSchema={fieldSchema.map((field) => {
|
||||
return ({
|
||||
...field,
|
||||
path: `${parentPath}.${rowIndex}${field.name ? `.${field.name}` : ''}`,
|
||||
});
|
||||
})}
|
||||
{blockType === 'blocks' && (
|
||||
<div className={`${baseClass}__section-header`}>
|
||||
<SectionTitle
|
||||
label={singularLabel}
|
||||
path={`${parentPath}.${rowIndex}.blockName`}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
|
||||
<ActionPanel
|
||||
rowIndex={rowIndex}
|
||||
addRow={addRow}
|
||||
removeRow={removeRow}
|
||||
singularLabel={singularLabel}
|
||||
verticalAlignment={actionPanelVerticalAlignment}
|
||||
isHovered={isHovered}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
icon="chevron"
|
||||
onClick={toggleRowCollapse}
|
||||
buttonStyle="icon-label"
|
||||
className={`toggle-collapse toggle-collapse--is-${isOpen ? 'open' : 'closed'}`}
|
||||
round
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimateHeight
|
||||
height={isOpen ? 'auto' : 0}
|
||||
duration={0}
|
||||
>
|
||||
<RenderFields
|
||||
customComponentsPath={customComponentsPath}
|
||||
fieldTypes={fieldTypes}
|
||||
key={rowIndex}
|
||||
permissions={permissions}
|
||||
fieldSchema={fieldSchema.map((field) => ({
|
||||
...field,
|
||||
path: `${parentPath}.${rowIndex}${field.name ? `.${field.name}` : ''}`,
|
||||
}))}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
|
||||
<ActionPanel
|
||||
rowIndex={rowIndex}
|
||||
addRow={addRow}
|
||||
removeRow={removeRow}
|
||||
singularLabel={singularLabel}
|
||||
verticalAlignment={actionPanelVerticalAlignment}
|
||||
isHovered={isHovered}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// HELPER MIXINS
|
||||
//////////////////////
|
||||
|
||||
@mixin realtively-position-panels {
|
||||
@mixin relatively-position-panels {
|
||||
.position-panel {
|
||||
position: relative;
|
||||
right: 0;
|
||||
@@ -117,7 +117,7 @@
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
@include realtively-position-panels();
|
||||
@include relatively-position-panels();
|
||||
|
||||
.position-panel__move-forward,
|
||||
.position-panel__move-backward {
|
||||
@@ -130,21 +130,25 @@
|
||||
// EXTERNAL SCOPES
|
||||
//////////////////////
|
||||
|
||||
.global-edit,
|
||||
.collection-edit {
|
||||
@include absolutely-position-panels();
|
||||
|
||||
@include mid-break {
|
||||
@include realtively-position-panels();
|
||||
@include relatively-position-panels();
|
||||
}
|
||||
}
|
||||
|
||||
.field-type.repeater .field-type.repeater {
|
||||
@include realtively-position-panels();
|
||||
.field-type.blocks .field-type.array,
|
||||
.field-type.array .field-type.array,
|
||||
.field-type.array .field-type.blocks,
|
||||
.field-type.blocks .field-type.blocks {
|
||||
@include relatively-position-panels();
|
||||
}
|
||||
|
||||
// remove padding above repeater rows to level
|
||||
// remove padding above array rows to level
|
||||
// the line with the top of the input label
|
||||
.field-type.repeater {
|
||||
.field-type.array {
|
||||
.draggable-section {
|
||||
&__content-wrapper {
|
||||
padding-top: 0;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export default createContext({});
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export default createContext({});
|
||||
101
src/client/components/forms/Form/buildStateFromSchema.js
Normal file
101
src/client/components/forms/Form/buildStateFromSchema.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const buildValidationPromise = async (fieldState, field) => {
|
||||
const validatedFieldState = fieldState;
|
||||
|
||||
validatedFieldState.valid = typeof field.validate === 'function' ? await field.validate(fieldState.value, field) : true;
|
||||
|
||||
if (typeof validatedFieldState.valid === 'string') {
|
||||
validatedFieldState.errorMessage = validatedFieldState.valid;
|
||||
validatedFieldState.valid = false;
|
||||
}
|
||||
};
|
||||
|
||||
const buildStateFromSchema = async (fieldSchema, fullData) => {
|
||||
if (fieldSchema && fullData) {
|
||||
const validationPromises = [];
|
||||
|
||||
const structureFieldState = (field, data = {}) => {
|
||||
const value = data[field.name] || field.defaultValue;
|
||||
|
||||
const fieldState = {
|
||||
value,
|
||||
initialValue: value,
|
||||
};
|
||||
|
||||
validationPromises.push(buildValidationPromise(fieldState, field));
|
||||
|
||||
return fieldState;
|
||||
};
|
||||
|
||||
const iterateFields = (fields, data, path = '') => fields.reduce((state, field) => {
|
||||
if (field.name && data[field.name]) {
|
||||
if (Array.isArray(data[field.name])) {
|
||||
if (field.type === 'array') {
|
||||
return {
|
||||
...state,
|
||||
...data[field.name].reduce((rowState, row, i) => ({
|
||||
...rowState,
|
||||
...iterateFields(field.fields, row, `${path}${field.name}.${i}.`),
|
||||
}), {}),
|
||||
};
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
return {
|
||||
...state,
|
||||
...data[field.name].reduce((rowState, row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
|
||||
const rowPath = `${path}${field.name}.${i}.`;
|
||||
|
||||
return {
|
||||
...rowState,
|
||||
[`${rowPath}blockType`]: {
|
||||
value: row.blockType,
|
||||
initialValue: row.blockType,
|
||||
valid: true,
|
||||
},
|
||||
[`${rowPath}blockName`]: {
|
||||
value: row.blockName,
|
||||
initialValue: row.blockName,
|
||||
valid: true,
|
||||
},
|
||||
...iterateFields(block.fields, row, rowPath),
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (field.fields) {
|
||||
return {
|
||||
...state,
|
||||
...iterateFields(field.fields, data[field.name], `${path}${field.name}.`),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...state,
|
||||
[`${path}${field.name}`]: structureFieldState(field, data),
|
||||
};
|
||||
}
|
||||
|
||||
if (field.fields) {
|
||||
return {
|
||||
...state,
|
||||
...iterateFields(field.fields, data, path),
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}, {});
|
||||
|
||||
const resultingState = iterateFields(fieldSchema, fullData);
|
||||
await Promise.all(validationPromises);
|
||||
return resultingState;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
|
||||
module.exports = buildStateFromSchema;
|
||||
26
src/client/components/forms/Form/context.js
Normal file
26
src/client/components/forms/Form/context.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const FormContext = createContext({});
|
||||
const FieldContext = createContext({});
|
||||
const SubmittedContext = createContext(false);
|
||||
const ProcessingContext = createContext(false);
|
||||
const ModifiedContext = createContext(false);
|
||||
|
||||
const useForm = () => useContext(FormContext);
|
||||
const useFormFields = () => useContext(FieldContext);
|
||||
const useFormSubmitted = () => useContext(SubmittedContext);
|
||||
const useFormProcessing = () => useContext(ProcessingContext);
|
||||
const useFormModified = () => useContext(ModifiedContext);
|
||||
|
||||
export {
|
||||
FormContext,
|
||||
FieldContext,
|
||||
SubmittedContext,
|
||||
ProcessingContext,
|
||||
ModifiedContext,
|
||||
useForm,
|
||||
useFormFields,
|
||||
useFormSubmitted,
|
||||
useFormProcessing,
|
||||
useFormModified,
|
||||
};
|
||||
@@ -1,9 +1,43 @@
|
||||
import { unflatten, flatten } from 'flatley';
|
||||
import flattenFilters from './flattenFilters';
|
||||
|
||||
//
|
||||
const unflattenRowsFromState = (state, path) => {
|
||||
// Take a copy of state
|
||||
const remainingFlattenedState = { ...state };
|
||||
|
||||
const rowsFromStateObject = {};
|
||||
|
||||
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
|
||||
|
||||
// Loop over all keys from state
|
||||
// If the key begins with the name of the parent field,
|
||||
// Add value to rowsFromStateObject and delete it from remaining state
|
||||
Object.keys(state).forEach((key) => {
|
||||
if (key.indexOf(`${path}.`) === 0) {
|
||||
if (!state[key].ignoreWhileFlattening) {
|
||||
const name = key.replace(pathPrefixToRemove, '');
|
||||
rowsFromStateObject[name] = state[key];
|
||||
rowsFromStateObject[name].initialValue = rowsFromStateObject[name].value;
|
||||
}
|
||||
|
||||
delete remainingFlattenedState[key];
|
||||
}
|
||||
});
|
||||
|
||||
const unflattenedRows = unflatten(rowsFromStateObject);
|
||||
|
||||
return {
|
||||
unflattenedRows: unflattenedRows[path.replace(pathPrefixToRemove, '')] || [],
|
||||
remainingFlattenedState,
|
||||
};
|
||||
};
|
||||
|
||||
function fieldReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'REPLACE_ALL':
|
||||
return {
|
||||
...action.value,
|
||||
};
|
||||
case 'REPLACE_STATE': {
|
||||
return action.state;
|
||||
}
|
||||
|
||||
case 'REMOVE': {
|
||||
const newState = { ...state };
|
||||
@@ -11,15 +45,112 @@ function fieldReducer(state, action) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'REMOVE_ROW': {
|
||||
const { rowIndex, path } = action;
|
||||
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
|
||||
|
||||
unflattenedRows.splice(rowIndex, 1);
|
||||
|
||||
const flattenedRowState = unflattenedRows.length > 0 ? flatten({ [path]: unflattenedRows }, { filters: flattenFilters }) : {};
|
||||
|
||||
return {
|
||||
...remainingFlattenedState,
|
||||
...flattenedRowState,
|
||||
};
|
||||
}
|
||||
|
||||
case 'ADD_ROW': {
|
||||
const {
|
||||
rowIndex, path, fieldSchema, blockType,
|
||||
} = action;
|
||||
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
|
||||
|
||||
// Get paths of sub fields
|
||||
const subFields = fieldSchema.reduce((acc, field) => {
|
||||
if (field.type === 'flexible' || field.type === 'repeater') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (field.name) {
|
||||
return {
|
||||
...acc,
|
||||
[field.name]: {
|
||||
value: null,
|
||||
valid: !field.required,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (field.fields) {
|
||||
return {
|
||||
...acc,
|
||||
...(field.fields.reduce((fields, subField) => ({
|
||||
...fields,
|
||||
[subField.name]: {
|
||||
value: null,
|
||||
valid: !field.required,
|
||||
},
|
||||
}), {})),
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (blockType) {
|
||||
subFields.blockType = {
|
||||
value: blockType,
|
||||
initialValue: blockType,
|
||||
valid: true,
|
||||
};
|
||||
|
||||
subFields.blockName = {
|
||||
value: null,
|
||||
initialValue: null,
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Add new object containing subfield names to unflattenedRows array
|
||||
unflattenedRows.splice(rowIndex + 1, 0, subFields);
|
||||
|
||||
const newState = {
|
||||
...remainingFlattenedState,
|
||||
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
|
||||
};
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'MOVE_ROW': {
|
||||
const { moveFromIndex, moveToIndex, path } = action;
|
||||
const { unflattenedRows, remainingFlattenedState } = unflattenRowsFromState(state, path);
|
||||
|
||||
// copy the row to move
|
||||
const copyOfMovingRow = unflattenedRows[moveFromIndex];
|
||||
// delete the row by index
|
||||
unflattenedRows.splice(moveFromIndex, 1);
|
||||
// insert row copyOfMovingRow back in
|
||||
unflattenedRows.splice(moveToIndex, 0, copyOfMovingRow);
|
||||
|
||||
const newState = {
|
||||
...remainingFlattenedState,
|
||||
...(flatten({ [path]: unflattenedRows }, { filters: flattenFilters })),
|
||||
};
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
default: {
|
||||
const newField = {
|
||||
value: action.value,
|
||||
valid: action.valid,
|
||||
errorMessage: action.errorMessage,
|
||||
disableFormData: action.disableFormData,
|
||||
ignoreWhileFlattening: action.ignoreWhileFlattening,
|
||||
initialValue: action.initialValue,
|
||||
};
|
||||
|
||||
if (action.disableFormData) newField.disableFormData = action.disableFormData;
|
||||
|
||||
return {
|
||||
...state,
|
||||
[action.path]: newField,
|
||||
|
||||
10
src/client/components/forms/Form/flattenFilters.js
Normal file
10
src/client/components/forms/Form/flattenFilters.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const flattenFilters = [{
|
||||
test: (_, value) => {
|
||||
const hasValidProperty = Object.prototype.hasOwnProperty.call(value, 'valid');
|
||||
const hasValueProperty = Object.prototype.hasOwnProperty.call(value, 'value');
|
||||
|
||||
return (hasValidProperty && hasValueProperty);
|
||||
},
|
||||
}];
|
||||
|
||||
export default flattenFilters;
|
||||
@@ -1,13 +1,10 @@
|
||||
import React, {
|
||||
useReducer, useEffect, useRef,
|
||||
useReducer, useEffect, useRef, useState, useCallback,
|
||||
} from 'react';
|
||||
import { objectToFormData } from 'object-to-formdata';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { unflatten } from 'flatley';
|
||||
import HiddenInput from '../field-types/HiddenInput';
|
||||
import FormContext from './FormContext';
|
||||
import FieldContext from './FieldContext';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { useStatusList } from '../../elements/Status';
|
||||
import { requests } from '../../../api';
|
||||
@@ -16,6 +13,8 @@ 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';
|
||||
@@ -40,36 +39,41 @@ const Form = (props) => {
|
||||
const {
|
||||
disabled,
|
||||
onSubmit,
|
||||
ajax,
|
||||
method,
|
||||
action,
|
||||
handleAjaxResponse,
|
||||
handleResponse,
|
||||
onSuccess,
|
||||
children,
|
||||
className,
|
||||
redirect,
|
||||
disableSuccessStatus,
|
||||
initialState,
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
const locale = useLocale();
|
||||
const { replaceStatus, addStatus, clearStatus } = useStatusList();
|
||||
const { refreshToken } = useUser();
|
||||
const { refreshCookie } = useUser();
|
||||
|
||||
const [modified, setModified] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const contextRef = useRef({ ...initContextState });
|
||||
|
||||
contextRef.current.initialState = initialState;
|
||||
|
||||
const [fields, dispatchFields] = useReducer(fieldReducer, {});
|
||||
contextRef.current.fields = fields;
|
||||
contextRef.current.dispatchFields = dispatchFields;
|
||||
|
||||
contextRef.current.submit = (e) => {
|
||||
const submit = useCallback((e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
contextRef.current.setSubmitted(true);
|
||||
setSubmitted(true);
|
||||
|
||||
const isValid = contextRef.current.validateForm();
|
||||
|
||||
@@ -96,116 +100,129 @@ const Form = (props) => {
|
||||
return onSubmit(fields);
|
||||
}
|
||||
|
||||
// If form is AJAX, fetch data
|
||||
if (ajax !== false) {
|
||||
e.preventDefault();
|
||||
e.preventDefault();
|
||||
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
|
||||
const formData = contextRef.current.createFormData();
|
||||
contextRef.current.setProcessing(true);
|
||||
const formData = contextRef.current.createFormData();
|
||||
setProcessing(true);
|
||||
|
||||
// Make the API call from the action
|
||||
return requests[method.toLowerCase()](action, {
|
||||
body: formData,
|
||||
}).then((res) => {
|
||||
contextRef.current.setModified(false);
|
||||
if (typeof handleAjaxResponse === 'function') return handleAjaxResponse(res);
|
||||
// Make the API call from the action
|
||||
return requests[method.toLowerCase()](action, {
|
||||
body: formData,
|
||||
}).then((res) => {
|
||||
setModified(false);
|
||||
if (typeof handleResponse === 'function') return handleResponse(res);
|
||||
|
||||
return res.json().then((json) => {
|
||||
contextRef.current.setProcessing(false);
|
||||
clearStatus();
|
||||
return res.json().then((json) => {
|
||||
setProcessing(false);
|
||||
clearStatus();
|
||||
|
||||
if (res.status < 400) {
|
||||
if (typeof onSuccess === 'function') onSuccess(json);
|
||||
if (res.status < 400) {
|
||||
if (typeof onSuccess === 'function') onSuccess(json);
|
||||
|
||||
if (redirect) {
|
||||
return history.push(redirect, json);
|
||||
}
|
||||
|
||||
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) => {
|
||||
return 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;
|
||||
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: 'An unknown error occurred.',
|
||||
message: json.message,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
return json;
|
||||
});
|
||||
}).catch((err) => {
|
||||
addStatus({
|
||||
message: err,
|
||||
type: 'error',
|
||||
});
|
||||
if (Array.isArray(json.errors)) {
|
||||
const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]);
|
||||
|
||||
fieldErrors.forEach((err) => {
|
||||
dispatchFields({
|
||||
...(contextRef.current?.fields?.[err.field] || {}),
|
||||
valid: false,
|
||||
errorMessage: err.message,
|
||||
path: err.field,
|
||||
});
|
||||
});
|
||||
|
||||
nonFieldErrors.forEach((err) => {
|
||||
addStatus({
|
||||
message: err.message || 'An unknown error occurred.',
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
if (fieldErrors.length > 0 && nonFieldErrors.length === 0) {
|
||||
addStatus({
|
||||
message: 'Please correct the fields below.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
addStatus({
|
||||
message: 'An unknown error occurred.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
addStatus({
|
||||
message: err,
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
}, [
|
||||
action,
|
||||
addStatus,
|
||||
clearStatus,
|
||||
disableSuccessStatus,
|
||||
disabled,
|
||||
fields,
|
||||
handleResponse,
|
||||
history,
|
||||
method,
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
redirect,
|
||||
replaceStatus,
|
||||
]);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
contextRef.current.getFields = () => {
|
||||
return contextRef.current.fields;
|
||||
};
|
||||
const getFields = useCallback(() => contextRef.current.fields, [contextRef]);
|
||||
const getField = useCallback((path) => contextRef.current.fields[path], [contextRef]);
|
||||
const getData = useCallback(() => reduceFieldsToValues(contextRef.current.fields, true), [contextRef]);
|
||||
|
||||
contextRef.current.getField = (path) => {
|
||||
return contextRef.current.fields[path];
|
||||
};
|
||||
|
||||
contextRef.current.getData = () => {
|
||||
return reduceFieldsToValues(contextRef.current.fields, true);
|
||||
};
|
||||
|
||||
contextRef.current.getSiblingData = (path) => {
|
||||
const getSiblingData = useCallback((path) => {
|
||||
let siblingFields = contextRef.current.fields;
|
||||
|
||||
// If this field is nested
|
||||
@@ -225,9 +242,9 @@ const Form = (props) => {
|
||||
}
|
||||
|
||||
return reduceFieldsToValues(siblingFields, true);
|
||||
};
|
||||
}, [contextRef]);
|
||||
|
||||
contextRef.current.getDataByPath = (path) => {
|
||||
const getDataByPath = useCallback((path) => {
|
||||
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1);
|
||||
const name = path.split('.').pop();
|
||||
|
||||
@@ -245,42 +262,42 @@ const Form = (props) => {
|
||||
const values = reduceFieldsToValues(data, true);
|
||||
const unflattenedData = unflatten(values);
|
||||
return unflattenedData?.[name];
|
||||
};
|
||||
}, [contextRef]);
|
||||
|
||||
contextRef.current.getUnflattenedValues = () => {
|
||||
return reduceFieldsToValues(contextRef.current.fields);
|
||||
};
|
||||
const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]);
|
||||
|
||||
contextRef.current.validateForm = () => {
|
||||
return !Object.values(contextRef.current.fields).some((field) => {
|
||||
return field.valid === false;
|
||||
});
|
||||
};
|
||||
const validateForm = useCallback(() => !Object.values(contextRef.current.fields).some((field) => field.valid === false), [contextRef]);
|
||||
|
||||
contextRef.current.createFormData = () => {
|
||||
const createFormData = useCallback(() => {
|
||||
const data = reduceFieldsToValues(contextRef.current.fields);
|
||||
return objectToFormData(data, { indices: true });
|
||||
};
|
||||
}, [contextRef]);
|
||||
|
||||
contextRef.current.setModified = (modified) => {
|
||||
contextRef.current.modified = modified;
|
||||
};
|
||||
contextRef.current.dispatchFields = dispatchFields;
|
||||
contextRef.current.submit = submit;
|
||||
contextRef.current.getFields = getFields;
|
||||
contextRef.current.getField = getField;
|
||||
contextRef.current.getData = getData;
|
||||
contextRef.current.getSiblingData = getSiblingData;
|
||||
contextRef.current.getDataByPath = getDataByPath;
|
||||
contextRef.current.getUnflattenedValues = getUnflattenedValues;
|
||||
contextRef.current.validateForm = validateForm;
|
||||
contextRef.current.createFormData = createFormData;
|
||||
contextRef.current.setModified = setModified;
|
||||
contextRef.current.setProcessing = setProcessing;
|
||||
contextRef.current.setSubmitted = setSubmitted;
|
||||
|
||||
contextRef.current.setSubmitted = (submitted) => {
|
||||
contextRef.current.submitted = submitted;
|
||||
};
|
||||
|
||||
contextRef.current.setProcessing = (processing) => {
|
||||
contextRef.current.processing = processing;
|
||||
};
|
||||
useEffect(() => {
|
||||
dispatchFields({ type: 'REPLACE_STATE', state: initialState });
|
||||
}, [initialState]);
|
||||
|
||||
useThrottledEffect(() => {
|
||||
refreshToken();
|
||||
refreshCookie();
|
||||
}, 15000, [fields]);
|
||||
|
||||
useEffect(() => {
|
||||
contextRef.current.modified = false;
|
||||
}, [locale, contextRef.current.modified]);
|
||||
setModified(false);
|
||||
}, [locale]);
|
||||
|
||||
const classes = [
|
||||
className,
|
||||
@@ -301,13 +318,16 @@ const Form = (props) => {
|
||||
...contextRef.current,
|
||||
}}
|
||||
>
|
||||
<HiddenInput
|
||||
path="locale"
|
||||
defaultValue={locale}
|
||||
/>
|
||||
{children}
|
||||
<SubmittedContext.Provider value={submitted}>
|
||||
<ProcessingContext.Provider value={processing}>
|
||||
<ModifiedContext.Provider value={modified}>
|
||||
{children}
|
||||
</ModifiedContext.Provider>
|
||||
</ProcessingContext.Provider>
|
||||
</SubmittedContext.Provider>
|
||||
</FieldContext.Provider>
|
||||
</FormContext.Provider>
|
||||
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -315,23 +335,22 @@ const Form = (props) => {
|
||||
Form.defaultProps = {
|
||||
redirect: '',
|
||||
onSubmit: null,
|
||||
ajax: true,
|
||||
method: 'POST',
|
||||
action: '',
|
||||
handleAjaxResponse: null,
|
||||
handleResponse: null,
|
||||
onSuccess: null,
|
||||
className: '',
|
||||
disableSuccessStatus: false,
|
||||
disabled: false,
|
||||
initialState: {},
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
disableSuccessStatus: PropTypes.bool,
|
||||
onSubmit: PropTypes.func,
|
||||
ajax: PropTypes.bool,
|
||||
method: PropTypes.oneOf(['post', 'POST', 'get', 'GET', 'put', 'PUT', 'delete', 'DELETE']),
|
||||
action: PropTypes.string,
|
||||
handleAjaxResponse: PropTypes.func,
|
||||
handleResponse: PropTypes.func,
|
||||
onSuccess: PropTypes.func,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
@@ -340,6 +359,7 @@ Form.propTypes = {
|
||||
className: PropTypes.string,
|
||||
redirect: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
initialState: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
export default Form;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
export default {
|
||||
processing: false,
|
||||
modified: false,
|
||||
submitted: false,
|
||||
getFields: () => { },
|
||||
getField: () => { },
|
||||
getData: () => { },
|
||||
@@ -13,4 +10,6 @@ export default {
|
||||
submit: () => { },
|
||||
dispatchFields: () => { },
|
||||
setModified: () => { },
|
||||
initialState: {},
|
||||
reset: 0,
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import FormContext from './FormContext';
|
||||
|
||||
export default () => useContext(FormContext);
|
||||
@@ -1,4 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import FieldContext from './FieldContext';
|
||||
|
||||
export default () => useContext(FieldContext);
|
||||
@@ -6,7 +6,7 @@ slides.1.heroInfo.title
|
||||
fields: [
|
||||
{
|
||||
name: slides,
|
||||
type: repeater,
|
||||
type: array,
|
||||
fields: [
|
||||
{
|
||||
type: group,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import React, { createContext, useEffect, useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import useIntersect from '../../../hooks/useIntersect';
|
||||
|
||||
import './index.scss';
|
||||
const baseClass = 'render-fields';
|
||||
|
||||
const intersectionObserverOptions = {
|
||||
rootMargin: '1000px',
|
||||
};
|
||||
|
||||
const RenderedFieldContext = createContext({});
|
||||
|
||||
@@ -11,86 +16,118 @@ export const useRenderedFields = () => useContext(RenderedFieldContext);
|
||||
const RenderFields = (props) => {
|
||||
const {
|
||||
fieldSchema,
|
||||
initialData,
|
||||
customComponentsPath: customComponentsPathFromProps,
|
||||
fieldTypes,
|
||||
filter,
|
||||
permissions,
|
||||
readOnly: readOnlyOverride,
|
||||
operation: operationFromProps,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const [hasIntersected, setHasIntersected] = useState(false);
|
||||
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions);
|
||||
const isIntersecting = Boolean(entry?.isIntersecting);
|
||||
|
||||
const { customComponentsPath: customComponentsPathFromContext, operation: operationFromContext } = useRenderedFields();
|
||||
|
||||
const customComponentsPath = customComponentsPathFromProps || customComponentsPathFromContext;
|
||||
const operation = operationFromProps || operationFromContext;
|
||||
const customComponentsPath = customComponentsPathFromProps || customComponentsPathFromContext;
|
||||
|
||||
const [contextValue, setContextValue] = useState({
|
||||
operation,
|
||||
customComponentsPath,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setContextValue({
|
||||
operation,
|
||||
customComponentsPath,
|
||||
});
|
||||
}, [operation, customComponentsPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isIntersecting && !hasIntersected) {
|
||||
setHasIntersected(true);
|
||||
}
|
||||
}, [isIntersecting, hasIntersected]);
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
className,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
if (fieldSchema) {
|
||||
return (
|
||||
<RenderedFieldContext.Provider value={{ customComponentsPath, operation }}>
|
||||
{fieldSchema.map((field, i) => {
|
||||
if (field?.hidden !== 'api' && field?.hidden !== true) {
|
||||
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
|
||||
const FieldComponent = field?.hidden === 'admin' ? fieldTypes.hidden : fieldTypes[field.type];
|
||||
<div
|
||||
ref={intersectionRef}
|
||||
className={classes}
|
||||
>
|
||||
{hasIntersected && (
|
||||
<RenderedFieldContext.Provider value={contextValue}>
|
||||
{fieldSchema.map((field, i) => {
|
||||
if (!field?.hidden && field?.admin?.disabled !== true) {
|
||||
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
|
||||
const FieldComponent = field?.admin?.hidden ? fieldTypes.hidden : fieldTypes[field.type];
|
||||
|
||||
let initialFieldData;
|
||||
let fieldPermissions = permissions[field.name];
|
||||
let fieldPermissions = permissions[field.name];
|
||||
|
||||
if (!field.name) {
|
||||
initialFieldData = initialData;
|
||||
fieldPermissions = permissions;
|
||||
} else if (initialData?.[field.name] !== undefined) {
|
||||
initialFieldData = initialData[field.name];
|
||||
}
|
||||
if (!field.name) {
|
||||
fieldPermissions = permissions;
|
||||
}
|
||||
|
||||
let { readOnly } = field;
|
||||
let { admin: { readOnly } = {} } = field;
|
||||
|
||||
if (readOnlyOverride) readOnly = true;
|
||||
if (readOnlyOverride) readOnly = true;
|
||||
|
||||
if (permissions?.[field?.name]?.read?.permission !== false) {
|
||||
if (permissions?.[field?.name]?.[operation]?.permission === false) {
|
||||
readOnly = true;
|
||||
if (permissions?.[field?.name]?.read?.permission !== false) {
|
||||
if (permissions?.[field?.name]?.[operation]?.permission === false) {
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
if (FieldComponent) {
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
key={i}
|
||||
path={`${customComponentsPath}${field.name ? `${field.name}.field` : ''}`}
|
||||
DefaultComponent={FieldComponent}
|
||||
componentProps={{
|
||||
...field,
|
||||
path: field.path || field.name,
|
||||
fieldTypes,
|
||||
admin: {
|
||||
...(field.admin || {}),
|
||||
readOnly,
|
||||
},
|
||||
permissions: fieldPermissions,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="missing-field"
|
||||
key={i}
|
||||
>
|
||||
No matched field found for
|
||||
{' '}
|
||||
"
|
||||
{field.label}
|
||||
"
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (FieldComponent) {
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
key={field.name || `field-${i}`}
|
||||
path={`${customComponentsPath}${field.name ? `${field.name}.field` : ''}`}
|
||||
DefaultComponent={FieldComponent}
|
||||
componentProps={{
|
||||
...field,
|
||||
path: field.path || field.name,
|
||||
fieldTypes,
|
||||
initialData: initialFieldData,
|
||||
readOnly,
|
||||
permissions: fieldPermissions,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="missing-field"
|
||||
key={i}
|
||||
>
|
||||
No matched field found for
|
||||
{' '}
|
||||
"
|
||||
{field.label}
|
||||
"
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</RenderedFieldContext.Provider>
|
||||
return null;
|
||||
})}
|
||||
</RenderedFieldContext.Provider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,19 +135,18 @@ const RenderFields = (props) => {
|
||||
};
|
||||
|
||||
RenderFields.defaultProps = {
|
||||
initialData: {},
|
||||
customComponentsPath: '',
|
||||
filter: null,
|
||||
readOnly: false,
|
||||
permissions: {},
|
||||
operation: undefined,
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
RenderFields.propTypes = {
|
||||
fieldSchema: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
).isRequired,
|
||||
initialData: PropTypes.shape({}),
|
||||
customComponentsPath: PropTypes.string,
|
||||
fieldTypes: PropTypes.shape({
|
||||
hidden: PropTypes.function,
|
||||
@@ -118,6 +154,8 @@ RenderFields.propTypes = {
|
||||
filter: PropTypes.func,
|
||||
permissions: PropTypes.shape({}),
|
||||
readOnly: PropTypes.bool,
|
||||
operation: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RenderFields;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.missing-field {
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FormContext from '../Form/FormContext';
|
||||
import { useFormProcessing } from '../Form/context';
|
||||
import Button from '../../elements/Button';
|
||||
|
||||
import './index.scss';
|
||||
@@ -8,12 +8,13 @@ import './index.scss';
|
||||
const baseClass = 'form-submit';
|
||||
|
||||
const FormSubmit = ({ children }) => {
|
||||
const formContext = useContext(FormContext);
|
||||
const processing = useFormProcessing();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formContext.processing ? true : undefined}
|
||||
disabled={processing ? true : undefined}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
261
src/client/components/forms/field-types/Array/index.js
Normal file
261
src/client/components/forms/field-types/Array/index.js
Normal file
@@ -0,0 +1,261 @@
|
||||
import React, { useEffect, useReducer, useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
import withCondition from '../../withCondition';
|
||||
import Button from '../../../elements/Button';
|
||||
import DraggableSection from '../../DraggableSection';
|
||||
import reducer from '../rowReducer';
|
||||
import { useRenderedFields } from '../../RenderFields';
|
||||
import { useForm } from '../../Form/context';
|
||||
import useFieldType from '../../useFieldType';
|
||||
import Error from '../../Error';
|
||||
import { array } from '../../../../../fields/validations';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'field-type array';
|
||||
|
||||
const ArrayFieldType = (props) => {
|
||||
const {
|
||||
label,
|
||||
name,
|
||||
path: pathFromProps,
|
||||
fields,
|
||||
fieldTypes,
|
||||
validate,
|
||||
required,
|
||||
maxRows,
|
||||
minRows,
|
||||
singularLabel,
|
||||
permissions,
|
||||
} = props;
|
||||
|
||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||
const { customComponentsPath } = useRenderedFields();
|
||||
const { getDataByPath, initialState, dispatchFields } = useForm();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value) => {
|
||||
const validationResult = validate(value, { minRows, maxRows, required });
|
||||
return validationResult;
|
||||
}, [validate, maxRows, minRows, required]);
|
||||
|
||||
const [disableFormData, setDisableFormData] = useState(false);
|
||||
|
||||
const {
|
||||
showError,
|
||||
errorMessage,
|
||||
value,
|
||||
setValue,
|
||||
} = useFieldType({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
disableFormData,
|
||||
ignoreWhileFlattening: true,
|
||||
required,
|
||||
});
|
||||
|
||||
const addRow = useCallback((rowIndex) => {
|
||||
dispatchRows({ type: 'ADD', rowIndex });
|
||||
dispatchFields({ type: 'ADD_ROW', rowIndex, fieldSchema: fields, path });
|
||||
setValue(value + 1);
|
||||
}, [dispatchRows, dispatchFields, fields, path, setValue, value]);
|
||||
|
||||
const removeRow = useCallback((rowIndex) => {
|
||||
dispatchRows({ type: 'REMOVE', rowIndex });
|
||||
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
|
||||
}, [dispatchRows, dispatchFields, path]);
|
||||
|
||||
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
|
||||
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
|
||||
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
||||
}, [dispatchRows, dispatchFields, path]);
|
||||
|
||||
const onDragEnd = useCallback((result) => {
|
||||
if (!result.destination) return;
|
||||
const sourceIndex = result.source.index;
|
||||
const destinationIndex = result.destination.index;
|
||||
moveRow(sourceIndex, destinationIndex);
|
||||
}, [moveRow]);
|
||||
|
||||
useEffect(() => {
|
||||
const data = getDataByPath(path);
|
||||
dispatchRows({ type: 'SET_ALL', data });
|
||||
}, [initialState, getDataByPath, path]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(rows?.length || 0);
|
||||
|
||||
if (rows?.length === 0) {
|
||||
setDisableFormData(false);
|
||||
} else {
|
||||
setDisableFormData(true);
|
||||
}
|
||||
}, [rows, setValue]);
|
||||
|
||||
return (
|
||||
<RenderArray
|
||||
onDragEnd={onDragEnd}
|
||||
label={label}
|
||||
showError={showError}
|
||||
errorMessage={errorMessage}
|
||||
rows={rows}
|
||||
singularLabel={singularLabel}
|
||||
addRow={addRow}
|
||||
removeRow={removeRow}
|
||||
moveRow={moveRow}
|
||||
path={path}
|
||||
customComponentsPath={customComponentsPath}
|
||||
name={name}
|
||||
fieldTypes={fieldTypes}
|
||||
fields={fields}
|
||||
permissions={permissions}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ArrayFieldType.defaultProps = {
|
||||
label: '',
|
||||
validate: array,
|
||||
required: false,
|
||||
maxRows: undefined,
|
||||
minRows: undefined,
|
||||
singularLabel: 'Row',
|
||||
permissions: {},
|
||||
};
|
||||
|
||||
ArrayFieldType.propTypes = {
|
||||
fields: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
).isRequired,
|
||||
label: PropTypes.string,
|
||||
singularLabel: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
validate: PropTypes.func,
|
||||
required: PropTypes.bool,
|
||||
maxRows: PropTypes.number,
|
||||
minRows: PropTypes.number,
|
||||
permissions: PropTypes.shape({
|
||||
fields: PropTypes.shape({}),
|
||||
}),
|
||||
};
|
||||
|
||||
const RenderArray = React.memo((props) => {
|
||||
const {
|
||||
onDragEnd,
|
||||
label,
|
||||
showError,
|
||||
errorMessage,
|
||||
rows,
|
||||
singularLabel,
|
||||
addRow,
|
||||
removeRow,
|
||||
moveRow,
|
||||
path,
|
||||
customComponentsPath,
|
||||
name,
|
||||
fieldTypes,
|
||||
fields,
|
||||
permissions,
|
||||
value,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className={baseClass}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h3>{label}</h3>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</header>
|
||||
<Droppable droppableId="array-drop">
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => (
|
||||
<DraggableSection
|
||||
key={row.key}
|
||||
id={row.key}
|
||||
blockType="array"
|
||||
singularLabel={singularLabel}
|
||||
isOpen={row.open}
|
||||
rowCount={rows.length}
|
||||
rowIndex={i}
|
||||
addRow={() => addRow(i)}
|
||||
removeRow={() => removeRow(i)}
|
||||
moveRow={moveRow}
|
||||
parentPath={path}
|
||||
initNull={row.initNull}
|
||||
customComponentsPath={`${customComponentsPath}${name}.fields.`}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
permissions={permissions.fields}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Button
|
||||
onClick={() => addRow(value)}
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
iconPosition="left"
|
||||
>
|
||||
{`Add ${singularLabel}`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
});
|
||||
|
||||
RenderArray.defaultProps = {
|
||||
label: undefined,
|
||||
showError: false,
|
||||
errorMessage: undefined,
|
||||
rows: [],
|
||||
singularLabel: 'Row',
|
||||
path: '',
|
||||
customComponentsPath: undefined,
|
||||
value: undefined,
|
||||
};
|
||||
|
||||
RenderArray.propTypes = {
|
||||
label: PropTypes.string,
|
||||
showError: PropTypes.bool,
|
||||
errorMessage: PropTypes.string,
|
||||
rows: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
singularLabel: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
customComponentsPath: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.number,
|
||||
onDragEnd: PropTypes.func.isRequired,
|
||||
addRow: PropTypes.func.isRequired,
|
||||
removeRow: PropTypes.func.isRequired,
|
||||
moveRow: PropTypes.func.isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
fields: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
).isRequired,
|
||||
permissions: PropTypes.shape({
|
||||
fields: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default withCondition(ArrayFieldType);
|
||||
@@ -1,6 +1,6 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.field-type.repeater {
|
||||
.field-type.array {
|
||||
background: white;
|
||||
|
||||
&__add-button-wrap {
|
||||
@@ -25,14 +25,13 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-type.repeater {
|
||||
.field-type.repeater__add-button-wrap {
|
||||
.field-type.array,
|
||||
.field-type.blocks {
|
||||
.field-type.array {
|
||||
.field-type.array__add-button-wrap {
|
||||
margin-left: base(2.65);
|
||||
}
|
||||
|
||||
.field-type.repeater__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,15 @@ const BlockSelection = (props) => {
|
||||
} = block;
|
||||
|
||||
const handleBlockSelection = () => {
|
||||
console.log('adding');
|
||||
close();
|
||||
addRow(addRowIndex, slug);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
className={baseClass}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
onClick={handleBlockSelection}
|
||||
>
|
||||
<div className={`${baseClass}__image`}>
|
||||
@@ -37,11 +36,10 @@ const BlockSelection = (props) => {
|
||||
alt={blockImageAltText}
|
||||
/>
|
||||
)
|
||||
: <DefaultBlockImage />
|
||||
}
|
||||
: <DefaultBlockImage />}
|
||||
</div>
|
||||
<div className={`${baseClass}__label`}>{labels.singular}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
padding: base(.75) base(.5);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
box-shadow: 0;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-gray;
|
||||
@@ -1,34 +1,31 @@
|
||||
import React, {
|
||||
useEffect, useReducer, useCallback,
|
||||
useEffect, useReducer, useCallback, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import withCondition from '../../withCondition';
|
||||
import Button from '../../../elements/Button';
|
||||
import reducer from '../rowReducer';
|
||||
import useForm from '../../Form/useForm';
|
||||
import { useForm } from '../../Form/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 { flexible } from '../../../../../fields/validations';
|
||||
import { blocks as blocksValidator } from '../../../../../fields/validations';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'field-type flexible';
|
||||
const baseClass = 'field-type blocks';
|
||||
|
||||
const Flexible = (props) => {
|
||||
const Blocks = (props) => {
|
||||
const {
|
||||
label,
|
||||
name,
|
||||
path: pathFromProps,
|
||||
blocks,
|
||||
defaultValue,
|
||||
initialData,
|
||||
singularLabel,
|
||||
fieldTypes,
|
||||
maxRows,
|
||||
@@ -40,6 +37,10 @@ const Flexible = (props) => {
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||
const { customComponentsPath } = useRenderedFields();
|
||||
const { getDataByPath, initialState, dispatchFields } = useForm();
|
||||
|
||||
const memoizedValidate = useCallback((value) => {
|
||||
const validationResult = validate(
|
||||
value,
|
||||
@@ -50,6 +51,8 @@ const Flexible = (props) => {
|
||||
return validationResult;
|
||||
}, [validate, maxRows, minRows, singularLabel, blocks, required]);
|
||||
|
||||
const [disableFormData, setDisableFormData] = useState(false);
|
||||
|
||||
const {
|
||||
showError,
|
||||
errorMessage,
|
||||
@@ -58,73 +61,127 @@ const Flexible = (props) => {
|
||||
} = useFieldType({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
disableFormData: true,
|
||||
initialData: initialData?.length,
|
||||
defaultValue: defaultValue?.length,
|
||||
disableFormData,
|
||||
ignoreWhileFlattening: true,
|
||||
required,
|
||||
});
|
||||
|
||||
const dataToInitialize = initialData || defaultValue;
|
||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||
const { customComponentsPath } = useRenderedFields();
|
||||
const { getDataByPath } = useForm();
|
||||
|
||||
const addRow = (index, blockType) => {
|
||||
const data = getDataByPath(path);
|
||||
|
||||
dispatchRows({
|
||||
type: 'ADD', index, data, initialRowData: { blockType },
|
||||
});
|
||||
const addRow = useCallback((rowIndex, blockType) => {
|
||||
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
|
||||
|
||||
dispatchRows({ type: 'ADD', rowIndex, blockType });
|
||||
dispatchFields({ type: 'ADD_ROW', rowIndex, fieldSchema: block.fields, path, blockType });
|
||||
setValue(value + 1);
|
||||
};
|
||||
|
||||
const removeRow = (index) => {
|
||||
const data = getDataByPath(path);
|
||||
|
||||
dispatchRows({
|
||||
type: 'REMOVE',
|
||||
index,
|
||||
data,
|
||||
});
|
||||
}, [path, setValue, value, blocks, dispatchFields]);
|
||||
|
||||
const removeRow = useCallback((rowIndex) => {
|
||||
dispatchRows({ type: 'REMOVE', rowIndex });
|
||||
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
|
||||
setValue(value - 1);
|
||||
};
|
||||
}, [path, setValue, value, dispatchFields]);
|
||||
|
||||
const moveRow = (moveFromIndex, moveToIndex) => {
|
||||
const data = getDataByPath(path);
|
||||
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
|
||||
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
|
||||
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
|
||||
}, [dispatchRows, dispatchFields, path]);
|
||||
|
||||
dispatchRows({
|
||||
type: 'MOVE', index: moveFromIndex, moveToIndex, data,
|
||||
});
|
||||
};
|
||||
const toggleCollapse = useCallback((rowIndex) => {
|
||||
dispatchRows({ type: 'TOGGLE_COLLAPSE', rowIndex });
|
||||
}, []);
|
||||
|
||||
const toggleCollapse = (index) => {
|
||||
dispatchRows({
|
||||
type: 'TOGGLE_COLLAPSE', index, rows,
|
||||
});
|
||||
};
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
const onDragEnd = useCallback((result) => {
|
||||
if (!result.destination) return;
|
||||
const sourceIndex = result.source.index;
|
||||
const destinationIndex = result.destination.index;
|
||||
moveRow(sourceIndex, destinationIndex);
|
||||
};
|
||||
}, [moveRow]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatchRows({
|
||||
type: 'SET_ALL',
|
||||
rows: dataToInitialize.reduce((acc, data) => ([
|
||||
...acc,
|
||||
{
|
||||
key: uuidv4(),
|
||||
open: true,
|
||||
data,
|
||||
},
|
||||
]), []),
|
||||
});
|
||||
}, [dataToInitialize]);
|
||||
const data = getDataByPath(path);
|
||||
dispatchRows({ type: 'SET_ALL', data });
|
||||
}, [initialState, getDataByPath, path]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(rows?.length || 0);
|
||||
|
||||
if (rows?.length === 0) {
|
||||
setDisableFormData(false);
|
||||
} else {
|
||||
setDisableFormData(true);
|
||||
}
|
||||
}, [rows, setValue]);
|
||||
|
||||
return (
|
||||
<RenderBlocks
|
||||
onDragEnd={onDragEnd}
|
||||
label={label}
|
||||
showError={showError}
|
||||
errorMessage={errorMessage}
|
||||
rows={rows}
|
||||
singularLabel={singularLabel}
|
||||
addRow={addRow}
|
||||
removeRow={removeRow}
|
||||
moveRow={moveRow}
|
||||
path={path}
|
||||
customComponentsPath={customComponentsPath}
|
||||
name={name}
|
||||
fieldTypes={fieldTypes}
|
||||
toggleCollapse={toggleCollapse}
|
||||
permissions={permissions}
|
||||
value={value}
|
||||
blocks={blocks}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Blocks.defaultProps = {
|
||||
label: '',
|
||||
singularLabel: 'Block',
|
||||
validate: blocksValidator,
|
||||
required: false,
|
||||
maxRows: undefined,
|
||||
minRows: undefined,
|
||||
permissions: {},
|
||||
};
|
||||
|
||||
Blocks.propTypes = {
|
||||
blocks: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
).isRequired,
|
||||
label: PropTypes.string,
|
||||
singularLabel: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
validate: PropTypes.func,
|
||||
required: PropTypes.bool,
|
||||
maxRows: PropTypes.number,
|
||||
minRows: PropTypes.number,
|
||||
permissions: PropTypes.shape({
|
||||
fields: PropTypes.shape({}),
|
||||
}),
|
||||
};
|
||||
|
||||
const RenderBlocks = React.memo((props) => {
|
||||
const {
|
||||
onDragEnd,
|
||||
label,
|
||||
showError,
|
||||
errorMessage,
|
||||
rows,
|
||||
singularLabel,
|
||||
addRow,
|
||||
removeRow,
|
||||
moveRow,
|
||||
path,
|
||||
customComponentsPath,
|
||||
name,
|
||||
fieldTypes,
|
||||
permissions,
|
||||
value,
|
||||
toggleCollapse,
|
||||
blocks,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
@@ -138,27 +195,22 @@ const Flexible = (props) => {
|
||||
/>
|
||||
</header>
|
||||
|
||||
<Droppable droppableId="flexible-drop">
|
||||
{provided => (
|
||||
<Droppable droppableId="blocks-drop">
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
let { blockType } = row.data;
|
||||
|
||||
if (!blockType) {
|
||||
blockType = dataToInitialize?.[i]?.blockType;
|
||||
}
|
||||
|
||||
const blockToRender = blocks.find(block => block.slug === blockType);
|
||||
const { blockType } = row;
|
||||
const blockToRender = blocks.find((block) => block.slug === blockType);
|
||||
|
||||
if (blockToRender) {
|
||||
return (
|
||||
<DraggableSection
|
||||
key={row.key}
|
||||
id={row.key}
|
||||
blockType="flexible"
|
||||
blockType="blocks"
|
||||
blocks={blocks}
|
||||
singularLabel={blockToRender?.labels?.singular}
|
||||
isOpen={row.open}
|
||||
@@ -169,7 +221,6 @@ const Flexible = (props) => {
|
||||
moveRow={moveRow}
|
||||
toggleRowCollapse={() => toggleCollapse(i)}
|
||||
parentPath={path}
|
||||
initialData={row.data}
|
||||
customComponentsPath={`${customComponentsPath}${name}.fields.`}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions.fields}
|
||||
@@ -178,7 +229,9 @@ const Flexible = (props) => {
|
||||
{
|
||||
name: 'blockType',
|
||||
type: 'text',
|
||||
hidden: 'admin',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -186,8 +239,7 @@ const Flexible = (props) => {
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
@@ -221,42 +273,43 @@ const Flexible = (props) => {
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
});
|
||||
|
||||
RenderBlocks.defaultProps = {
|
||||
label: undefined,
|
||||
showError: false,
|
||||
errorMessage: undefined,
|
||||
rows: [],
|
||||
singularLabel: 'Row',
|
||||
path: '',
|
||||
customComponentsPath: undefined,
|
||||
value: undefined,
|
||||
};
|
||||
|
||||
Flexible.defaultProps = {
|
||||
label: '',
|
||||
defaultValue: [],
|
||||
initialData: [],
|
||||
singularLabel: 'Block',
|
||||
validate: flexible,
|
||||
required: false,
|
||||
maxRows: undefined,
|
||||
minRows: undefined,
|
||||
permissions: {},
|
||||
};
|
||||
|
||||
Flexible.propTypes = {
|
||||
defaultValue: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
initialData: PropTypes.arrayOf(
|
||||
RenderBlocks.propTypes = {
|
||||
label: PropTypes.string,
|
||||
showError: PropTypes.bool,
|
||||
errorMessage: PropTypes.string,
|
||||
rows: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
singularLabel: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
customComponentsPath: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.number,
|
||||
onDragEnd: PropTypes.func.isRequired,
|
||||
addRow: PropTypes.func.isRequired,
|
||||
removeRow: PropTypes.func.isRequired,
|
||||
moveRow: PropTypes.func.isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
permissions: PropTypes.shape({
|
||||
fields: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
blocks: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
).isRequired,
|
||||
label: PropTypes.string,
|
||||
singularLabel: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
validate: PropTypes.func,
|
||||
required: PropTypes.bool,
|
||||
maxRows: PropTypes.number,
|
||||
minRows: PropTypes.number,
|
||||
permissions: PropTypes.shape({
|
||||
fields: PropTypes.shape({}),
|
||||
}),
|
||||
toggleCollapse: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withCondition(Flexible);
|
||||
export default withCondition(Blocks);
|
||||
@@ -1,6 +1,6 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.field-type.flexible {
|
||||
.field-type.blocks {
|
||||
|
||||
&__add-button-wrap {
|
||||
|
||||
@@ -24,3 +24,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-type.array,
|
||||
.field-type.blocks {
|
||||
.field-type.blocks {
|
||||
.field-type.blocks__add-button-wrap {
|
||||
margin-left: base(2.65);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,15 @@ const Checkbox = (props) => {
|
||||
name,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
label,
|
||||
readOnly,
|
||||
onChange,
|
||||
disableFormData,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
@@ -41,8 +41,6 @@ const Checkbox = (props) => {
|
||||
} = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
disableFormData,
|
||||
});
|
||||
@@ -95,12 +93,8 @@ const Checkbox = (props) => {
|
||||
Checkbox.defaultProps = {
|
||||
label: null,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
defaultValue: false,
|
||||
initialData: false,
|
||||
admin: {},
|
||||
validate: checkbox,
|
||||
width: undefined,
|
||||
style: {},
|
||||
path: '',
|
||||
onChange: undefined,
|
||||
disableFormData: false,
|
||||
@@ -109,13 +103,13 @@ Checkbox.defaultProps = {
|
||||
Checkbox.propTypes = {
|
||||
path: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
readOnly: PropTypes.bool,
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
required: PropTypes.bool,
|
||||
defaultValue: PropTypes.bool,
|
||||
initialData: PropTypes.bool,
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
label: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
disableFormData: PropTypes.bool,
|
||||
|
||||
@@ -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/useFormFields';
|
||||
import { useFormFields } from '../../Form/context';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@ const DateTime = (props) => {
|
||||
path: pathFromProps,
|
||||
name,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
errorMessage,
|
||||
label,
|
||||
readOnly,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
@@ -43,8 +43,6 @@ const DateTime = (props) => {
|
||||
} = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
});
|
||||
|
||||
@@ -86,14 +84,10 @@ const DateTime = (props) => {
|
||||
DateTime.defaultProps = {
|
||||
label: null,
|
||||
required: false,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
validate: date,
|
||||
errorMessage: defaultError,
|
||||
width: undefined,
|
||||
style: {},
|
||||
admin: {},
|
||||
path: '',
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
DateTime.propTypes = {
|
||||
@@ -101,13 +95,13 @@ DateTime.propTypes = {
|
||||
path: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.string,
|
||||
validate: PropTypes.func,
|
||||
errorMessage: PropTypes.string,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
readOnly: PropTypes.bool,
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default withCondition(DateTime);
|
||||
|
||||
@@ -13,15 +13,15 @@ const Email = (props) => {
|
||||
name,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
label,
|
||||
placeholder,
|
||||
autoComplete,
|
||||
readOnly,
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
@@ -33,9 +33,6 @@ const Email = (props) => {
|
||||
|
||||
const fieldType = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
});
|
||||
@@ -91,12 +88,10 @@ Email.defaultProps = {
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
placeholder: undefined,
|
||||
width: undefined,
|
||||
style: {},
|
||||
admin: {},
|
||||
autoComplete: undefined,
|
||||
validate: email,
|
||||
path: '',
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
Email.propTypes = {
|
||||
@@ -107,11 +102,13 @@ Email.propTypes = {
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.string,
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withCondition(Email);
|
||||
|
||||
@@ -7,7 +7,7 @@ import './index.scss';
|
||||
|
||||
const Group = (props) => {
|
||||
const {
|
||||
label, fields, name, path: pathFromProps, fieldTypes, initialData,
|
||||
label, fields, name, path: pathFromProps, fieldTypes,
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
@@ -18,15 +18,12 @@ const Group = (props) => {
|
||||
<div className="field-type group">
|
||||
<h3>{label}</h3>
|
||||
<RenderFields
|
||||
initialData={initialData}
|
||||
fieldTypes={fieldTypes}
|
||||
customComponentsPath={`${customComponentsPath}${name}.fields.`}
|
||||
fieldSchema={fields.map((subField) => {
|
||||
return {
|
||||
...subField,
|
||||
path: `${path}${subField.name ? `.${subField.name}` : ''}`,
|
||||
};
|
||||
})}
|
||||
fieldSchema={fields.map((subField) => ({
|
||||
...subField,
|
||||
path: `${path}${subField.name ? `.${subField.name}` : ''}`,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -34,12 +31,10 @@ const Group = (props) => {
|
||||
|
||||
Group.defaultProps = {
|
||||
label: '',
|
||||
initialData: {},
|
||||
path: '',
|
||||
};
|
||||
|
||||
Group.propTypes = {
|
||||
initialData: PropTypes.shape({}),
|
||||
fields: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
).isRequired,
|
||||
|
||||
@@ -8,8 +8,6 @@ const HiddenInput = (props) => {
|
||||
name,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
@@ -17,8 +15,6 @@ const HiddenInput = (props) => {
|
||||
const { value, setValue } = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -33,8 +29,6 @@ const HiddenInput = (props) => {
|
||||
|
||||
HiddenInput.defaultProps = {
|
||||
required: false,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
path: '',
|
||||
};
|
||||
|
||||
@@ -42,8 +36,6 @@ HiddenInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.string,
|
||||
};
|
||||
|
||||
export default withCondition(HiddenInput);
|
||||
|
||||
@@ -13,24 +13,24 @@ const NumberField = (props) => {
|
||||
name,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
label,
|
||||
placeholder,
|
||||
max,
|
||||
min,
|
||||
readOnly,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value) => {
|
||||
const validationResult = validate(value, { min, max });
|
||||
const validationResult = validate(value, { min, max, required });
|
||||
return validationResult;
|
||||
}, [validate, max, min]);
|
||||
}, [validate, max, min, required]);
|
||||
|
||||
const {
|
||||
value,
|
||||
@@ -39,13 +39,16 @@ const NumberField = (props) => {
|
||||
errorMessage,
|
||||
} = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
});
|
||||
|
||||
const handleChange = useCallback((e) => {
|
||||
let val = parseInt(e.target.value, 10);
|
||||
if (Number.isNaN(val)) val = '';
|
||||
setValue(val);
|
||||
}, [setValue]);
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
'number',
|
||||
@@ -72,7 +75,7 @@ const NumberField = (props) => {
|
||||
/>
|
||||
<input
|
||||
value={value || ''}
|
||||
onChange={e => setValue(parseInt(e.target.value, 10))}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly ? 'disabled' : undefined}
|
||||
placeholder={placeholder}
|
||||
type="number"
|
||||
@@ -85,17 +88,13 @@ const NumberField = (props) => {
|
||||
|
||||
NumberField.defaultProps = {
|
||||
label: null,
|
||||
path: undefined,
|
||||
required: false,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
placeholder: undefined,
|
||||
width: undefined,
|
||||
style: {},
|
||||
max: undefined,
|
||||
min: undefined,
|
||||
path: '',
|
||||
readOnly: false,
|
||||
validate: number,
|
||||
admin: {},
|
||||
};
|
||||
|
||||
NumberField.propTypes = {
|
||||
@@ -103,15 +102,15 @@ NumberField.propTypes = {
|
||||
path: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
defaultValue: PropTypes.number,
|
||||
initialData: PropTypes.number,
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.string,
|
||||
max: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
readOnly: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withCondition(NumberField);
|
||||
|
||||
@@ -13,8 +13,6 @@ const Password = (props) => {
|
||||
path: pathFromProps,
|
||||
name,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
@@ -38,8 +36,6 @@ const Password = (props) => {
|
||||
} = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
});
|
||||
@@ -82,8 +78,6 @@ const Password = (props) => {
|
||||
|
||||
Password.defaultProps = {
|
||||
required: false,
|
||||
initialData: undefined,
|
||||
defaultValue: undefined,
|
||||
validate: password,
|
||||
width: undefined,
|
||||
style: {},
|
||||
@@ -95,8 +89,6 @@ Password.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
initialData: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
@@ -15,13 +15,13 @@ const RadioGroup = (props) => {
|
||||
name,
|
||||
path: pathFromProps,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
label,
|
||||
readOnly,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
options,
|
||||
} = props;
|
||||
|
||||
@@ -40,8 +40,6 @@ const RadioGroup = (props) => {
|
||||
} = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
});
|
||||
|
||||
@@ -70,7 +68,7 @@ const RadioGroup = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
{options?.map((option) => {
|
||||
const isSelected = !value ? (option.value === defaultValue) : (option.value === value);
|
||||
const isSelected = option.value === value;
|
||||
|
||||
return (
|
||||
<RadioInput
|
||||
@@ -88,25 +86,21 @@ const RadioGroup = (props) => {
|
||||
RadioGroup.defaultProps = {
|
||||
label: null,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
defaultValue: null,
|
||||
initialData: undefined,
|
||||
validate: radio,
|
||||
width: undefined,
|
||||
style: {},
|
||||
admin: {},
|
||||
path: '',
|
||||
};
|
||||
|
||||
RadioGroup.propTypes = {
|
||||
path: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
readOnly: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.string,
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.string,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, {
|
||||
Component, useState, useEffect, useCallback,
|
||||
Component, useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Cookies from 'universal-cookie';
|
||||
import some from 'async-some';
|
||||
import config from 'payload/config';
|
||||
import withCondition from '../../withCondition';
|
||||
@@ -14,16 +13,14 @@ import { relationship } from '../../../../../fields/validations';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const cookies = new Cookies();
|
||||
|
||||
const {
|
||||
cookiePrefix, serverURL, routes: { api }, collections,
|
||||
serverURL, routes: { api }, collections,
|
||||
} = config;
|
||||
|
||||
const cookieTokenName = `${cookiePrefix}-token`;
|
||||
|
||||
const maxResultsPerRequest = 10;
|
||||
|
||||
const baseClass = 'relationship';
|
||||
|
||||
class Relationship extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -36,6 +33,7 @@ class Relationship extends Component {
|
||||
lastFullyLoadedRelation: -1,
|
||||
lastLoadedPage: 1,
|
||||
options: [],
|
||||
errorLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +49,7 @@ class Relationship extends Component {
|
||||
}
|
||||
|
||||
getNextOptions = (params = {}) => {
|
||||
const { errorLoading } = this.state;
|
||||
const { clear } = params;
|
||||
|
||||
if (clear) {
|
||||
@@ -59,47 +58,55 @@ class Relationship extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
relations, lastFullyLoadedRelation, lastLoadedPage, search,
|
||||
} = this.state;
|
||||
const token = cookies.get(cookieTokenName);
|
||||
if (!errorLoading) {
|
||||
const {
|
||||
relations, lastFullyLoadedRelation, lastLoadedPage, search,
|
||||
} = this.state;
|
||||
|
||||
const relationsToSearch = relations.slice(lastFullyLoadedRelation + 1);
|
||||
const relationsToSearch = relations.slice(lastFullyLoadedRelation + 1);
|
||||
|
||||
if (relationsToSearch.length > 0) {
|
||||
some(relationsToSearch, async (relation, callback) => {
|
||||
const collection = collections.find(coll => coll.slug === relation);
|
||||
const fieldToSearch = collection.useAsTitle || 'id';
|
||||
const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : '';
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`, {
|
||||
headers: {
|
||||
Authorization: `JWT ${token}`,
|
||||
},
|
||||
if (relationsToSearch.length > 0) {
|
||||
some(relationsToSearch, async (relation, callback) => {
|
||||
const collection = collections.find((coll) => coll.slug === relation);
|
||||
const fieldToSearch = collection?.admin?.useAsTitle || 'id';
|
||||
const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : '';
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (data.hasNextPage) {
|
||||
return callback(false, {
|
||||
data,
|
||||
relation,
|
||||
});
|
||||
}
|
||||
|
||||
return callback({ relation, data });
|
||||
}
|
||||
|
||||
let error = 'There was a problem loading options for this field.';
|
||||
|
||||
if (response.status === 403) {
|
||||
error = 'You do not have permission to load options for this field.';
|
||||
}
|
||||
|
||||
return this.setState({
|
||||
errorLoading: error,
|
||||
});
|
||||
}, (lastPage, nextPage) => {
|
||||
if (nextPage) {
|
||||
const { data, relation } = nextPage;
|
||||
this.addOptions(data, relation);
|
||||
} else {
|
||||
const { data, relation } = lastPage;
|
||||
this.addOptions(data, relation);
|
||||
this.setState({
|
||||
lastFullyLoadedRelation: relations.indexOf(relation),
|
||||
lastLoadedPage: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.hasNextPage) {
|
||||
return callback(false, {
|
||||
data,
|
||||
relation,
|
||||
});
|
||||
}
|
||||
|
||||
return callback({ relation, data });
|
||||
}, (lastPage, nextPage) => {
|
||||
if (nextPage) {
|
||||
const { data, relation } = nextPage;
|
||||
this.addOptions(data, relation);
|
||||
} else {
|
||||
const { data, relation } = lastPage;
|
||||
this.addOptions(data, relation);
|
||||
this.setState({
|
||||
lastFullyLoadedRelation: relations.indexOf(relation),
|
||||
lastLoadedPage: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +116,7 @@ class Relationship extends Component {
|
||||
const { hasMany } = this.props;
|
||||
|
||||
if (hasMany && Array.isArray(selectedValue)) {
|
||||
return selectedValue.map(val => val.value);
|
||||
return selectedValue.map((val) => val.value);
|
||||
}
|
||||
|
||||
return selectedValue ? selectedValue.value : selectedValue;
|
||||
@@ -136,9 +143,9 @@ class Relationship extends Component {
|
||||
});
|
||||
} else if (value) {
|
||||
if (hasMany) {
|
||||
foundValue = value.map(val => options.find(option => option.value === val));
|
||||
foundValue = value.map((val) => options.find((option) => option.value === val));
|
||||
} else {
|
||||
foundValue = options.find(option => option.value === value);
|
||||
foundValue = options.find((option) => option.value === value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,31 +155,29 @@ class Relationship extends Component {
|
||||
addOptions = (data, relation) => {
|
||||
const { hasMultipleRelations } = this.props;
|
||||
const { lastLoadedPage, options } = this.state;
|
||||
const collection = collections.find(coll => coll.slug === relation);
|
||||
const collection = collections.find((coll) => coll.slug === relation);
|
||||
|
||||
if (!hasMultipleRelations) {
|
||||
this.setState({
|
||||
options: [
|
||||
...options,
|
||||
...data.docs.map(doc => ({
|
||||
label: doc[collection.useAsTitle || 'id'],
|
||||
...data.docs.map((doc) => ({
|
||||
label: doc[collection?.admin?.useAsTitle || 'id'],
|
||||
value: doc.id,
|
||||
})),
|
||||
],
|
||||
});
|
||||
} else {
|
||||
const allOptionGroups = [...options];
|
||||
const optionsToAddTo = allOptionGroups.find(optionGroup => optionGroup.label === collection.labels.plural);
|
||||
const optionsToAddTo = allOptionGroups.find((optionGroup) => optionGroup.label === collection.labels.plural);
|
||||
|
||||
const newOptions = data.docs.map((doc) => {
|
||||
return {
|
||||
label: doc[collection.useAsTitle || 'id'],
|
||||
value: {
|
||||
relationTo: collection.slug,
|
||||
value: doc.id,
|
||||
},
|
||||
};
|
||||
});
|
||||
const newOptions = data.docs.map((doc) => ({
|
||||
label: doc[collection?.admin?.useAsTitle || 'id'],
|
||||
value: {
|
||||
relationTo: collection.slug,
|
||||
value: doc.id,
|
||||
},
|
||||
}));
|
||||
|
||||
if (optionsToAddTo) {
|
||||
optionsToAddTo.options = [
|
||||
@@ -211,13 +216,11 @@ class Relationship extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { options } = this.state;
|
||||
const { options, errorLoading } = this.state;
|
||||
|
||||
const {
|
||||
path,
|
||||
required,
|
||||
style,
|
||||
width,
|
||||
errorMessage,
|
||||
label,
|
||||
hasMany,
|
||||
@@ -225,22 +228,23 @@ class Relationship extends Component {
|
||||
showError,
|
||||
formProcessing,
|
||||
setValue,
|
||||
readOnly,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
} = this.props;
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
'relationship',
|
||||
baseClass,
|
||||
showError && 'error',
|
||||
errorLoading && 'error-loading',
|
||||
readOnly && 'read-only',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const valueToRender = this.findValueInOptions(options, value);
|
||||
|
||||
// ///////////////////////////////////////////
|
||||
// TODO: simplify formatValue pattern seen below with react select
|
||||
// ///////////////////////////////////////////
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
@@ -258,56 +262,59 @@ class Relationship extends Component {
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
<ReactSelect
|
||||
onInputChange={this.handleInputChange}
|
||||
onChange={!readOnly ? setValue : undefined}
|
||||
formatValue={this.formatSelectedValue}
|
||||
onMenuScrollToBottom={this.handleMenuScrollToBottom}
|
||||
findValueInOptions={this.findValueInOptions}
|
||||
value={valueToRender}
|
||||
showError={showError}
|
||||
disabled={formProcessing}
|
||||
options={options}
|
||||
isMulti={hasMany}
|
||||
/>
|
||||
{!errorLoading && (
|
||||
<ReactSelect
|
||||
onInputChange={this.handleInputChange}
|
||||
onChange={!readOnly ? setValue : undefined}
|
||||
formatValue={this.formatSelectedValue}
|
||||
onMenuScrollToBottom={this.handleMenuScrollToBottom}
|
||||
findValueInOptions={this.findValueInOptions}
|
||||
value={valueToRender}
|
||||
showError={showError}
|
||||
disabled={formProcessing}
|
||||
options={options}
|
||||
isMulti={hasMany}
|
||||
/>
|
||||
)}
|
||||
{errorLoading && (
|
||||
<div className={`${baseClass}__error-loading`}>
|
||||
{errorLoading}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Relationship.defaultProps = {
|
||||
style: {},
|
||||
required: false,
|
||||
errorMessage: '',
|
||||
hasMany: false,
|
||||
width: undefined,
|
||||
showError: false,
|
||||
value: undefined,
|
||||
path: '',
|
||||
formProcessing: false,
|
||||
readOnly: false,
|
||||
admin: {},
|
||||
};
|
||||
|
||||
Relationship.propTypes = {
|
||||
readOnly: PropTypes.bool,
|
||||
relationTo: PropTypes.oneOfType([
|
||||
PropTypes.oneOf(Object.keys(collections).map((key) => {
|
||||
return collections[key].slug;
|
||||
})),
|
||||
PropTypes.oneOf(Object.keys(collections).map((key) => collections[key].slug)),
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.oneOf(Object.keys(collections).map((key) => {
|
||||
return collections[key].slug;
|
||||
})),
|
||||
PropTypes.oneOf(Object.keys(collections).map((key) => collections[key].slug)),
|
||||
),
|
||||
]).isRequired,
|
||||
required: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
errorMessage: PropTypes.string,
|
||||
showError: PropTypes.bool,
|
||||
label: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
formProcessing: PropTypes.bool,
|
||||
width: PropTypes.string,
|
||||
hasMany: PropTypes.bool,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
hasMultipleRelations: PropTypes.bool.isRequired,
|
||||
@@ -319,14 +326,11 @@ Relationship.propTypes = {
|
||||
};
|
||||
|
||||
const RelationshipFieldType = (props) => {
|
||||
const [formattedInitialData, setFormattedInitialData] = useState(undefined);
|
||||
|
||||
const {
|
||||
defaultValue, relationTo, hasMany, validate, path, name, initialData, required,
|
||||
relationTo, validate, path, name, required,
|
||||
} = props;
|
||||
|
||||
const hasMultipleRelations = Array.isArray(relationTo);
|
||||
const dataToInitialize = initialData || defaultValue;
|
||||
|
||||
const memoizedValidate = useCallback((value) => {
|
||||
const validationResult = validate(value, { required });
|
||||
@@ -337,39 +341,10 @@ const RelationshipFieldType = (props) => {
|
||||
const fieldType = useFieldType({
|
||||
...props,
|
||||
path: path || name,
|
||||
initialData: formattedInitialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
required,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const formatInitialData = (valueToFormat) => {
|
||||
if (hasMultipleRelations) {
|
||||
return {
|
||||
...valueToFormat,
|
||||
value: valueToFormat.value.id,
|
||||
};
|
||||
}
|
||||
|
||||
return valueToFormat.id;
|
||||
};
|
||||
|
||||
if (dataToInitialize) {
|
||||
if (hasMany && Array.isArray(dataToInitialize)) {
|
||||
const newFormattedInitialData = [];
|
||||
|
||||
dataToInitialize.forEach((individualValue) => {
|
||||
newFormattedInitialData.push(formatInitialData(individualValue));
|
||||
});
|
||||
|
||||
setFormattedInitialData(newFormattedInitialData);
|
||||
} else {
|
||||
setFormattedInitialData(formatInitialData(dataToInitialize));
|
||||
}
|
||||
}
|
||||
}, [dataToInitialize, hasMany, hasMultipleRelations]);
|
||||
|
||||
return (
|
||||
<Relationship
|
||||
{...props}
|
||||
|
||||
@@ -3,3 +3,11 @@
|
||||
.field-type.relationship {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.relationship__error-loading {
|
||||
border: 1px solid $color-red;
|
||||
min-height: base(2);
|
||||
padding: base(.5) base(.75);
|
||||
background-color: $color-red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import React, { useEffect, useReducer, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import withCondition from '../../withCondition';
|
||||
import Button from '../../../elements/Button';
|
||||
import DraggableSection from '../../DraggableSection';
|
||||
import reducer from '../rowReducer';
|
||||
import { useRenderedFields } from '../../RenderFields';
|
||||
import useForm from '../../Form/useForm';
|
||||
import useFieldType from '../../useFieldType';
|
||||
import Error from '../../Error';
|
||||
import { repeater } from '../../../../../fields/validations';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'field-type repeater';
|
||||
|
||||
const Repeater = (props) => {
|
||||
const {
|
||||
label,
|
||||
name,
|
||||
path: pathFromProps,
|
||||
fields,
|
||||
defaultValue,
|
||||
initialData,
|
||||
fieldTypes,
|
||||
validate,
|
||||
required,
|
||||
maxRows,
|
||||
minRows,
|
||||
labels: {
|
||||
singular: singularLabel,
|
||||
},
|
||||
permissions,
|
||||
} = props;
|
||||
|
||||
const dataToInitialize = initialData || defaultValue;
|
||||
const [rows, dispatchRows] = useReducer(reducer, []);
|
||||
const { customComponentsPath } = useRenderedFields();
|
||||
const { getDataByPath } = useForm();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value) => {
|
||||
const validationResult = validate(value, { minRows, maxRows, required });
|
||||
return validationResult;
|
||||
}, [validate, maxRows, minRows, required]);
|
||||
|
||||
const {
|
||||
showError,
|
||||
errorMessage,
|
||||
value,
|
||||
setValue,
|
||||
} = useFieldType({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
disableFormData: true,
|
||||
initialData: initialData?.length,
|
||||
defaultValue: defaultValue?.length,
|
||||
required,
|
||||
});
|
||||
|
||||
const addRow = (rowIndex) => {
|
||||
const data = getDataByPath(path);
|
||||
|
||||
dispatchRows({
|
||||
type: 'ADD', index: rowIndex, data,
|
||||
});
|
||||
|
||||
setValue(value + 1);
|
||||
};
|
||||
|
||||
const removeRow = (rowIndex) => {
|
||||
const data = getDataByPath(path);
|
||||
|
||||
dispatchRows({
|
||||
type: 'REMOVE',
|
||||
index: rowIndex,
|
||||
data,
|
||||
});
|
||||
|
||||
setValue(value - 1);
|
||||
};
|
||||
|
||||
const moveRow = (moveFromIndex, moveToIndex) => {
|
||||
const data = getDataByPath(path);
|
||||
|
||||
dispatchRows({
|
||||
type: 'MOVE', index: moveFromIndex, moveToIndex, data,
|
||||
});
|
||||
};
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
if (!result.destination) return;
|
||||
const sourceIndex = result.source.index;
|
||||
const destinationIndex = result.destination.index;
|
||||
moveRow(sourceIndex, destinationIndex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatchRows({
|
||||
type: 'SET_ALL',
|
||||
rows: dataToInitialize.reduce((acc, data) => ([
|
||||
...acc,
|
||||
{
|
||||
key: uuidv4(),
|
||||
open: true,
|
||||
data,
|
||||
},
|
||||
]), []),
|
||||
});
|
||||
}, [dataToInitialize]);
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className={baseClass}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h3>{label}</h3>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</header>
|
||||
<Droppable droppableId="repeater-drop">
|
||||
{provided => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
return (
|
||||
<DraggableSection
|
||||
key={row.key}
|
||||
id={row.key}
|
||||
blockType="repeater"
|
||||
singularLabel={singularLabel}
|
||||
isOpen={row.open}
|
||||
rowCount={rows.length}
|
||||
rowIndex={i}
|
||||
addRow={() => addRow(i)}
|
||||
removeRow={() => removeRow(i)}
|
||||
moveRow={moveRow}
|
||||
parentPath={path}
|
||||
initialData={row.data}
|
||||
initNull={row.initNull}
|
||||
customComponentsPath={`${customComponentsPath}${name}.fields.`}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
permissions={permissions.fields}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Button
|
||||
onClick={() => addRow(value)}
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
iconPosition="left"
|
||||
>
|
||||
{`Add ${singularLabel}`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
Repeater.defaultProps = {
|
||||
label: '',
|
||||
defaultValue: [],
|
||||
initialData: [],
|
||||
validate: repeater,
|
||||
required: false,
|
||||
maxRows: undefined,
|
||||
minRows: undefined,
|
||||
labels: {
|
||||
singular: 'Row',
|
||||
},
|
||||
permissions: {},
|
||||
};
|
||||
|
||||
Repeater.propTypes = {
|
||||
defaultValue: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
initialData: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
fields: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
).isRequired,
|
||||
label: PropTypes.string,
|
||||
labels: PropTypes.shape({
|
||||
singular: PropTypes.string,
|
||||
}),
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
validate: PropTypes.func,
|
||||
required: PropTypes.bool,
|
||||
maxRows: PropTypes.number,
|
||||
minRows: PropTypes.number,
|
||||
permissions: PropTypes.shape({
|
||||
fields: PropTypes.shape({}),
|
||||
}),
|
||||
};
|
||||
|
||||
export default withCondition(Repeater);
|
||||
@@ -75,14 +75,14 @@ const RichText = (props) => {
|
||||
path: pathFromProps,
|
||||
name,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
label,
|
||||
placeholder,
|
||||
readOnly,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const editor = useMemo(() => pipe(createEditor(), ...withPlugins), []);
|
||||
@@ -92,8 +92,6 @@ const RichText = (props) => {
|
||||
const fieldType = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate,
|
||||
});
|
||||
|
||||
@@ -150,12 +148,8 @@ const RichText = (props) => {
|
||||
RichText.defaultProps = {
|
||||
label: null,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
placeholder: undefined,
|
||||
width: undefined,
|
||||
style: {},
|
||||
admin: {},
|
||||
validate: richText,
|
||||
path: '',
|
||||
};
|
||||
@@ -164,13 +158,13 @@ RichText.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
readOnly: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,29 +7,24 @@ import './index.scss';
|
||||
|
||||
const Row = (props) => {
|
||||
const {
|
||||
fields, fieldTypes, initialData, path, permissions,
|
||||
fields, fieldTypes, path, permissions,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="field-type row">
|
||||
<RenderFields
|
||||
permissions={permissions}
|
||||
initialData={initialData}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields.map((field) => {
|
||||
return {
|
||||
...field,
|
||||
path: `${path ? `${path}.` : ''}${field.name}`,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<RenderFields
|
||||
className="field-type row"
|
||||
permissions={permissions}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
path: `${path ? `${path}.` : ''}${field.name}`,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Row.defaultProps = {
|
||||
path: '',
|
||||
initialData: undefined,
|
||||
permissions: {},
|
||||
};
|
||||
|
||||
@@ -39,7 +34,6 @@ Row.propTypes = {
|
||||
).isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
path: PropTypes.string,
|
||||
initialData: PropTypes.shape({}),
|
||||
permissions: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { select } from '../../../../../fields/validations';
|
||||
import './index.scss';
|
||||
|
||||
const findFullOption = (value, options) => {
|
||||
const matchedOption = options.find(option => option?.value === value);
|
||||
const matchedOption = options.find((option) => option?.value === value);
|
||||
|
||||
if (matchedOption) {
|
||||
if (typeof matchedOption === 'object' && matchedOption.label && matchedOption.value) {
|
||||
@@ -65,15 +65,15 @@ const Select = (props) => {
|
||||
path: pathFromProps,
|
||||
name,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
label,
|
||||
options,
|
||||
hasMany,
|
||||
readOnly,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const path = pathFromProps || name;
|
||||
@@ -92,8 +92,6 @@ const Select = (props) => {
|
||||
path,
|
||||
label,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
});
|
||||
|
||||
@@ -137,34 +135,24 @@ const Select = (props) => {
|
||||
};
|
||||
|
||||
Select.defaultProps = {
|
||||
style: {},
|
||||
admin: {},
|
||||
required: false,
|
||||
validate: select,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
hasMany: false,
|
||||
width: undefined,
|
||||
path: '',
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
Select.propTypes = {
|
||||
required: PropTypes.bool,
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.string.isRequired,
|
||||
defaultValue: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.array,
|
||||
]),
|
||||
initialData: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.array,
|
||||
]),
|
||||
validate: PropTypes.func,
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
width: PropTypes.string,
|
||||
options: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.string,
|
||||
|
||||
@@ -13,14 +13,14 @@ const Text = (props) => {
|
||||
path: pathFromProps,
|
||||
name,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
label,
|
||||
placeholder,
|
||||
readOnly,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
minLength,
|
||||
maxLength,
|
||||
} = props;
|
||||
@@ -34,9 +34,6 @@ const Text = (props) => {
|
||||
|
||||
const fieldType = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
});
|
||||
@@ -88,12 +85,8 @@ const Text = (props) => {
|
||||
Text.defaultProps = {
|
||||
label: null,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
admin: {},
|
||||
placeholder: undefined,
|
||||
width: undefined,
|
||||
style: {},
|
||||
validate: text,
|
||||
path: '',
|
||||
minLength: undefined,
|
||||
@@ -104,13 +97,13 @@ Text.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
readOnly: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.string,
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
|
||||
@@ -13,14 +13,14 @@ const Textarea = (props) => {
|
||||
path: pathFromProps,
|
||||
name,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
validate,
|
||||
style,
|
||||
width,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
label,
|
||||
placeholder,
|
||||
readOnly,
|
||||
minLength,
|
||||
maxLength,
|
||||
rows,
|
||||
@@ -40,9 +40,6 @@ const Textarea = (props) => {
|
||||
errorMessage,
|
||||
} = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData,
|
||||
defaultValue,
|
||||
validate: memoizedValidate,
|
||||
enableDebouncedValue: true,
|
||||
});
|
||||
@@ -87,14 +84,10 @@ const Textarea = (props) => {
|
||||
Textarea.defaultProps = {
|
||||
required: false,
|
||||
label: null,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
validate: textarea,
|
||||
width: undefined,
|
||||
style: {},
|
||||
placeholder: null,
|
||||
path: '',
|
||||
readOnly: false,
|
||||
admin: {},
|
||||
minLength: undefined,
|
||||
maxLength: undefined,
|
||||
rows: 8,
|
||||
@@ -104,14 +97,14 @@ Textarea.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.string,
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
minLength: PropTypes.number,
|
||||
maxLength: PropTypes.number,
|
||||
rows: PropTypes.number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, useModal } from '@trbl/react-modal';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import config from '../../../../../config';
|
||||
import MinimalTemplate from '../../../../templates/Minimal';
|
||||
import Form from '../../../Form';
|
||||
@@ -67,7 +67,7 @@ const AddUploadModal = (props) => {
|
||||
/>
|
||||
</header>
|
||||
<RenderFields
|
||||
filter={field => (!field.position || (field.position && field.position !== 'sidebar'))}
|
||||
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
customComponentsPath={`${collection.slug}.fields.`}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, useModal } from '@trbl/react-modal';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import config from '../../../../../config';
|
||||
import MinimalTemplate from '../../../../templates/Minimal';
|
||||
import Button from '../../../../elements/Button';
|
||||
@@ -74,6 +74,7 @@ const SelectExistingUploadModal = (props) => {
|
||||
/>
|
||||
</header>
|
||||
<ListControls
|
||||
enableColumns={false}
|
||||
handleChange={setListControls}
|
||||
collection={{
|
||||
...collection,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useModal } from '@trbl/react-modal';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import config from '../../../../config';
|
||||
import useFieldType from '../../useFieldType';
|
||||
import withCondition from '../../withCondition';
|
||||
@@ -14,7 +14,7 @@ import SelectExistingModal from './SelectExisting';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const { collections } = config;
|
||||
const { collections, serverURL, routes: { api } } = config;
|
||||
|
||||
const baseClass = 'upload';
|
||||
|
||||
@@ -26,29 +26,32 @@ const Upload = (props) => {
|
||||
path: pathFromProps,
|
||||
name,
|
||||
required,
|
||||
defaultValue,
|
||||
initialData,
|
||||
style,
|
||||
width,
|
||||
admin: {
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
label,
|
||||
readOnly,
|
||||
validate,
|
||||
relationTo,
|
||||
fieldTypes,
|
||||
} = props;
|
||||
|
||||
const collection = collections.find(coll => coll.slug === relationTo);
|
||||
const collection = collections.find((coll) => coll.slug === relationTo);
|
||||
|
||||
const path = pathFromProps || name;
|
||||
const addModalSlug = `${path}-add`;
|
||||
const selectExistingModalSlug = `${path}-select-existing`;
|
||||
|
||||
const memoizedValidate = useCallback((value) => {
|
||||
const validationResult = validate(value, { required });
|
||||
return validationResult;
|
||||
}, [validate, required]);
|
||||
|
||||
const fieldType = useFieldType({
|
||||
path,
|
||||
required,
|
||||
initialData: initialData?.id,
|
||||
defaultValue,
|
||||
validate,
|
||||
validate: memoizedValidate,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -66,10 +69,19 @@ const Upload = (props) => {
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
setInternalValue(initialData);
|
||||
if (typeof value === 'string') {
|
||||
const fetchFile = async () => {
|
||||
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`);
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
setInternalValue(json);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFile();
|
||||
}
|
||||
}, [initialData]);
|
||||
}, [value, setInternalValue, relationTo]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -89,7 +101,7 @@ const Upload = (props) => {
|
||||
required={required}
|
||||
/>
|
||||
{collection?.upload && (
|
||||
<>
|
||||
<React.Fragment>
|
||||
{internalValue && (
|
||||
<FileDetails
|
||||
{...collection.upload}
|
||||
@@ -142,7 +154,7 @@ const Upload = (props) => {
|
||||
addModalSlug,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -151,11 +163,7 @@ const Upload = (props) => {
|
||||
Upload.defaultProps = {
|
||||
label: null,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
defaultValue: undefined,
|
||||
initialData: undefined,
|
||||
width: undefined,
|
||||
style: {},
|
||||
admin: {},
|
||||
validate: upload,
|
||||
path: '',
|
||||
};
|
||||
@@ -164,14 +172,12 @@ Upload.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
readOnly: PropTypes.bool,
|
||||
defaultValue: PropTypes.string,
|
||||
initialData: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
validate: PropTypes.func,
|
||||
width: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
admin: PropTypes.shape({
|
||||
readOnly: PropTypes.bool,
|
||||
style: PropTypes.shape({}),
|
||||
width: PropTypes.string,
|
||||
}),
|
||||
relationTo: PropTypes.string.isRequired,
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
label: PropTypes.oneOfType([
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
position: relative;
|
||||
|
||||
&__wrap {
|
||||
padding: base(1.5);
|
||||
display: flex;
|
||||
padding: base(1.5) base(1.5) $baseline;
|
||||
background: $color-background-gray;
|
||||
|
||||
.btn {
|
||||
margin: 0 $baseline 0 0;
|
||||
margin: 0 $baseline base(.5) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export { default as auth } from './Auth';
|
||||
export { default as file } from './File';
|
||||
|
||||
export { default as email } from './Email';
|
||||
export { default as hidden } from './HiddenInput';
|
||||
export { default as text } from './Text';
|
||||
@@ -15,8 +12,8 @@ export { default as checkbox } from './Checkbox';
|
||||
export { default as richText } from './RichText';
|
||||
export { default as radio } from './RadioGroup';
|
||||
|
||||
export { default as flexible } from './Flexible';
|
||||
export { default as blocks } from './Blocks';
|
||||
export { default as group } from './Group';
|
||||
export { default as repeater } from './Repeater';
|
||||
export { default as array } from './Array';
|
||||
export { default as row } from './Row';
|
||||
export { default as upload } from './Upload';
|
||||
|
||||
@@ -2,59 +2,57 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const reducer = (currentState, action) => {
|
||||
const {
|
||||
type, index, moveToIndex, rows, data = [], initialRowData = {},
|
||||
type, rowIndex, moveFromIndex, moveToIndex, data, blockType,
|
||||
} = action;
|
||||
|
||||
const stateCopy = [...currentState];
|
||||
|
||||
switch (type) {
|
||||
case 'SET_ALL':
|
||||
return rows;
|
||||
case 'SET_ALL': {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((dataRow) => {
|
||||
const row = {
|
||||
key: uuidv4(),
|
||||
open: true,
|
||||
};
|
||||
|
||||
if (dataRow.blockType) {
|
||||
row.blockType = dataRow.blockType;
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
case 'TOGGLE_COLLAPSE':
|
||||
stateCopy[index].open = !stateCopy[index].open;
|
||||
stateCopy[rowIndex].open = !stateCopy[rowIndex].open;
|
||||
return stateCopy;
|
||||
|
||||
case 'ADD': {
|
||||
stateCopy.splice(index + 1, 0, {
|
||||
const newRow = {
|
||||
open: true,
|
||||
key: uuidv4(),
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
data.splice(index + 1, 0, initialRowData);
|
||||
if (blockType) newRow.blockType = blockType;
|
||||
|
||||
const result = stateCopy.map((row, i) => {
|
||||
return {
|
||||
...row,
|
||||
data: {
|
||||
...(data[i] || {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
stateCopy.splice(rowIndex + 1, 0, newRow);
|
||||
|
||||
return result;
|
||||
return stateCopy;
|
||||
}
|
||||
|
||||
|
||||
case 'REMOVE':
|
||||
stateCopy.splice(index, 1);
|
||||
stateCopy.splice(rowIndex, 1);
|
||||
return stateCopy;
|
||||
|
||||
case 'MOVE': {
|
||||
const stateCopyWithNewData = stateCopy.map((row, i) => {
|
||||
return {
|
||||
...row,
|
||||
data: {
|
||||
...(data[i] || {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const movingRowState = { ...stateCopyWithNewData[index] };
|
||||
stateCopyWithNewData.splice(index, 1);
|
||||
stateCopyWithNewData.splice(moveToIndex, 0, movingRowState);
|
||||
return stateCopyWithNewData;
|
||||
const movingRowState = { ...stateCopy[moveFromIndex] };
|
||||
stateCopy.splice(moveFromIndex, 1);
|
||||
stateCopy.splice(moveToIndex, 0, movingRowState);
|
||||
return stateCopy;
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
import {
|
||||
useContext, useCallback, useEffect, useState,
|
||||
useCallback, useEffect, useState,
|
||||
} from 'react';
|
||||
import FormContext from '../Form/FormContext';
|
||||
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context';
|
||||
import useDebounce from '../../../hooks/useDebounce';
|
||||
import useUnmountEffect from '../../../hooks/useUnmountEffect';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const useFieldType = (options) => {
|
||||
const {
|
||||
path,
|
||||
initialData: data,
|
||||
defaultValue,
|
||||
validate,
|
||||
disableFormData,
|
||||
enableDebouncedValue,
|
||||
disableFormData,
|
||||
ignoreWhileFlattening,
|
||||
} = options;
|
||||
|
||||
// Determine what the initial data is to be used
|
||||
// If initialData is defined, that means that data has been provided
|
||||
// via the API and should override any default values present.
|
||||
// If no initialData, use default value
|
||||
const initialData = data !== undefined ? data : defaultValue;
|
||||
|
||||
const formContext = useContext(FormContext);
|
||||
const formContext = useForm();
|
||||
const submitted = useFormSubmitted();
|
||||
const processing = useFormProcessing();
|
||||
const modified = useFormModified();
|
||||
|
||||
const {
|
||||
dispatchFields, submitted, processing, getField, setModified, modified,
|
||||
dispatchFields, getField, setModified, reset,
|
||||
} = formContext;
|
||||
|
||||
const [internalValue, setInternalValue] = useState(initialData);
|
||||
const [internalValue, setInternalValue] = useState(undefined);
|
||||
|
||||
// Debounce internal values to update form state only every 60ms
|
||||
const debouncedValue = useDebounce(internalValue, 120);
|
||||
|
||||
// Get field by path
|
||||
const field = getField(path);
|
||||
const fieldExists = Boolean(field);
|
||||
|
||||
const initialValue = field?.initialValue;
|
||||
|
||||
// Valid could be a string equal to an error message
|
||||
const valid = (field && typeof field.valid === 'boolean') ? field.valid : true;
|
||||
@@ -54,48 +50,39 @@ const useFieldType = (options) => {
|
||||
fieldToDispatch.valid = false;
|
||||
}
|
||||
|
||||
if (disableFormData) {
|
||||
fieldToDispatch.disableFormData = true;
|
||||
}
|
||||
fieldToDispatch.disableFormData = disableFormData;
|
||||
fieldToDispatch.ignoreWhileFlattening = ignoreWhileFlattening;
|
||||
fieldToDispatch.initialValue = initialValue;
|
||||
|
||||
dispatchFields(fieldToDispatch);
|
||||
}, [path, dispatchFields, validate, disableFormData]);
|
||||
|
||||
}, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue]);
|
||||
|
||||
// Method to return from `useFieldType`, used to
|
||||
// update internal field values from field component(s)
|
||||
// as fast as they arrive. NOTE - this method is NOT debounced
|
||||
const setValue = useCallback((e) => {
|
||||
const value = (e && e.target) ? e.target.value : e;
|
||||
const val = (e && e.target) ? e.target.value : e;
|
||||
|
||||
if (!modified) setModified(true);
|
||||
|
||||
setInternalValue(value);
|
||||
setInternalValue(val);
|
||||
}, [setModified, modified]);
|
||||
|
||||
// Remove field from state on "unmount"
|
||||
// This is mostly used for repeater / flex content row modifications
|
||||
useUnmountEffect(() => {
|
||||
formContext.dispatchFields({ path, type: 'REMOVE' });
|
||||
});
|
||||
useEffect(() => {
|
||||
setInternalValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
// The only time that the FORM value should be updated
|
||||
// is when the debounced value updates. So, when the debounced value updates,
|
||||
// send it up to the form
|
||||
|
||||
const formValue = enableDebouncedValue ? debouncedValue : internalValue;
|
||||
const valueToSend = enableDebouncedValue ? debouncedValue : internalValue;
|
||||
|
||||
useEffect(() => {
|
||||
if (!fieldExists || formValue !== undefined) {
|
||||
sendField(formValue);
|
||||
if (valueToSend !== undefined) {
|
||||
sendField(valueToSend);
|
||||
}
|
||||
}, [formValue, sendField, fieldExists]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData !== undefined) {
|
||||
setInternalValue(initialData);
|
||||
}
|
||||
}, [initialData]);
|
||||
}, [valueToSend, sendField]);
|
||||
|
||||
return {
|
||||
...options,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useFormFields from '../Form/useFormFields';
|
||||
import { useFormFields } from '../Form/context';
|
||||
|
||||
const withCondition = (Field) => {
|
||||
const CheckForCondition = (props) => {
|
||||
const { condition } = props;
|
||||
const {
|
||||
admin: {
|
||||
condition,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
if (condition) {
|
||||
return <WithCondition {...props} />;
|
||||
@@ -15,19 +19,27 @@ const withCondition = (Field) => {
|
||||
};
|
||||
|
||||
CheckForCondition.defaultProps = {
|
||||
condition: null,
|
||||
admin: undefined,
|
||||
name: '',
|
||||
path: '',
|
||||
};
|
||||
|
||||
CheckForCondition.propTypes = {
|
||||
condition: PropTypes.func,
|
||||
admin: PropTypes.shape({
|
||||
condition: PropTypes.func,
|
||||
}),
|
||||
name: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
};
|
||||
|
||||
const WithCondition = (props) => {
|
||||
const { condition, name, path } = props;
|
||||
const {
|
||||
name,
|
||||
path,
|
||||
admin: {
|
||||
condition,
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const { getData, getSiblingData } = useFormFields();
|
||||
|
||||
@@ -43,13 +55,15 @@ const withCondition = (Field) => {
|
||||
};
|
||||
|
||||
WithCondition.defaultProps = {
|
||||
condition: null,
|
||||
admin: undefined,
|
||||
name: '',
|
||||
path: '',
|
||||
};
|
||||
|
||||
WithCondition.propTypes = {
|
||||
condition: PropTypes.func,
|
||||
admin: PropTypes.shape({
|
||||
condition: PropTypes.func,
|
||||
}),
|
||||
name: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { ScrollInfoProvider } from '@trbl/react-scroll-info';
|
||||
import { WindowInfoProvider } from '@trbl/react-window-info';
|
||||
import { ModalProvider, ModalContainer } from '@trbl/react-modal';
|
||||
import { ScrollInfoProvider } from '@faceless-ui/scroll-info';
|
||||
import { WindowInfoProvider } from '@faceless-ui/window-info';
|
||||
import { ModalProvider, ModalContainer } from '@faceless-ui/modal';
|
||||
import { SearchParamsProvider } from './utilities/SearchParams';
|
||||
import { LocaleProvider } from './utilities/Locale';
|
||||
import StatusList, { StatusListProvider } from './elements/Status';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import NavigationPrompt from 'react-router-navigation-prompt';
|
||||
import useForm from '../../forms/Form/useForm';
|
||||
import { useForm } from '../../forms/Form/context';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import Button from '../../elements/Button';
|
||||
|
||||
@@ -13,27 +13,25 @@ const LeaveWithoutSaving = () => {
|
||||
|
||||
return (
|
||||
<NavigationPrompt when={modified}>
|
||||
{({ onConfirm, onCancel }) => {
|
||||
return (
|
||||
<div className={modalSlug}>
|
||||
<MinimalTemplate>
|
||||
<h1>Leave without saving</h1>
|
||||
<p>Your changes have not been saved. If you leave now, you will lose your changes.</p>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
Stay on this page
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Leave anyway
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
{({ onConfirm, onCancel }) => (
|
||||
<div className={modalSlug}>
|
||||
<MinimalTemplate>
|
||||
<h1>Leave without saving</h1>
|
||||
<p>Your changes have not been saved. If you leave now, you will lose your changes.</p>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
Stay on this page
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Leave anyway
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</div>
|
||||
)}
|
||||
</NavigationPrompt>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useModal, Modal } from '@trbl/react-modal';
|
||||
import { useModal, Modal } from '@faceless-ui/modal';
|
||||
import config from 'payload/config';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import Button from '../../elements/Button';
|
||||
@@ -13,7 +13,7 @@ const baseClass = 'stay-logged-in';
|
||||
const { routes: { admin } } = config;
|
||||
|
||||
const StayLoggedInModal = (props) => {
|
||||
const { refreshToken } = props;
|
||||
const { refreshCookie } = props;
|
||||
const history = useHistory();
|
||||
const { closeAll: closeAllModals } = useModal();
|
||||
|
||||
@@ -36,7 +36,7 @@ const StayLoggedInModal = (props) => {
|
||||
Log out
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
refreshToken();
|
||||
refreshCookie();
|
||||
closeAllModals();
|
||||
}}
|
||||
>
|
||||
@@ -49,7 +49,7 @@ const StayLoggedInModal = (props) => {
|
||||
};
|
||||
|
||||
StayLoggedInModal.propTypes = {
|
||||
refreshToken: PropTypes.func.isRequired,
|
||||
refreshCookie: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default StayLoggedInModal;
|
||||
|
||||
@@ -37,6 +37,7 @@ const RenderCustomComponent = (props) => {
|
||||
|
||||
RenderCustomComponent.defaultProps = {
|
||||
path: undefined,
|
||||
componentProps: {},
|
||||
};
|
||||
|
||||
RenderCustomComponent.propTypes = {
|
||||
@@ -47,7 +48,7 @@ RenderCustomComponent.propTypes = {
|
||||
PropTypes.node,
|
||||
PropTypes.element,
|
||||
]).isRequired,
|
||||
componentProps: PropTypes.shape({}).isRequired,
|
||||
componentProps: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
export default RenderCustomComponent;
|
||||
|
||||
@@ -31,7 +31,9 @@ const DefaultAccount = (props) => {
|
||||
const {
|
||||
slug,
|
||||
fields,
|
||||
useAsTitle,
|
||||
admin: {
|
||||
useAsTitle,
|
||||
},
|
||||
timestamps,
|
||||
preview,
|
||||
} = collection;
|
||||
@@ -61,7 +63,7 @@ const DefaultAccount = (props) => {
|
||||
</h1>
|
||||
</header>
|
||||
<RenderFields
|
||||
filter={field => (!field.position || (field.position && field.position !== 'sidebar'))}
|
||||
filter={(field) => (!field.position || (field.position && field.position !== 'sidebar'))}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
initialData={dataToRender}
|
||||
@@ -100,7 +102,7 @@ const DefaultAccount = (props) => {
|
||||
</div>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields
|
||||
filter={field => field.position === 'sidebar'}
|
||||
filter={(field) => field.position === 'sidebar'}
|
||||
position="sidebar"
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
@@ -114,7 +116,7 @@ const DefaultAccount = (props) => {
|
||||
<div>{data?.id}</div>
|
||||
</li>
|
||||
{timestamps && (
|
||||
<>
|
||||
<React.Fragment>
|
||||
{data.updatedAt && (
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>Last Modified</div>
|
||||
@@ -127,7 +129,7 @@ const DefaultAccount = (props) => {
|
||||
<div>{format(new Date(data.createdAt), 'MMMM do yyyy, h:mma')}</div>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
</ul>
|
||||
@@ -150,7 +152,9 @@ DefaultAccount.propTypes = {
|
||||
singular: PropTypes.string,
|
||||
}),
|
||||
slug: PropTypes.string,
|
||||
useAsTitle: PropTypes.string,
|
||||
admin: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
}),
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
preview: PropTypes.func,
|
||||
timestamps: PropTypes.bool,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import config from 'payload/config';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import StatusList, { useStatusList } from '../../elements/Status';
|
||||
import Form from '../../forms/Form';
|
||||
import RenderFields from '../../forms/RenderFields';
|
||||
import * as fieldTypes from '../../forms/field-types';
|
||||
@@ -16,29 +14,20 @@ const {
|
||||
admin: { user: userSlug }, collections, serverURL, routes: { admin, api },
|
||||
} = config;
|
||||
|
||||
const userConfig = collections.find(collection => collection.slug === userSlug);
|
||||
const userConfig = collections.find((collection) => collection.slug === userSlug);
|
||||
|
||||
const baseClass = 'create-first-user';
|
||||
|
||||
const CreateFirstUser = (props) => {
|
||||
const { setInitialized } = props;
|
||||
const { addStatus } = useStatusList();
|
||||
const { setToken } = useUser();
|
||||
const history = useHistory();
|
||||
|
||||
const handleAjaxResponse = (res) => {
|
||||
res.json().then((data) => {
|
||||
if (data.token) {
|
||||
setToken(data.token);
|
||||
setInitialized(true);
|
||||
history.push(`${admin}`);
|
||||
} else {
|
||||
addStatus({
|
||||
type: 'error',
|
||||
message: 'There was a problem creating your first user.',
|
||||
});
|
||||
}
|
||||
});
|
||||
const onSuccess = (json) => {
|
||||
if (json?.user?.token) {
|
||||
setToken(json.user.token);
|
||||
}
|
||||
|
||||
setInitialized(true);
|
||||
};
|
||||
|
||||
const fields = [
|
||||
@@ -59,11 +48,10 @@ const CreateFirstUser = (props) => {
|
||||
<MinimalTemplate className={baseClass}>
|
||||
<h1>Welcome to Payload</h1>
|
||||
<p>To begin, create your first user.</p>
|
||||
<StatusList />
|
||||
<Form
|
||||
handleAjaxResponse={handleAjaxResponse}
|
||||
disableSuccessStatus
|
||||
onSuccess={onSuccess}
|
||||
method="POST"
|
||||
redirect={admin}
|
||||
action={`${serverURL}${api}/${userSlug}/first-register`}
|
||||
>
|
||||
<RenderFields
|
||||
|
||||
89
src/client/components/views/Dashboard/Default.js
Normal file
89
src/client/components/views/Dashboard/Default.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import config from 'payload/config';
|
||||
import { useUser } from '../../data/User';
|
||||
import { useStepNav } from '../../elements/StepNav';
|
||||
import Eyebrow from '../../elements/Eyebrow';
|
||||
import Card from '../../elements/Card';
|
||||
import Button from '../../elements/Button';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
routes: {
|
||||
admin,
|
||||
},
|
||||
} = config;
|
||||
|
||||
const baseClass = 'dashboard';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [filteredGlobals, setFilteredGlobals] = useState([]);
|
||||
const { setStepNav } = useStepNav();
|
||||
const { push } = useHistory();
|
||||
const { permissions } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredGlobals(
|
||||
globals.filter((global) => permissions?.[global.slug]?.read?.permission),
|
||||
);
|
||||
}, [permissions]);
|
||||
|
||||
useEffect(() => {
|
||||
setStepNav([]);
|
||||
}, [setStepNav]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Eyebrow />
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<h3 className={`${baseClass}__label`}>Collections</h3>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{collections.map((collection) => {
|
||||
if (permissions?.[collection.slug]?.read?.permission) {
|
||||
return (
|
||||
<li key={collection.slug}>
|
||||
<Card
|
||||
title={collection.labels.plural}
|
||||
onClick={() => push({ pathname: `${admin}/collections/${collection.slug}` })}
|
||||
actions={(
|
||||
<Button
|
||||
el="link"
|
||||
to={`${admin}/collections/${collection.slug}/create`}
|
||||
icon="plus"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</ul>
|
||||
{(filteredGlobals.length > 0) && (
|
||||
<React.Fragment>
|
||||
<h3 className={`${baseClass}__label`}>Globals</h3>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{filteredGlobals.map((global) => (
|
||||
<li key={global.slug}>
|
||||
<Card
|
||||
title={global.label}
|
||||
onClick={() => push({ pathname: `${admin}/globals/${global.slug}` })}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,18 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import config from 'payload/config';
|
||||
import { useStepNav } from '../../elements/StepNav';
|
||||
import Eyebrow from '../../elements/Eyebrow';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const {
|
||||
routes: {
|
||||
admin,
|
||||
},
|
||||
} = config;
|
||||
|
||||
const baseClass = 'dashboard';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import DefaultDashboard from './Default';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { setStepNav } = useStepNav();
|
||||
@@ -22,15 +11,10 @@ const Dashboard = () => {
|
||||
}, [setStepNav]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Eyebrow />
|
||||
<h1>Dashboard</h1>
|
||||
<Link to={`${admin}/login`}>Login</Link>
|
||||
<br />
|
||||
<Link to={`${admin}/collections/pages`}>Pages List</Link>
|
||||
<br />
|
||||
<Link to={`${admin}/collections/pages/test123`}>Edit Page</Link>
|
||||
</div>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultDashboard}
|
||||
path="views.List"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,66 @@
|
||||
.dashboard {
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.dashboard {
|
||||
width: 100%;
|
||||
padding-right: base(2);
|
||||
|
||||
&__wrap {
|
||||
padding: base(2);
|
||||
background: white;
|
||||
}
|
||||
|
||||
&__label {
|
||||
@extend %body;
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
&__card-list {
|
||||
width: calc(100% + #{$baseline});
|
||||
margin: 0 - base(.5);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0 base(.5) $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
@include large-break {
|
||||
li {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
padding-right: 0;
|
||||
|
||||
&__wrap {
|
||||
padding: $baseline;
|
||||
}
|
||||
|
||||
li {
|
||||
width: 33.33%;
|
||||
}
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
&__card-list {
|
||||
width: calc(100% + #{base(.5)});
|
||||
margin: 0 - base(.25);
|
||||
}
|
||||
|
||||
li {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0 base(.25) base(.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const ForgotPassword = () => {
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||
const { user } = useUser();
|
||||
|
||||
const handleAjaxResponse = (res) => {
|
||||
const handleResponse = (res) => {
|
||||
res.json()
|
||||
.then(() => {
|
||||
setHasSubmitted(true);
|
||||
@@ -86,7 +86,7 @@ const ForgotPassword = () => {
|
||||
<StatusList />
|
||||
<Form
|
||||
novalidate
|
||||
handleAjaxResponse={handleAjaxResponse}
|
||||
handleResponse={handleResponse}
|
||||
method="POST"
|
||||
action={`${serverURL}${api}/${userSlug}/forgot-password`}
|
||||
>
|
||||
|
||||
140
src/client/components/views/Global/Default.js
Normal file
140
src/client/components/views/Global/Default.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import format from 'date-fns/format';
|
||||
import Eyebrow from '../../elements/Eyebrow';
|
||||
import Form from '../../forms/Form';
|
||||
import PreviewButton from '../../elements/PreviewButton';
|
||||
import FormSubmit from '../../forms/Submit';
|
||||
import RenderFields from '../../forms/RenderFields';
|
||||
import CopyToClipboard from '../../elements/CopyToClipboard';
|
||||
import * as fieldTypes from '../../forms/field-types';
|
||||
import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'global-edit';
|
||||
|
||||
const DefaultGlobalView = (props) => {
|
||||
const {
|
||||
global, data, onSave, permissions, action, apiURL,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
slug,
|
||||
fields,
|
||||
preview,
|
||||
label,
|
||||
} = global;
|
||||
|
||||
const hasSavePermission = permissions?.update?.permission;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Form
|
||||
className={`${baseClass}__form`}
|
||||
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.`}
|
||||
/>
|
||||
</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>
|
||||
{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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DefaultGlobalView.defaultProps = {
|
||||
data: undefined,
|
||||
};
|
||||
|
||||
DefaultGlobalView.propTypes = {
|
||||
global: PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
slug: PropTypes.string,
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
preview: PropTypes.func,
|
||||
}).isRequired,
|
||||
data: PropTypes.shape({
|
||||
updatedAt: PropTypes.string,
|
||||
}),
|
||||
onSave: PropTypes.func.isRequired,
|
||||
permissions: PropTypes.shape({
|
||||
update: PropTypes.shape({
|
||||
permission: PropTypes.bool,
|
||||
}),
|
||||
fields: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
action: PropTypes.string.isRequired,
|
||||
apiURL: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DefaultGlobalView;
|
||||
@@ -1,68 +1,80 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import config from 'payload/config';
|
||||
import { useStepNav } from '../../elements/StepNav';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
import Form from '../../forms/Form';
|
||||
import RenderFields from '../../forms/RenderFields';
|
||||
import * as fieldTypes from '../../forms/field-types';
|
||||
import { useUser } from '../../data/User';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
|
||||
const {
|
||||
serverURL,
|
||||
routes: {
|
||||
admin,
|
||||
api,
|
||||
},
|
||||
} = config;
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import DefaultGlobal from './Default';
|
||||
|
||||
const baseClass = 'global-edit';
|
||||
const { serverURL, routes: { admin, api } } = config;
|
||||
|
||||
const Global = (props) => {
|
||||
const { global: { slug, label, fields } } = props;
|
||||
const GlobalView = (props) => {
|
||||
const { state: locationState } = useLocation();
|
||||
const history = useHistory();
|
||||
const locale = useLocale();
|
||||
const { setStepNav } = useStepNav();
|
||||
const { permissions } = useUser();
|
||||
|
||||
const { global } = props;
|
||||
|
||||
const {
|
||||
slug,
|
||||
label,
|
||||
} = global;
|
||||
|
||||
const onSave = (json) => {
|
||||
history.push(`${admin}/globals/${global.slug}`, {
|
||||
status: {
|
||||
message: json.message,
|
||||
type: 'success',
|
||||
},
|
||||
data: json.doc,
|
||||
});
|
||||
};
|
||||
|
||||
const [{ data }] = usePayloadAPI(
|
||||
`${serverURL}${api}/globals/${slug}`,
|
||||
{ initialParams: { 'fallback-locale': 'null' } },
|
||||
);
|
||||
|
||||
const dataToRender = locationState?.data || data;
|
||||
|
||||
useEffect(() => {
|
||||
setStepNav([{
|
||||
url: `${admin}/globals/${slug}`,
|
||||
const nav = [{
|
||||
label,
|
||||
}]);
|
||||
}, [setStepNav, slug, label]);
|
||||
}];
|
||||
|
||||
setStepNav(nav);
|
||||
}, [setStepNav, label]);
|
||||
|
||||
const globalPermissions = permissions?.[slug];
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
Edit
|
||||
{' '}
|
||||
{label}
|
||||
</h1>
|
||||
</header>
|
||||
<Form
|
||||
className={`${baseClass}__form`}
|
||||
method={data ? 'put' : 'post'}
|
||||
action={`${serverURL}${api}/globals/${slug}`}
|
||||
>
|
||||
<RenderFields
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
initialData={data}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultGlobal}
|
||||
path={`${slug}.views.Edit`}
|
||||
componentProps={{
|
||||
data: dataToRender,
|
||||
permissions: globalPermissions,
|
||||
global,
|
||||
onSave,
|
||||
apiURL: `${serverURL}${api}/globals/${slug}?depth=0`,
|
||||
action: `${serverURL}${api}/globals/${slug}?locale=${locale}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Global.propTypes = {
|
||||
GlobalView.propTypes = {
|
||||
global: PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
slug: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
slug: PropTypes.string.isRequired,
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default Global;
|
||||
export default GlobalView;
|
||||
|
||||
186
src/client/components/views/Global/index.scss
Normal file
186
src/client/components/views/Global/index.scss
Normal file
@@ -0,0 +1,186 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.global-edit {
|
||||
width: 100%;
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
h1 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__collection-actions {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: base(1.5) 0 base(.5);
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
margin-right: base(.75);
|
||||
}
|
||||
}
|
||||
|
||||
&__edit {
|
||||
background: white;
|
||||
padding: base(5) base(6) base(6);
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
padding-top: base(3);
|
||||
padding-bottom: base(1);
|
||||
width: base(22);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__collection-actions,
|
||||
&__document-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields,
|
||||
&__api-url {
|
||||
padding-left: base(1.5);
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
@include blur-bg;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding-right: $baseline;
|
||||
}
|
||||
|
||||
&__document-actions--with-preview {
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
width: calc(50% - #{base(.5)});
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-right: base(.5);
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
|
||||
.form-submit {
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding-left: base(2);
|
||||
padding-right: base(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__api-url {
|
||||
margin-bottom: base(1.5);
|
||||
padding-right: base(1.5);
|
||||
|
||||
a {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin: 0;
|
||||
padding-top: $baseline;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
margin-bottom: base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
&__collection-actions,
|
||||
&__api-url {
|
||||
a, button {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--is-editing {
|
||||
.collection-edit__sidebar {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include large-break {
|
||||
&__edit {
|
||||
padding: base(3.5);
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__sidebar {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__edit {
|
||||
padding: $baseline;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
&__document-actions,
|
||||
&__meta,
|
||||
&__sidebar-fields,
|
||||
&__api-url {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
|
||||
&__api-url {
|
||||
margin-bottom: base(.5);
|
||||
}
|
||||
|
||||
&__collection-actions {
|
||||
margin-top: base(.5);
|
||||
padding-left: $baseline;
|
||||
padding-bottom: 0;
|
||||
order: 1;
|
||||
|
||||
li {
|
||||
margin: 0 base(.5) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
padding-bottom: base(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import config from 'payload/config';
|
||||
import { useUser } from '../../data/User';
|
||||
import Minimal from '../../templates/Minimal';
|
||||
@@ -10,7 +11,9 @@ const { routes: { admin } } = config;
|
||||
|
||||
const baseClass = 'logout';
|
||||
|
||||
const Logout = () => {
|
||||
const Logout = (props) => {
|
||||
const { inactivity } = props;
|
||||
|
||||
const { logOut } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,7 +23,12 @@ const Logout = () => {
|
||||
return (
|
||||
<Minimal className={baseClass}>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<h2>You have been logged out successfully.</h2>
|
||||
{inactivity && (
|
||||
<h2>You have been logged out due to inactivity.</h2>
|
||||
)}
|
||||
{!inactivity && (
|
||||
<h2>You have been logged out successfully.</h2>
|
||||
)}
|
||||
<br />
|
||||
<Button
|
||||
el="anchor"
|
||||
@@ -34,4 +42,12 @@ const Logout = () => {
|
||||
);
|
||||
};
|
||||
|
||||
Logout.defaultProps = {
|
||||
inactivity: false,
|
||||
};
|
||||
|
||||
Logout.propTypes = {
|
||||
inactivity: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Logout;
|
||||
|
||||
@@ -20,7 +20,7 @@ const ResetPassword = () => {
|
||||
const history = useHistory();
|
||||
const { user, setToken } = useUser();
|
||||
|
||||
const handleAjaxResponse = (res) => {
|
||||
const handleResponse = (res) => {
|
||||
res.json()
|
||||
.then((data) => {
|
||||
if (data.token) {
|
||||
@@ -60,7 +60,7 @@ const ResetPassword = () => {
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<StatusList />
|
||||
<Form
|
||||
handleAjaxResponse={handleAjaxResponse}
|
||||
handleResponse={handleResponse}
|
||||
method="POST"
|
||||
action={`${serverURL}${api}/${userSlug}/reset-password`}
|
||||
redirect={admin}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import useFieldType from '../../useFieldType';
|
||||
import Label from '../../Label';
|
||||
import Button from '../../../elements/Button';
|
||||
import CopyToClipboard from '../../../elements/CopyToClipboard';
|
||||
import { text } from '../../../../../fields/validations';
|
||||
import useFormFields from '../../Form/useFormFields';
|
||||
import useFieldType from '../../../../forms/useFieldType';
|
||||
import Label from '../../../../forms/Label';
|
||||
import Button from '../../../../elements/Button';
|
||||
import CopyToClipboard from '../../../../elements/CopyToClipboard';
|
||||
import { text } from '../../../../../../fields/validations';
|
||||
import { useFormFields } from '../../../../forms/Form/context';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const path = 'apiKey';
|
||||
const baseClass = 'api-key';
|
||||
const validate = val => text(val, { minLength: 24, maxLength: 48 });
|
||||
const validate = (val) => text(val, { minLength: 24, maxLength: 48 });
|
||||
|
||||
const APIKey = (props) => {
|
||||
const {
|
||||
initialData,
|
||||
} = props;
|
||||
const APIKey = () => {
|
||||
const [initialAPIKey, setInitialAPIKey] = useState(null);
|
||||
|
||||
const { getField } = useFormFields();
|
||||
|
||||
@@ -36,7 +33,6 @@ const APIKey = (props) => {
|
||||
|
||||
const fieldType = useFieldType({
|
||||
path: 'apiKey',
|
||||
initialData: initialData || uuidv4(),
|
||||
validate,
|
||||
});
|
||||
|
||||
@@ -45,6 +41,16 @@ const APIKey = (props) => {
|
||||
setValue,
|
||||
} = fieldType;
|
||||
|
||||
useEffect(() => {
|
||||
setInitialAPIKey(uuidv4());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!apiKeyValue) {
|
||||
setValue(initialAPIKey);
|
||||
}
|
||||
}, [apiKeyValue, setValue, initialAPIKey]);
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
'api-key',
|
||||
@@ -52,7 +58,7 @@ const APIKey = (props) => {
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<>
|
||||
<React.Fragment>
|
||||
<div className={classes}>
|
||||
<Label
|
||||
htmlFor={path}
|
||||
@@ -73,16 +79,8 @@ const APIKey = (props) => {
|
||||
>
|
||||
Generate new API Key
|
||||
</Button>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
APIKey.defaultProps = {
|
||||
initialData: undefined,
|
||||
};
|
||||
|
||||
APIKey.propTypes = {
|
||||
initialData: PropTypes.string,
|
||||
};
|
||||
|
||||
export default APIKey;
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Email from '../Email';
|
||||
import Password from '../Password';
|
||||
import Checkbox from '../Checkbox';
|
||||
import Button from '../../../elements/Button';
|
||||
import ConfirmPassword from '../ConfirmPassword';
|
||||
import useFormFields from '../../Form/useFormFields';
|
||||
import Email from '../../../../forms/field-types/Email';
|
||||
import Password from '../../../../forms/field-types/Password';
|
||||
import Checkbox from '../../../../forms/field-types/Checkbox';
|
||||
import Button from '../../../../elements/Button';
|
||||
import ConfirmPassword from '../../../../forms/field-types/ConfirmPassword';
|
||||
import { useFormFields, useFormModified } from '../../../../forms/Form/context';
|
||||
import APIKey from './APIKey';
|
||||
|
||||
import './index.scss';
|
||||
@@ -13,19 +13,25 @@ import './index.scss';
|
||||
const baseClass = 'auth-fields';
|
||||
|
||||
const Auth = (props) => {
|
||||
const { initialData, useAPIKey, requirePassword } = props;
|
||||
const { useAPIKey, requirePassword } = props;
|
||||
const [changingPassword, setChangingPassword] = useState(requirePassword);
|
||||
const { getField } = useFormFields();
|
||||
const modified = useFormModified();
|
||||
|
||||
const enableAPIKey = getField('enableAPIKey');
|
||||
|
||||
useEffect(() => {
|
||||
if (!modified) {
|
||||
setChangingPassword(false);
|
||||
}
|
||||
}, [modified]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Email
|
||||
required
|
||||
name="email"
|
||||
label="Email"
|
||||
initialData={initialData?.email}
|
||||
autoComplete="email"
|
||||
/>
|
||||
{changingPassword && (
|
||||
@@ -60,12 +66,11 @@ const Auth = (props) => {
|
||||
{useAPIKey && (
|
||||
<div className={`${baseClass}__api-key`}>
|
||||
<Checkbox
|
||||
initialData={initialData?.enableAPIKey}
|
||||
label="Enable API Key"
|
||||
name="enableAPIKey"
|
||||
/>
|
||||
{enableAPIKey?.value && (
|
||||
<APIKey initialData={initialData?.apiKey} />
|
||||
<APIKey />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -74,14 +79,11 @@ const Auth = (props) => {
|
||||
};
|
||||
|
||||
Auth.defaultProps = {
|
||||
initialData: undefined,
|
||||
useAPIKey: false,
|
||||
requirePassword: false,
|
||||
};
|
||||
|
||||
Auth.propTypes = {
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
initialData: PropTypes.shape({}),
|
||||
useAPIKey: PropTypes.bool,
|
||||
requirePassword: PropTypes.bool,
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
@import '../shared.scss';
|
||||
@import '../../../../../scss/styles.scss';
|
||||
@import '../../../../forms/field-types/shared.scss';
|
||||
|
||||
.auth-fields {
|
||||
margin: base(1.5) 0 base(2);
|
||||
@@ -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 Loading from '../../../elements/Loading';
|
||||
import PreviewButton from '../../../elements/PreviewButton';
|
||||
import FormSubmit from '../../../forms/Submit';
|
||||
import RenderFields from '../../../forms/RenderFields';
|
||||
@@ -14,10 +15,12 @@ import DeleteDocument from '../../../elements/DeleteDocument';
|
||||
import * as fieldTypes from '../../../forms/field-types';
|
||||
import RenderTitle from '../../../elements/RenderTitle';
|
||||
import LeaveWithoutSaving from '../../../modals/LeaveWithoutSaving';
|
||||
import Auth from './Auth';
|
||||
import Upload from './Upload';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const { serverURL, routes: { api, admin } } = config;
|
||||
const { routes: { admin } } = config;
|
||||
|
||||
const baseClass = 'collection-edit';
|
||||
|
||||
@@ -25,26 +28,30 @@ const DefaultEditView = (props) => {
|
||||
const { params: { id } = {} } = useRouteMatch();
|
||||
|
||||
const {
|
||||
collection, isEditing, data, onSave, permissions,
|
||||
collection,
|
||||
isEditing,
|
||||
data,
|
||||
onSave,
|
||||
permissions,
|
||||
isLoading,
|
||||
initialState,
|
||||
apiURL,
|
||||
action,
|
||||
hasSavePermission,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
slug,
|
||||
fields,
|
||||
useAsTitle,
|
||||
admin: {
|
||||
useAsTitle,
|
||||
},
|
||||
timestamps,
|
||||
preview,
|
||||
auth,
|
||||
upload,
|
||||
} = collection;
|
||||
|
||||
const apiURL = `${serverURL}${api}/${slug}/${id}`;
|
||||
let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}`;
|
||||
const hasSavePermission = (isEditing && permissions?.update?.permission) || (!isEditing && permissions?.update?.permission);
|
||||
|
||||
if (auth && !isEditing) {
|
||||
action = `${action}/register`;
|
||||
}
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
isEditing && `${baseClass}--is-editing`,
|
||||
@@ -58,36 +65,56 @@ const DefaultEditView = (props) => {
|
||||
action={action}
|
||||
onSuccess={onSave}
|
||||
disabled={!hasSavePermission}
|
||||
initialState={initialState}
|
||||
>
|
||||
<div className={`${baseClass}__main`}>
|
||||
<Eyebrow />
|
||||
<LeaveWithoutSaving />
|
||||
<div className={`${baseClass}__edit`}>
|
||||
<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.position || (field.position && field.position !== 'sidebar'))}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
initialData={data}
|
||||
customComponentsPath={`${slug}.fields.`}
|
||||
/>
|
||||
{isLoading && (
|
||||
<Loading />
|
||||
)}
|
||||
{!isLoading && (
|
||||
<React.Fragment>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
<RenderTitle {...{ data, useAsTitle, fallback: '[Untitled]' }} />
|
||||
</h1>
|
||||
</header>
|
||||
{auth && (
|
||||
<Auth
|
||||
useAPIKey={auth.useAPIKey}
|
||||
requirePassword={!isEditing}
|
||||
/>
|
||||
)}
|
||||
{upload && (
|
||||
<Upload
|
||||
data={data}
|
||||
{...upload}
|
||||
fieldTypes={fieldTypes}
|
||||
/>
|
||||
)}
|
||||
<RenderFields
|
||||
operation={isEditing ? 'update' : 'create'}
|
||||
readOnly={!hasSavePermission}
|
||||
permissions={permissions.fields}
|
||||
filter={(field) => (!field?.admin?.position || (field?.admin?.position !== 'sidebar'))}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields}
|
||||
customComponentsPath={`${slug}.fields.`}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -99,68 +126,73 @@ const DefaultEditView = (props) => {
|
||||
)}
|
||||
</ul>
|
||||
) : undefined}
|
||||
<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>
|
||||
)}
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields
|
||||
operation={isEditing ? 'update' : 'create'}
|
||||
readOnly={!hasSavePermission}
|
||||
permissions={permissions.fields}
|
||||
filter={field => field.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 && (
|
||||
<>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
<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}
|
||||
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>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -169,29 +201,40 @@ const DefaultEditView = (props) => {
|
||||
|
||||
DefaultEditView.defaultProps = {
|
||||
isEditing: false,
|
||||
isLoading: true,
|
||||
data: undefined,
|
||||
onSave: null,
|
||||
initialState: undefined,
|
||||
apiURL: undefined,
|
||||
};
|
||||
|
||||
DefaultEditView.propTypes = {
|
||||
hasSavePermission: PropTypes.bool.isRequired,
|
||||
action: PropTypes.string.isRequired,
|
||||
apiURL: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
collection: PropTypes.shape({
|
||||
labels: PropTypes.shape({
|
||||
plural: PropTypes.string,
|
||||
singular: PropTypes.string,
|
||||
}),
|
||||
slug: PropTypes.string,
|
||||
useAsTitle: PropTypes.string,
|
||||
admin: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
}),
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
preview: PropTypes.func,
|
||||
timestamps: PropTypes.bool,
|
||||
auth: PropTypes.shape({}),
|
||||
auth: PropTypes.shape({
|
||||
useAPIKey: PropTypes.bool,
|
||||
}),
|
||||
upload: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
data: PropTypes.shape({
|
||||
updatedAt: PropTypes.string,
|
||||
createdAt: PropTypes.string,
|
||||
}),
|
||||
onSave: PropTypes.func,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
permissions: PropTypes.shape({
|
||||
create: PropTypes.shape({
|
||||
permission: PropTypes.bool,
|
||||
@@ -204,6 +247,7 @@ DefaultEditView.propTypes = {
|
||||
}),
|
||||
fields: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
initialState: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
export default DefaultEditView;
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, {
|
||||
useState, useRef, useEffect, useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useFieldType from '../../useFieldType';
|
||||
import Button from '../../../elements/Button';
|
||||
import FileDetails from '../../../elements/FileDetails';
|
||||
import Error from '../../Error';
|
||||
import useFieldType from '../../../../forms/useFieldType';
|
||||
import Button from '../../../../elements/Button';
|
||||
import FileDetails from '../../../../elements/FileDetails';
|
||||
import Error from '../../../../forms/Error';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -34,10 +34,10 @@ const File = (props) => {
|
||||
const [replacingFile, setReplacingFile] = useState(false);
|
||||
|
||||
const {
|
||||
initialData = {}, adminThumbnail, staticURL,
|
||||
data = {}, adminThumbnail, staticURL,
|
||||
} = props;
|
||||
|
||||
const { filename } = initialData;
|
||||
const { filename } = data;
|
||||
|
||||
const {
|
||||
value,
|
||||
@@ -111,7 +111,7 @@ const File = (props) => {
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return () => { };
|
||||
}, [handleDragIn, handleDragOut, handleDrop, dropRef]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,7 +122,7 @@ const File = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
setReplacingFile(false);
|
||||
}, [initialData]);
|
||||
}, [data]);
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -138,7 +138,7 @@ const File = (props) => {
|
||||
/>
|
||||
{(filename && !replacingFile) && (
|
||||
<FileDetails
|
||||
{...initialData}
|
||||
{...data}
|
||||
staticURL={staticURL}
|
||||
adminThumbnail={adminThumbnail}
|
||||
handleRemove={() => {
|
||||
@@ -167,7 +167,7 @@ const File = (props) => {
|
||||
</div>
|
||||
)}
|
||||
{!value && (
|
||||
<>
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`${baseClass}__drop-zone`}
|
||||
ref={dropRef}
|
||||
@@ -181,7 +181,7 @@ const File = (props) => {
|
||||
</Button>
|
||||
<span className={`${baseClass}__drag-label`}>or drag and drop a file here</span>
|
||||
</div>
|
||||
</>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -195,13 +195,13 @@ const File = (props) => {
|
||||
};
|
||||
|
||||
File.defaultProps = {
|
||||
initialData: undefined,
|
||||
data: undefined,
|
||||
adminThumbnail: undefined,
|
||||
};
|
||||
|
||||
File.propTypes = {
|
||||
fieldTypes: PropTypes.shape({}).isRequired,
|
||||
initialData: PropTypes.shape({
|
||||
data: PropTypes.shape({
|
||||
filename: PropTypes.string,
|
||||
mimeType: PropTypes.string,
|
||||
filesize: PropTypes.number,
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
@import '../../../../../scss/styles.scss';
|
||||
|
||||
.file-field {
|
||||
position: relative;
|
||||
@@ -5,21 +5,14 @@ import config from 'payload/config';
|
||||
import { useStepNav } from '../../../elements/StepNav';
|
||||
import usePayloadAPI from '../../../../hooks/usePayloadAPI';
|
||||
import { useUser } from '../../../data/User';
|
||||
import formatFields from './formatFields';
|
||||
|
||||
import RenderCustomComponent from '../../../utilities/RenderCustomComponent';
|
||||
import DefaultEdit from './Default';
|
||||
import buildStateFromSchema from '../../../forms/Form/buildStateFromSchema';
|
||||
|
||||
const { serverURL, routes: { admin, api } } = config;
|
||||
|
||||
const EditView = (props) => {
|
||||
const { params: { id } = {} } = useRouteMatch();
|
||||
const { state: locationState } = useLocation();
|
||||
const history = useHistory();
|
||||
const { setStepNav } = useStepNav();
|
||||
const [fields, setFields] = useState([]);
|
||||
const { permissions } = useUser();
|
||||
|
||||
const { collection, isEditing } = props;
|
||||
|
||||
const {
|
||||
@@ -27,9 +20,20 @@ const EditView = (props) => {
|
||||
labels: {
|
||||
plural: pluralLabel,
|
||||
},
|
||||
useAsTitle,
|
||||
admin: {
|
||||
useAsTitle,
|
||||
},
|
||||
fields,
|
||||
auth,
|
||||
} = collection;
|
||||
|
||||
const { params: { id } = {} } = useRouteMatch();
|
||||
const { state: locationState } = useLocation();
|
||||
const history = useHistory();
|
||||
const { setStepNav } = useStepNav();
|
||||
const [initialState, setInitialState] = useState({});
|
||||
const { permissions } = useUser();
|
||||
|
||||
const onSave = (json) => {
|
||||
history.push(`${admin}/collections/${collection.slug}/${json?.doc?.id}`, {
|
||||
status: {
|
||||
@@ -40,9 +44,9 @@ const EditView = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const [{ data }] = usePayloadAPI(
|
||||
const [{ data, isLoading }] = usePayloadAPI(
|
||||
(isEditing ? `${serverURL}${api}/${slug}/${id}` : null),
|
||||
{ initialParams: { 'fallback-locale': 'null' } },
|
||||
{ initialParams: { 'fallback-locale': 'null', depth: 0 } },
|
||||
);
|
||||
|
||||
const dataToRender = locationState?.data || data;
|
||||
@@ -67,21 +71,39 @@ const EditView = (props) => {
|
||||
}, [setStepNav, isEditing, pluralLabel, dataToRender, slug, useAsTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
setFields(formatFields(collection, isEditing));
|
||||
}, [collection, isEditing]);
|
||||
const awaitInitialState = async () => {
|
||||
const state = await buildStateFromSchema(fields, dataToRender);
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [dataToRender, fields]);
|
||||
|
||||
const collectionPermissions = permissions?.[slug];
|
||||
|
||||
const apiURL = `${serverURL}${api}/${slug}/${id}`;
|
||||
let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}?depth=0`;
|
||||
const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission);
|
||||
|
||||
if (auth && !isEditing) {
|
||||
action = `${action}/register`;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultEdit}
|
||||
path={`${slug}.views.Edit`}
|
||||
componentProps={{
|
||||
isLoading,
|
||||
data: dataToRender,
|
||||
collection: { ...collection, fields },
|
||||
collection,
|
||||
permissions: collectionPermissions,
|
||||
isEditing,
|
||||
onSave,
|
||||
initialState,
|
||||
hasSavePermission,
|
||||
apiURL,
|
||||
action,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -98,9 +120,12 @@ EditView.propTypes = {
|
||||
singular: PropTypes.string,
|
||||
}),
|
||||
slug: PropTypes.string,
|
||||
useAsTitle: PropTypes.string,
|
||||
admin: PropTypes.shape({
|
||||
useAsTitle: PropTypes.string,
|
||||
}),
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
preview: PropTypes.func,
|
||||
auth: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__sidebar-fields {
|
||||
padding-right: $baseline;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin: 0;
|
||||
padding-top: $baseline;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user