Merge branch 'master' of github.com:trouble/payload

This commit is contained in:
Jarrod Flesch
2020-03-27 14:13:43 -04:00
23 changed files with 616 additions and 182 deletions

View File

@@ -15,6 +15,7 @@ import Edit from './views/collections/Edit';
import EditGlobal from './views/globals/Edit';
import { requests } from '../api';
import customComponents from './custom-components';
import RedirectToLogin from './utilities/RedirectToLogin';
const Routes = () => {
const [initialized, setInitialized] = useState(null);
@@ -151,7 +152,7 @@ const Routes = () => {
</Switch>
);
}
return <Redirect to={`${match.url}/login`} />;
return <RedirectToLogin />;
}}
/>
</Switch>

View File

@@ -9,14 +9,26 @@ const cookies = new Cookies();
const Context = createContext({});
const UserProvider = ({ children }) => {
const cookieToken = cookies.get('token');
const [token, setToken] = useState('');
const [user, setUser] = useState(cookieToken ? jwtDecode(cookieToken) : null);
const [user, setUser] = useState(null);
useEffect(() => {
const cookieToken = cookies.get('token');
if (cookieToken) {
const decoded = jwtDecode(cookieToken);
if (decoded.exp > Date.now() / 1000) {
setUser(decoded);
}
}
}, []);
useEffect(() => {
if (token) {
setUser(jwtDecode(token));
cookies.set('token', token, { path: '/' });
const decoded = jwtDecode(token);
if (decoded.exp > Date.now() / 1000) {
setUser(decoded);
cookies.set('token', token, { path: '/' });
}
}
}, [token]);

View File

@@ -123,8 +123,6 @@ const Form = (props) => {
baseClass,
].filter(Boolean).join(' ');
// console.log(fields);
return (
<form
noValidate

View File

@@ -55,7 +55,9 @@ function fieldReducer(state, action) {
}
case 'ADD_ROW': {
const { rowIndex, name, fields } = action;
const {
rowIndex, name, fields, blockType,
} = action;
const { rowsFromState, remainingState } = splitRowsFromState(state, name);
// Get names of sub fields
@@ -70,6 +72,8 @@ function fieldReducer(state, action) {
};
}, {});
if (blockType) subFields.blockType = blockType;
// Add new object containing subfield names to rowsFromState array
rowsFromState.splice(rowIndex + 1, 0, subFields);
@@ -92,7 +96,7 @@ function fieldReducer(state, action) {
return {
...remainingState,
...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })),
...(flatten({ [name]: rowsFromState }, { filters: flattenFilters })),
};
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import { asModal } from '@trbl/react-modal';
const baseClass = 'flexible-add-row-modal';
const AddRowModal = (props) => {
const {
addRow,
blocks,
rowIndexBeingAdded,
closeAllModals,
} = props;
const handleAddRow = (blockType) => {
addRow(rowIndexBeingAdded, blockType);
closeAllModals();
};
return (
<div className={baseClass}>
<ul>
{blocks.map((block, i) => {
return (
<li key={i}>
<button
onClick={() => handleAddRow(block.slug)}
type="button"
>
{block.labels.singular}
</button>
</li>
);
})}
</ul>
</div>
);
};
AddRowModal.defaultProps = {
rowIndexBeingAdded: null,
};
AddRowModal.propTypes = {
addRow: PropTypes.func.isRequired,
closeAllModals: PropTypes.func.isRequired,
blocks: PropTypes.arrayOf(
PropTypes.shape({
labels: PropTypes.shape({
singular: PropTypes.string,
}),
previewImage: PropTypes.string,
slug: PropTypes.string,
}),
).isRequired,
rowIndexBeingAdded: PropTypes.number,
};
export default asModal(AddRowModal);

View File

@@ -0,0 +1,125 @@
import React from 'react';
import PropTypes from 'prop-types';
import AnimateHeight from 'react-animate-height';
import { Draggable } from 'react-beautiful-dnd';
// eslint-disable-next-line import/no-cycle
import RenderFields from '../../../RenderFields';
import IconButton from '../../../../controls/IconButton';
import './index.scss';
const baseClass = 'flexible-row';
const FlexibleRow = (props) => {
const {
addRow,
removeRow,
rowIndex,
parentName,
block,
defaultValue,
dispatchCollapsibleStates,
collapsibleStates,
} = props;
const handleCollapseClick = () => {
dispatchCollapsibleStates({
type: 'UPDATE_COLLAPSIBLE_STATUS',
collapsibleIndex: rowIndex,
});
};
return (
<Draggable
draggableId={`row-${rowIndex}`}
index={rowIndex}
>
{(providedDrag) => {
return (
<div
ref={providedDrag.innerRef}
className={baseClass}
{...providedDrag.draggableProps}
>
<div className={`${baseClass}__header`}>
<div
{...providedDrag.dragHandleProps}
className={`${baseClass}__header__drag-handle`}
/>
<div className={`${baseClass}__header__row-index`}>
{`${block.labels.singular} ${rowIndex + 1 > 9 ? rowIndex + 1 : `0${rowIndex + 1}`}`}
</div>
<div className={`${baseClass}__header__controls`}>
<IconButton
iconName="crosshair"
onClick={addRow}
size="small"
/>
<IconButton
iconName="crossOut"
onClick={removeRow}
size="small"
/>
<IconButton
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${collapsibleStates[rowIndex] ? 'open' : 'closed'}`}
iconName="arrow"
onClick={handleCollapseClick}
size="small"
/>
</div>
</div>
<AnimateHeight
className={`${baseClass}__content`}
height={collapsibleStates[rowIndex] ? 'auto' : 0}
duration={0}
>
<RenderFields
key={rowIndex}
fields={block.fields.map((field) => {
const fieldName = `${parentName}.${rowIndex}.${field.name}`;
return ({
...field,
name: fieldName,
defaultValue: defaultValue?.[field.name],
});
})}
/>
</AnimateHeight>
</div>
);
}}
</Draggable>
);
};
FlexibleRow.defaultProps = {
defaultValue: null,
collapsibleStates: [],
};
FlexibleRow.propTypes = {
block: PropTypes.shape({
labels: PropTypes.shape({
singular: PropTypes.string,
}),
fields: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
slug: PropTypes.string,
}).isRequired,
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
rowIndex: PropTypes.number.isRequired,
parentName: PropTypes.string.isRequired,
fieldState: PropTypes.shape({}).isRequired,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.shape({})]),
dispatchCollapsibleStates: PropTypes.func.isRequired,
collapsibleStates: PropTypes.arrayOf(PropTypes.bool),
};
export default FlexibleRow;

View File

@@ -0,0 +1,90 @@
@import '../../../../../scss/styles.scss';
.flexible-row {
background: $light-gray;
flex-direction: column;
position: relative;
margin-top: base(.5);
&:hover {
@include shadow-sm;
}
&__collapse__icon {
&--open {
svg {
transform: rotate(90deg);
}
}
&--closed {
svg {
transform: rotate(-90deg);
}
}
}
&__header {
padding: base(.75) base(1);
display: flex;
align-items: center;
position: relative;
&__drag-handle {
width: 100%;
height: 100%;
position: absolute;
left: 0;
z-index: 1;
}
// elements above the drag handle
&__controls,
&__header__row-index {
position: relative;
z-index: 2;
}
&__row-index {
font-family: $font-body;
font-size: base(.5);
}
&__heading {
font-family: $font-body;
margin: 0;
font-size: base(.65);
}
&__controls {
margin-left: auto;
.btn {
margin-top: 0;
margin-bottom: 0;
}
.icon-button--crossOut,
.icon-button--crosshair {
margin-right: base(.25);
}
.icon-button--crosshair {
border-color: $primary;
@include color-svg($primary);
&:hover {
background: lighten($primary, 50%);
}
}
}
}
&__content {
box-shadow: inset 0px 1px 0px white;
> div {
padding: base(.75) base(1);
}
}
}

View File

@@ -0,0 +1,165 @@
import React, {
useContext, useEffect, useReducer, useState, Fragment,
} from 'react';
import PropTypes from 'prop-types';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { ModalContext } from '@trbl/react-modal';
import FormContext from '../../Form/Context';
import Section from '../../../layout/Section';
import FlexibleRow from './FlexibleRow'; // eslint-disable-line import/no-cycle
import AddRowModal from './AddRowModal';
import collapsibleReducer from './reducer';
import './index.scss';
const baseClass = 'field-type flexible';
const Flexible = (props) => {
const {
label,
name,
blocks,
defaultValue,
} = props;
const { toggle: toggleModal, closeAll: closeAllModals } = useContext(ModalContext);
const [rowIndexBeingAdded, setRowIndexBeingAdded] = useState(null);
const [rowCount, setRowCount] = useState(0);
const [collapsibleStates, dispatchCollapsibleStates] = useReducer(collapsibleReducer, []);
const formContext = useContext(FormContext);
const modalSlug = `flexible-${name}`;
const { fields: fieldState, dispatchFields } = formContext;
const addRow = (rowIndex, blockType) => {
const blockToAdd = blocks.find(block => block.slug === blockType);
dispatchFields({
type: 'ADD_ROW', rowIndex, name, fields: blockToAdd.fields, blockType,
});
dispatchCollapsibleStates({
type: 'ADD_COLLAPSIBLE', collapsibleIndex: rowIndex,
});
setRowCount(rowCount + 1);
};
const removeRow = (rowIndex) => {
dispatchFields({
type: 'REMOVE_ROW', rowIndex, name,
});
dispatchCollapsibleStates({
type: 'REMOVE_COLLAPSIBLE',
collapsibleIndex: rowIndex,
});
setRowCount(rowCount - 1);
};
const moveRow = (moveFromIndex, moveToIndex) => {
dispatchFields({
type: 'MOVE_ROW', moveFromIndex, moveToIndex, name,
});
dispatchCollapsibleStates({
type: 'MOVE_COLLAPSIBLE', collapsibleIndex: moveFromIndex, moveToIndex,
});
};
useEffect(() => {
setRowCount(defaultValue.length);
dispatchCollapsibleStates({
type: 'SET_ALL_COLLAPSIBLES',
payload: Array.from(Array(defaultValue.length).keys()).reduce(acc => ([...acc, true]), []), // sets all collapsibles to open on first load
});
}, [defaultValue]);
const openAddRowModal = (rowIndex) => {
setRowIndexBeingAdded(rowIndex);
toggleModal(modalSlug);
};
const onDragEnd = (result) => {
if (!result.destination) return;
const sourceIndex = result.source.index;
const destinationIndex = result.destination.index;
moveRow(sourceIndex, destinationIndex);
};
return (
<Fragment>
<DragDropContext onDragEnd={onDragEnd}>
<div className={baseClass}>
<Section
heading={label}
className="flexible"
rowCount={rowCount}
addRow={() => openAddRowModal(0)}
useAddRowButton
>
<Droppable droppableId="flexible-drop">
{provided => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{rowCount !== 0
&& Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
const blockType = fieldState[`${name}.${rowIndex}.blockType`];
const blockToRender = blocks.find(block => block.slug === blockType);
return (
<FlexibleRow
key={rowIndex}
parentName={name}
addRow={() => openAddRowModal(rowIndex)}
removeRow={() => removeRow(rowIndex)}
rowIndex={rowIndex}
fieldState={fieldState}
block={blockToRender}
defaultValue={defaultValue[rowIndex]}
dispatchCollapsibleStates={dispatchCollapsibleStates}
collapsibleStates={collapsibleStates}
/>
);
})
}
{provided.placeholder}
</div>
)}
</Droppable>
</Section>
</div>
</DragDropContext>
<AddRowModal
closeAllModals={closeAllModals}
addRow={addRow}
rowIndexBeingAdded={rowIndexBeingAdded}
slug={modalSlug}
blocks={blocks}
/>
</Fragment>
);
};
Flexible.defaultProps = {
label: '',
defaultValue: [],
};
Flexible.propTypes = {
defaultValue: PropTypes.arrayOf(
PropTypes.shape({}),
),
blocks: PropTypes.arrayOf(
PropTypes.shape({}),
).isRequired,
label: PropTypes.string,
name: PropTypes.string.isRequired,
};
export default Flexible;

View File

@@ -0,0 +1,3 @@
.field-type.flexible {
background: white;
}

View File

@@ -0,0 +1,35 @@
const collapsibleReducer = (currentState, action) => {
const {
type, collapsibleIndex, moveToIndex, payload,
} = action;
const stateCopy = [...currentState];
const movingCollapsibleState = stateCopy[collapsibleIndex];
switch (type) {
case 'SET_ALL_COLLAPSIBLES':
return payload;
case 'ADD_COLLAPSIBLE':
stateCopy.splice(collapsibleIndex + 1, 0, true);
return stateCopy;
case 'REMOVE_COLLAPSIBLE':
stateCopy.splice(collapsibleIndex, 1);
return stateCopy;
case 'UPDATE_COLLAPSIBLE_STATUS':
stateCopy[collapsibleIndex] = !movingCollapsibleState;
return stateCopy;
case 'MOVE_COLLAPSIBLE':
stateCopy.splice(collapsibleIndex, 1);
stateCopy.splice(moveToIndex, 0, movingCollapsibleState);
return stateCopy;
default:
return currentState;
}
};
export default collapsibleReducer;

View File

@@ -101,7 +101,6 @@ RepeaterRow.propTypes = {
rowIndex: PropTypes.number.isRequired,
parentName: PropTypes.string.isRequired,
fields: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
fieldState: PropTypes.shape({}).isRequired,
rowCount: PropTypes.number,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.shape({})]),
dispatchCollapsibleStates: PropTypes.func.isRequired,

View File

@@ -94,23 +94,22 @@ const Repeater = (props) => {
{...provided.droppableProps}
>
{rowCount !== 0
&& Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
return (
<RepeaterRow
key={rowIndex}
parentName={name}
addRow={() => addRow(rowIndex)}
removeRow={() => removeRow(rowIndex)}
rowIndex={rowIndex}
fieldState={fieldState}
fields={fields}
rowCount={rowCount}
defaultValue={defaultValue[rowIndex]}
dispatchCollapsibleStates={dispatchCollapsibleStates}
collapsibleStates={collapsibleStates}
/>
);
})
&& Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
return (
<RepeaterRow
key={rowIndex}
parentName={name}
addRow={() => addRow(rowIndex)}
removeRow={() => removeRow(rowIndex)}
rowIndex={rowIndex}
fields={fields}
rowCount={rowCount}
defaultValue={defaultValue[rowIndex]}
dispatchCollapsibleStates={dispatchCollapsibleStates}
collapsibleStates={collapsibleStates}
/>
);
})
}
{provided.placeholder}
</div>

View File

@@ -6,6 +6,7 @@ import text from './Text';
import relationship from './Relationship';
import password from './Password';
import repeater from './Repeater';
import flexible from './Flexible';
import textarea from './Textarea';
import select from './Select';
import number from './Number';
@@ -18,6 +19,7 @@ export default {
text,
relationship,
// upload,
flexible,
number,
password,
repeater,

View File

@@ -26,7 +26,7 @@ const useFieldType = (options) => {
}, [name, required, dispatchFields, validate]);
useEffect(() => {
sendField(defaultValue);
if (defaultValue != null) sendField(defaultValue);
}, [defaultValue, sendField]);
const valid = formContext.fields[name] ? formContext.fields[name].valid : true;

View File

@@ -1,6 +1,7 @@
import React, { Suspense } from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { ModalProvider, ModalContainer } from '@trbl/react-modal';
import Loading from './views/Loading';
import { SearchParamsProvider } from './utilities/SearchParams';
import { LocaleProvider } from './utilities/Locale';
@@ -14,15 +15,21 @@ const Index = () => {
return (
<UserProvider>
<Router>
<StatusListProvider>
<SearchParamsProvider>
<LocaleProvider>
<Suspense fallback={<Loading />}>
<Routes />
</Suspense>
</LocaleProvider>
</SearchParamsProvider>
</StatusListProvider>
<ModalProvider
classPrefix="payload"
transTime={0}
>
<StatusListProvider>
<SearchParamsProvider>
<LocaleProvider>
<Suspense fallback={<Loading />}>
<Routes />
</Suspense>
</LocaleProvider>
</SearchParamsProvider>
</StatusListProvider>
<ModalContainer />
</ModalProvider>
</Router>
</UserProvider>
);

View File

@@ -1,102 +0,0 @@
///////////////////////////////////////////////////////
// Takes a modal component and
// a slug to match against a 'modal' URL param
///////////////////////////////////////////////////////
import React, { Component } from 'react';
import { createPortal } from 'react-dom';
import { withRouter } from 'react-router';
import queryString from 'qs';
import Close from '../../graphics/Close';
import Button from '../../controls/Button';
import './index.scss';
const asModal = (PassedComponent, modalSlug) => {
class AsModal extends Component {
constructor(props) {
super(props);
this.state = {
open: false,
el: null
}
}
bindEsc = event => {
if (event.keyCode === 27) {
const params = { ...this.props.searchParams };
delete params.modal;
this.props.history.push({
search: queryString.stringify(params)
})
}
}
isOpen = () => {
// Slug can come from either a HOC or from a prop
const slug = this.props.modalSlug ? this.props.modalSlug : modalSlug;
if (this.props.searchParams.modal === slug) {
return true;
}
return false;
}
componentDidMount() {
document.addEventListener('keydown', this.bindEsc, false);
if (this.isOpen()) {
this.setState({ open: true })
}
// Slug can come from either a HOC or from a prop
const slug = this.props.modalSlug ? this.props.modalSlug : modalSlug;
this.setState({
el: document.querySelector(`#${slug}`)
})
}
componentWillUnmount() {
document.removeEventListener('keydown', this.bindEsc, false);
}
componentDidUpdate(prevProps, prevState) {
let open = this.isOpen();
if (open !== prevState.open && open) {
this.setState({ open: true })
} else if (open !== prevState.open) {
this.setState({ open: false })
}
}
render() {
// Slug can come from either a HOC or from a prop
const slug = this.props.modalSlug ? this.props.modalSlug : modalSlug;
const modalDomNode = document.getElementById('portal');
return createPortal(
<div className={`modal${this.state.open ? ' open' : ''}`}>
<Button el="link" type="icon" className="close" to={{ search: '' }}>
<Close />
</Button>
<PassedComponent id={slug} {...this.props} isOpen={this.state.open} />
</div>,
modalDomNode
);
}
}
return withRouter(connect(mapStateToProps)(AsModal));
}
export default asModal;

View File

@@ -1,21 +0,0 @@
@import '../../../scss/styles.scss';
.modal {
transform: translateZ(0);
opacity: 0;
visibility: hidden;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
height: 100vh;
background-color: rgba(white, .96);
&.open {
opacity: 1;
visibility: visible;
z-index: 100;
}
}

View File

@@ -0,0 +1,23 @@
import React, { useEffect } from 'react';
import {
Redirect,
} from 'react-router-dom';
import { useStatusList } from '../../modules/Status';
import config from '../../../config/sanitizedClientConfig';
const RedirectToLogin = () => {
const { addStatus } = useStatusList();
useEffect(() => {
addStatus({
message: 'You need to log in to be able to do that.',
type: 'error',
});
}, [addStatus]);
return (
<Redirect to={`${config.routes.admin}/login`} />
);
};
export default RedirectToLogin;