Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin UI: cleaned up react-router usage to match latest recommendations #3029

Merged
merged 11 commits into from
May 25, 2020
5 changes: 5 additions & 0 deletions .changeset/wild-mirrors-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystonejs/app-admin-ui': patch
---

Cleaned up react-router usage to match latest recommendations.
177 changes: 67 additions & 110 deletions packages/app-admin-ui/client/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { Fragment, Suspense, useMemo } from 'react';
import React, { Suspense, useMemo } from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/react-hooks';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
import { BrowserRouter, Redirect, Route, Switch, useParams } from 'react-router-dom';
import { ToastProvider } from 'react-toast-notifications';
import { Global } from '@emotion/core';

import { globalStyles } from '@arch-ui/theme';
import { InfoIcon } from '@arch-ui/icons';

import { initApolloClient } from './apolloClient';
import Nav from './components/Nav';
Expand All @@ -15,8 +14,7 @@ import ConnectivityListener from './components/ConnectivityListener';
import KeyboardShortcuts from './components/KeyboardShortcuts';
import PageLoading from './components/PageLoading';
import ToastContainer from './components/ToastContainer';
import DocTitle from './components/DocTitle';
import PageError from './components/PageError';

import { AdminMetaProvider, useAdminMeta } from './providers/AdminMeta';
import { ListProvider } from './providers/List';
import { HooksProvider } from './providers/Hooks';
Expand All @@ -26,96 +24,74 @@ import ListPage from './pages/List';
import ListNotFoundPage from './pages/ListNotFound';
import ItemPage from './pages/Item';
import InvalidRoutePage from './pages/InvalidRoute';
import NoListsPage from './pages/NoLists';
import SignoutPage from './pages/Signout';

export const KeystoneAdminUI = () => {
const {
listKeys,
getListByPath,
adminPath,
signinPath,
signoutPath,
apiPath,
pages,
hooks,
} = useAdminMeta();
const HomePageWrapper = () => {
const { listKeys } = useAdminMeta();

const apolloClient = useMemo(() => initApolloClient({ uri: apiPath }), [apiPath]);
if (listKeys.length === 0) {
return <NoListsPage />;
}

return <HomePage />;
};

const ListPageWrapper = () => {
const { getListByPath, adminPath } = useAdminMeta();
const { listKey } = useParams();

// TODO: Permission query to show/hide a list from the menu
const list = getListByPath(listKey);
if (!list) {
return <ListNotFoundPage listKey={listKey} />;
}

return (
<ListProvider key={listKey} list={list}>
<Switch>
<Route exact path={`${adminPath}/:list`} children={<ListPage />} />
<Route exact path={`${adminPath}/:list/:itemId`} children={<ItemPage />} />
<Route children={<InvalidRoutePage />} />,
</Switch>
</ListProvider>
);
};

const MainPageWrapper = () => {
const { adminPath, pages } = useAdminMeta();

const routes = [
const customRoutes = [
...pages
.filter(page => typeof page.path === 'string')
.map(page => {
const Page = page.component;
const config = page.config || {};
return {
path: `${adminPath}/${page.path}`,
component: () => {
return <Page {...config} />;
},
exact: true,
};
}),
{
path: `${adminPath}`,
component: () => {
if (listKeys.length === 0) {
return (
<Fragment>
<DocTitle title="Home" />
<PageError icon={InfoIcon}>
<p>
No lists defined.{' '}
<a target="_blank" href="https://keystonejs.com/tutorials/add-lists">
Get started by creating your first list.
</a>
</p>
</PageError>
</Fragment>
);
}

return <HomePage />;
},
exact: true,
},
{
path: `${adminPath}/:listKey`,
component: ({
match: {
params: { listKey },
},
}) => {
// TODO: Permission query to show/hide a list from the
// menu
const list = getListByPath(listKey);
if (!list) {
return <ListNotFoundPage listKey={listKey} />;
}

return (
<ListProvider key={listKey} list={list}>
<Switch>
<Route exact path={`${adminPath}/:list`} render={() => <ListPage key={listKey} />} />
,
<Route
exact
path={`${adminPath}/:list/:itemId`}
render={({
match: {
params: { itemId },
},
}) => <ItemPage key={`${listKey}-${itemId}`} itemId={itemId} />}
/>
,
<Route render={() => <InvalidRoutePage />} />,
</Switch>
</ListProvider>
);
},
},
.filter(({ path }) => typeof path === 'string')
.map(({ component: Page, config = {}, path }) => ({
path: `${adminPath}/${path}`,
children: <Page {...config} />,
})),
];

return (
<ScrollToTop>
<Nav>
<Suspense fallback={<PageLoading />}>
<Switch>
{customRoutes.map(({ path, children }) => (
<Route exact key={path} path={path} children={children} />
))}
<Route exact path={adminPath} children={<HomePageWrapper />} />
<Route path={`${adminPath}/:listKey`} children={<ListPageWrapper />} />
</Switch>
</Suspense>
</Nav>
</ScrollToTop>
);
};

export const KeystoneAdminUI = () => {
const { adminPath, signinPath, signoutPath, apiPath, hooks } = useAdminMeta();

const apolloClient = useMemo(() => initApolloClient({ uri: apiPath }), [apiPath]);

return (
<HooksProvider hooks={hooks}>
<ApolloProvider client={apolloClient}>
Expand All @@ -125,28 +101,9 @@ export const KeystoneAdminUI = () => {
<Global styles={globalStyles} />
<BrowserRouter>
<Switch>
<Route exact path={signinPath}>
<Redirect to={adminPath} />
</Route>
<Route exact path={signoutPath} render={() => <SignoutPage />} />
<Route>
<ScrollToTop>
<Nav>
<Suspense fallback={<PageLoading />}>
<Switch>
{routes.map(route => (
<Route
exact={route.exact ? true : false}
key={route.path}
path={route.path}
render={route.component}
/>
))}
</Switch>
</Suspense>
</Nav>
</ScrollToTop>
</Route>
<Route exact path={signinPath} children={<Redirect to={adminPath} />} />
<Route exact path={signoutPath} children={<SignoutPage />} />
<Route children={<MainPageWrapper />} />
</Switch>
</BrowserRouter>
</ToastProvider>
Expand Down
54 changes: 25 additions & 29 deletions packages/app-admin-ui/client/pages/Home/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment, useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { useQuery } from '@apollo/react-hooks';

import { Container, Grid, Cell } from '@arch-ui/layout';
Expand Down Expand Up @@ -81,36 +81,32 @@ const Homepage = () => {
);
}

// NOTE: `loading` is intentionally omitted here
// the display of a loading indicator for counts is deferred to the
// list component so we don't block rendering the lists immediately
// to the user.
return (
<Fragment>
<main>
<DocTitle title="Home" />
{
// NOTE: `loading` is intentionally omitted here
// the display of a loading indicator for counts is deferred to the
// list component so we don't block rendering the lists immediately
// to the user.
}
<main>
<Container>
<HeaderInset>
<PageTitle>Dashboard</PageTitle>
</HeaderInset>
<Grid ref={measureElement} gap={16}>
{allowedLists.map(list => {
const { key, path } = list;
const meta = data && data[list.gqlNames.listQueryMetaName];
return (
<ListProvider list={list} key={key}>
<Cell width={cellWidth}>
<Box to={`${adminPath}/${path}`} meta={meta} />
</Cell>
</ListProvider>
);
})}
</Grid>
</Container>
</main>
</Fragment>
<Container>
<HeaderInset>
<PageTitle>Dashboard</PageTitle>
</HeaderInset>
<Grid ref={measureElement} gap={16}>
{allowedLists.map(list => {
const { key, path } = list;
const meta = data && data[list.gqlNames.listQueryMetaName];
return (
<ListProvider list={list} key={key}>
<Cell width={cellWidth}>
<Box to={`${adminPath}/${path}`} meta={meta} />
</Cell>
</ListProvider>
);
})}
</Grid>
</Container>
</main>
);
};

Expand Down
5 changes: 3 additions & 2 deletions packages/app-admin-ui/client/pages/Item/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { jsx } from '@emotion/core';
import { Fragment, Suspense, useMemo, useCallback, useState, useRef, useEffect } from 'react';
import { useMutation, useQuery } from '@apollo/react-hooks';
import { useHistory } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import { useToasts } from 'react-toast-notifications';
import memoizeOne from 'memoize-one';

Expand Down Expand Up @@ -336,7 +336,8 @@ const ItemNotFound = ({ errorMessage, list }) => (
</PageError>
);

const ItemPage = ({ itemId }) => {
const ItemPage = () => {
const { itemId } = useParams();
const { list } = useList();

// network-only because the data we mutate with is important for display
Expand Down
6 changes: 1 addition & 5 deletions packages/app-admin-ui/client/pages/ListNotFound.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ const ListNotFoundPage = ({ listKey }) => {

return (
<PageError icon={IssueOpenedIcon}>
<p>
The list &ldquo;
{listKey}
&rdquo; doesn&apos;t exist
</p>
<p>{`The list “${listKey}” does not exist`}</p>
<Button to={adminPath} variant="ghost">
Go Home
</Button>
Expand Down
22 changes: 22 additions & 0 deletions packages/app-admin-ui/client/pages/NoLists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

import DocTitle from '../components/DocTitle';
import PageError from '../components/PageError';

import { InfoIcon } from '@arch-ui/icons';

const NoListsPage = () => {
return (
<PageError icon={InfoIcon}>
<DocTitle title="Home" />
<p>
No lists defined.{' '}
<a target="_blank" href="https://keystonejs.com/tutorials/add-lists">
Get started by creating your first list.
</a>
</p>
</PageError>
);
};

export default NoListsPage;
4 changes: 2 additions & 2 deletions packages/app-admin-ui/client/public.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const Keystone = () => {
{authStrategy ? (
<BrowserRouter>
<Switch>
<Route exact path={signoutPath} render={() => <SignoutPage />} />
<Route render={() => <SigninPage />} />
<Route exact path={signoutPath} children={<SignoutPage />} />
<Route children={<SigninPage />} />
</Switch>
</BrowserRouter>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Access Control Lists > Admin UI', () => {

cy.visit(`admin/${slug}`);

cy.get('body').should('contain', `The list “${slug}” doesn't exist`);
cy.get('body').should('contain', `The list “${slug}” does not exist`);
});

it(`is visible when readable`, () => {
Expand All @@ -49,7 +49,7 @@ describe('Access Control Lists > Admin UI', () => {

cy.visit(`/admin/${slug}`);

cy.get('body').should('not.contain', `The list “${slug}” doesn't exist`);
cy.get('body').should('not.contain', `The list “${slug}” does not exist`);
cy.get('body h1').should('contain', prettyName);
});
});
Expand Down