revises APIError import structure, updates Localizer and StepNav

This commit is contained in:
James
2020-01-19 16:57:26 -05:00
parent 8f05e11743
commit 8beb457041
22 changed files with 391 additions and 293 deletions

View File

@@ -24,6 +24,13 @@ module.exports = {
"aspects": ["invalidHref", "preferButton"]
}],
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/label-has-for": [2, {
"components": ["Label"],
"required": {
"every": ["id"]
},
"allowChildren": false
}],
"react/no-array-index-key": 0,
"max-len": 0,
"react/no-danger": 0,

View File

@@ -8,6 +8,7 @@ module.exports = {
},
useAsTitle: 'email',
useAsUsername: 'email',
passwordIndex: 1,
policies: {
create: (req, res, next) => {
return next();

View File

@@ -21,18 +21,20 @@ const requests = {
post: (url, body) =>
fetch(`${url}`, {
method: 'post',
body,
body: JSON.stringify(body),
headers: {
...setJWT()
...setJWT(),
'Content-Type': 'application/json',
},
}),
put: (url, body) =>
fetch(`${url}`, {
method: 'put',
body,
body: JSON.stringify(body),
headers: {
...setJWT()
...setJWT(),
'Content-Type': 'application/json',
},
}),
};

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, Fragment } from 'react';
import Cookies from 'universal-cookie';
import {
Route, Switch, withRouter, Redirect,
@@ -56,67 +56,71 @@ const Routes = () => {
if (cookies.get('token')) {
return (
<DefaultTemplate>
<Route
path={`${match.url}/media-library`}
component={MediaLibrary}
/>
<Route
path={`${match.url}/create-user`}
component={CreateUser}
/>
<Route
path={`${match.url}/`}
exact
component={Dashboard}
/>
<Switch>
<Route
path={`${match.url}/media-library`}
component={MediaLibrary}
/>
<Route
path={`${match.url}/create-user`}
component={CreateUser}
/>
<Route
path={`${match.url}/`}
exact
component={Dashboard}
/>
{config.collections.map((collection) => {
const components = collection.components ? collection.components : {};
return (
<Switch key={collection.slug}>
<Route
path={`${match.url}/collections/${collection.slug}/create`}
exact
render={(routeProps) => {
return (
<Edit
{...routeProps}
collection={collection}
/>
);
}}
/>
{config.collections.map((collection) => {
const components = collection.components ? collection.components : {};
return (
<Fragment key={collection.slug}>
<Route
path={`${match.url}/collections/${collection.slug}/create`}
exact
render={(routeProps) => {
return (
<Edit
{...routeProps}
collection={collection}
/>
);
}}
/>
<Route
path={`${match.url}/collections/${collection.slug}/:id`}
exact
render={(routeProps) => {
return (
<Edit
{...routeProps}
collection={collection}
/>
);
}}
/>
<Route
path={`${match.url}/collections/${collection.slug}/:id`}
exact
render={(routeProps) => {
return (
<Edit
{...routeProps}
collection={collection}
/>
);
}}
/>
<Route
path={`${match.url}/collections/${collection.slug}`}
exact
render={(routeProps) => {
const ListComponent = components.List ? components.List : List;
return (
<ListComponent
{...routeProps}
collection={collection}
/>
);
}}
/>
</Switch>
);
})}
<Route
path={`${match.url}/collections/${collection.slug}`}
exact
render={(routeProps) => {
const ListComponent = components.List ? components.List : List;
return (
<ListComponent
{...routeProps}
collection={collection}
/>
);
}}
/>
</Fragment>
);
})}
<Route>
<h1>Not Found</h1>
</Route>
</Switch>
</DefaultTemplate>
);
}

View File

@@ -70,6 +70,8 @@ const Form = (props) => {
setProcessing(true);
console.log(data);
// Make the API call from the action
api.requests[method.toLowerCase()](action, data).then(
(res) => {

View File

@@ -1,117 +1,163 @@
import React, { Component } from 'react';
import FormContext from '../../Form/Context'
import React, { useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import FormContext from '../../Form/Context';
import Tooltip from '../../../modules/Tooltip';
import './index.scss';
const fieldType = (PassedComponent, type, validate, errors) => {
const baseClass = 'field-type';
class FieldType extends Component {
const asFieldType = (PassedComponent, type, validate, errors) => {
const FieldType = (props) => {
const formContext = useContext(FormContext);
constructor(props) {
super(props);
const {
name,
id,
value,
required,
initialValue,
valueOverride,
onChange,
} = props;
this.state = {
init: false
};
}
sendField(value) {
this.props.context.setValue({
name: this.props.name,
value: value,
valid: this.props.required && validate
? validate(value || '', this.props.type)
: true
const sendField = (valueToSend) => {
formContext.setValue({
name,
value: valueToSend,
valid: required && validate
? validate(valueToSend || '', type)
: true,
});
}
};
componentDidMount() {
let value = this.props.value ? this.props.value : '';
value = this.props.initialValue ? this.props.initialValue : value;
value = this.props.valueOverride ? this.props.valueOverride : value;
this.sendField(value);
useEffect(() => {
let valueToInitialize = value;
if (initialValue) valueToInitialize = initialValue;
if (valueOverride) valueToInitialize = valueOverride;
sendField(valueToInitialize);
}, []);
this.setState({
init: true
});
}
useEffect(() => {
sendField(valueOverride);
}, [valueOverride]);
componentDidUpdate(prevProps) {
if (prevProps.valueOverride !== this.props.valueOverride) {
this.sendField(this.props.valueOverride);
}
useEffect(() => {
sendField(initialValue);
}, [initialValue]);
if (prevProps.initialValue !== this.props.initialValue) {
this.sendField(this.props.initialValue);
}
}
const classList = [baseClass, type];
const valid = formContext.fields[name] ? formContext.fields[name].valid : true;
const showError = valid === false && formContext.submitted;
render() {
const valid = this.props.context.fields[this.props.name]
? this.props.context.fields[this.props.name].valid
: true;
if (showError) classList.push('error');
const showError = valid === false && this.props.context.submitted;
let valueToRender = formContext.fields[name] ? formContext.fields[name].value : '';
let className = `field-type ${type}${showError ? ' error' : ''}`;
// If valueOverride present, field is being controlled by state outside form
valueToRender = valueOverride || value;
let value = this.props.context.fields[this.props.name] ? this.props.context.fields[this.props.name].value : '';
const classes = classList.filter(Boolean).join(' ');
// If valueOverride present, field is being controlled by state outside form
value = this.props.valueOverride ? this.props.valueOverride : value;
return (
<PassedComponent {...this.props}
className={className}
value={value}
label={<Label {...this.props} />}
error={<Error showError={showError} type={this.props.type} />}
onChange={e => {
this.sendField(e.target.value);
this.props.onChange && this.props.onChange(e);
}} />
)
}
}
const Label = props => {
if (props.label) {
return (
<label htmlFor={props.id ? props.id : props.name}>
{props.label}
{props.required &&
<span className="required">*</span>
}
</label>
)
}
return null;
}
const Error = props => {
if (props.showError) {
return (
<Tooltip className="error-message">
{props.error && errors[props.error]}
{!props.error && errors}
</Tooltip>
)
}
return null;
}
const FieldTypeWithContext = props => {
return (
<FormContext.Consumer>
{context => <FieldType {...props} context={context} />}
</FormContext.Consumer>
<PassedComponent
{...props}
className={classes}
value={valueToRender}
label={(
<Label
htmlFor={id || name}
{...props}
/>
)}
error={(
<Error
showError={showError}
type={type}
/>
)}
onChange={(e) => {
sendField(e.target.value);
if (onChange && typeof onChange === 'function') onChange(e);
}}
/>
);
};
return FieldTypeWithContext;
}
FieldType.defaultProps = {
value: '',
required: false,
initialValue: '',
valueOverride: '',
onChange: null,
id: '',
};
export default fieldType;
FieldType.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string,
required: PropTypes.bool,
type: PropTypes.string.isRequired,
initialValue: PropTypes.string,
valueOverride: PropTypes.string,
onChange: PropTypes.func,
id: PropTypes.string,
};
const Label = (props) => {
const {
label, required, htmlFor,
} = props;
if (label) {
return (
<label htmlFor={htmlFor}>
{label}
{required
&& <span className="required">*</span>
}
</label>
);
}
return null;
};
Label.defaultProps = {
required: false,
};
Label.propTypes = {
label: PropTypes.string.isRequired,
htmlFor: PropTypes.string.isRequired,
required: PropTypes.bool,
};
const Error = (props) => {
const { error, showError } = props;
if (showError) {
return (
<Tooltip className="error-message">
{error && errors[error]}
{!error && errors}
</Tooltip>
);
}
return null;
};
Error.defaultProps = {
showError: false,
};
Error.propTypes = {
error: PropTypes.string.isRequired,
showError: PropTypes.bool,
};
return FieldType;
};
export default asFieldType;

View File

@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import Sidebar from '../Sidebar';
import StepNav from '../../modules/StepNav';
import StepNav, { StepNavProvider } from '../../modules/StepNav';
import Localizer from '../../modules/Localizer';
import './index.scss';
@@ -9,15 +10,24 @@ const DefaultTemplate = ({ children }) => {
return (
<div className="default-template">
<div className="wrap">
<Sidebar />
<div className="eyebrow">
<StepNav />
<Localizer />
</div>
{children}
<StepNavProvider>
<Sidebar />
<div className="eyebrow">
<StepNav />
<Localizer />
</div>
{children}
</StepNavProvider>
</div>
</div>
);
};
DefaultTemplate.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export default DefaultTemplate;

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { withRouter, NavLink, Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { useLocation, NavLink, Link } from 'react-router-dom';
import config from 'payload-config';
import Arrow from '../../graphics/Arrow';
@@ -8,11 +7,7 @@ import Icon from '../../graphics/Icon';
import './index.scss';
const mapState = state => ({
config: state.common.config,
});
const Sidebar = (props) => {
const Sidebar = () => {
const {
collections,
globals,
@@ -21,6 +16,8 @@ const Sidebar = (props) => {
},
} = config;
const location = useLocation();
return (
<aside className="sidebar">
<Link to="/">
@@ -37,7 +34,7 @@ const Sidebar = (props) => {
</NavLink>
{collections && Object.keys(collections).map((key, i) => {
const href = `${admin}/collections/${collections[key].slug}`;
const classes = props.location.pathname.indexOf(href) > -1
const classes = location.pathname.indexOf(href) > -1
? 'active'
: undefined;
@@ -57,7 +54,7 @@ const Sidebar = (props) => {
<nav>
{globals && globals.map((global, i) => {
const href = `${admin}/globals/${global.slug}`;
const classes = props.location.pathname.indexOf(href) > -1
const classes = location.pathname.indexOf(href) > -1
? 'active'
: undefined;
@@ -77,4 +74,4 @@ const Sidebar = (props) => {
);
};
export default withRouter(connect(mapState)(Sidebar));
export default Sidebar;

View File

@@ -1,66 +1,64 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link, withRouter } from 'react-router-dom';
import Arrow from '../../graphics/Arrow';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import config from 'payload-config';
import qs from 'qs';
import { useLocale } from '../../utilities/Locale';
import { useSearchParams } from '../../utilities/SearchParams';
import Arrow from '../../graphics/Arrow';
import './index.scss';
const mapState = state => ({
config: state.common.config,
locale: state.common.locale,
searchParams: state.common.searchParams
})
const baseClass = 'localizer';
class Localizer extends Component {
const Localizer = () => {
const [active, setActive] = useState(false);
const activeLocale = useLocale();
const searchParams = useSearchParams();
constructor() {
super();
const { locales } = config.localization ? config.localization : { locales: [] };
this.state = {
active: false
}
}
if (locales.length <= 1) return null;
toggleActive = () =>
this.setState({ active: !this.state.active })
const classes = [
baseClass,
active && `${baseClass}--active`,
].filter(Boolean).join(' ');
render() {
let locales = [];
return (
<div className={classes}>
<button
type="button"
onClick={() => setActive(!active)}
className={`${baseClass}__current`}
>
<Arrow />
{activeLocale}
</button>
<ul>
{locales.map((locale, i) => {
if (activeLocale === locale) return null;
if (this.props.config && this.props.config.localization) locales = this.props.config.localization.locales;
const newParams = {
...searchParams,
locale,
};
if (locales.length <= 1) return null;
const search = qs.stringify(newParams);
return (
<div className={`localizer${this.state.active ? ' active' : ''}`}>
<button onClick={this.toggleActive} className="current-locale">
<Arrow />{this.props.locale}
</button>
<ul>
{locales.map((locale, i) => {
return (
<li key={i}>
<Link
to={{ search }}
onClick={() => setActive(false)}
>
{locale}
</Link>
</li>
);
})}
</ul>
</div>
);
};
if (locale === this.props.locale) return null;
const newParams = {
...this.props.searchParams,
locale
};
const search = qs.stringify(newParams);
return (
<li key={i}>
<Link to={{ search }} onClick={this.toggleActive}>
{locale}
</Link>
</li>
);
})}
</ul>
</div>
)
}
}
export default withRouter(connect(mapState)(Localizer));
export default Localizer;

View File

@@ -3,7 +3,7 @@
.localizer {
position: relative;
.current-locale {
&__current {
@extend %uppercase-label;
@extend %btn-reset;
display: flex;
@@ -43,7 +43,7 @@
visibility: hidden;
}
&.active {
&--active {
svg {
transform: rotate(-90deg);
}

View File

@@ -72,8 +72,7 @@ const StatusList = () => {
export {
StatusListProvider,
StatusList,
useStatusList,
};
export default Context;
export default StatusList;

View File

@@ -1,36 +1,74 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import React, { useState, createContext, useContext } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Arrow from '../../graphics/Arrow';
import './index.scss';
const mapStateToProps = state => ({
nav: state.common.stepNav
});
const Context = createContext({});
class StepNav extends Component {
render() {
const dashboardLabel = <span>Dashboard</span>;
const StepNavProvider = ({ children }) => {
const [nav, setNav] = useState([]);
return (
<nav className="step-nav">
{this.props.nav.length > 0
? <Link to="/">{dashboardLabel}<Arrow /></Link>
: dashboardLabel
}
{this.props.nav.map((item, i) => {
const StepLabel = <span key={i}>{item.label}</span>;
return (
<Context.Provider value={{
nav,
setNav,
}}
>
{children}
</Context.Provider>
);
};
const Step = this.props.nav.length === i + 1
? StepLabel
: <Link to={item.url} key={i}>{StepLabel}<Arrow /></Link>;
StepNavProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
return Step;
})}
</nav>
);
}
}
const useStepNav = () => useContext(Context);
export default connect(mapStateToProps)(StepNav);
const StepNav = () => {
const dashboardLabel = <span>Dashboard</span>;
const { nav } = useStepNav();
return (
<nav className="step-nav">
{nav.length > 0
? (
<Link to="/">
{dashboardLabel}
<Arrow />
</Link>
)
: dashboardLabel
}
{nav.map((item, i) => {
const StepLabel = <span key={i}>{item.label}</span>;
const Step = nav.length === i + 1
? StepLabel
: (
<Link
to={item.url}
key={i}
>
{StepLabel}
<Arrow />
</Link>
);
return Step;
})}
</nav>
);
};
export {
StepNavProvider,
useStepNav,
};
export default StepNav;

View File

@@ -1,12 +1,12 @@
import React, { createContext, useContext } from 'react';
import config from 'payload-config';
import PropTypes from 'prop-types';
import searchParamsContext from '../SearchParams';
import { useSearchParams } from '../SearchParams';
const Context = createContext({});
export const LocaleProvider = ({ children }) => {
const searchParams = useContext(searchParamsContext);
const searchParams = useSearchParams();
let activeLocale = null;

View File

@@ -1,4 +1,4 @@
import React, { createContext } from 'react';
import React, { createContext, useContext } from 'react';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import qs from 'qs';
@@ -27,4 +27,4 @@ SearchParamsProvider.propTypes = {
]).isRequired,
};
export default Context;
export const useSearchParams = () => useContext(Context);

View File

@@ -1,22 +0,0 @@
import { Component } from 'react';
import { connect } from 'react-redux';
const mapDispatchToProps = dispatch => ({
set: nav => dispatch({ type: 'SET_STEP_NAV', payload: nav })
});
class SetStepNav extends Component {
componentDidMount() {
this.props.set(this.props.nav);
}
componentDidUpdate(prevProps) {
if (prevProps.nav !== this.props.nav) {
this.props.set(this.props.nav);
}
}
render() { return null; }
}
export default connect(null, mapDispatchToProps)(SetStepNav);

View File

@@ -14,9 +14,23 @@ const handleAjaxResponse = (res) => {
cookies.set('token', res.token, { path: '/' });
};
const passwordField = {
name: 'password',
label: 'Password',
type: 'password',
};
const baseClass = 'create-first-user';
const CreateFirstUser = () => {
const fields = [...config.user.fields];
if (config.user.passwordIndex) {
fields.splice(config.user.passwordIndex, 0, passwordField);
} else {
fields.push(passwordField);
}
return (
<ContentBlock className={baseClass}>
<div className={`${baseClass}__wrap`}>
@@ -25,9 +39,9 @@ const CreateFirstUser = () => {
<Form
handleAjaxResponse={handleAjaxResponse}
method="POST"
action="/first-user"
action="/first-register"
>
<RenderFields fields={config.user.fields} />
<RenderFields fields={fields} />
<FormSubmit>Create</FormSubmit>
</Form>
</div>

View File

@@ -19,7 +19,7 @@ class ExtendableError extends Error {
* Class representing an API error.
* @extends ExtendableError
*/
export class APIError extends ExtendableError {
class APIError extends ExtendableError {
/**
* Creates an API error.
* @param {string} message - Error message.
@@ -30,3 +30,5 @@ export class APIError extends ExtendableError {
super(message, status, isPublic);
}
}
export default APIError;

View File

@@ -1,4 +1,4 @@
import { APIError } from './APIError';
import APIError from './APIError';
export class DuplicateCollection extends APIError {
constructor(config) {

View File

@@ -1,4 +1,4 @@
import { APIError } from './APIError';
import APIError from './APIError';
export class DuplicateGlobal extends APIError {
constructor(config) {

View File

@@ -1,4 +1,4 @@
import { APIError } from './APIError';
import APIError from './APIError';
export class MissingCollectionLabel extends APIError {
constructor(config) {

View File

@@ -1,4 +1,4 @@
import { APIError } from './APIError';
import APIError from './APIError';
export class MissingGlobalLabel extends APIError {
constructor(config) {

View File

@@ -1,5 +1,5 @@
import httpStatus from 'http-status';
import { APIError } from './APIError';
import APIError from './APIError';
export class NotFound extends APIError {
constructor() {