feat: builds revisions list view
This commit is contained in:
@@ -204,6 +204,27 @@ const Routes = () => {
|
||||
return null;
|
||||
})}
|
||||
|
||||
{collections.map((collection) => {
|
||||
if (collection.revisions) {
|
||||
return (
|
||||
<Route
|
||||
key={`${collection.slug}-view-revision`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id/revisions/:revisionID`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.readRevisions?.permission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
{globals && globals.map((global) => (
|
||||
<Route
|
||||
key={`${global.slug}`}
|
||||
|
||||
11
src/admin/components/elements/IDLabel/index.scss
Normal file
11
src/admin/components/elements/IDLabel/index.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.id-label {
|
||||
font-size: base(.75);
|
||||
font-weight: normal;
|
||||
color: $color-gray;
|
||||
background: $color-background-gray;
|
||||
padding: base(.25) base(.5);
|
||||
border-radius: $style-radius-m;
|
||||
display: inline-flex;
|
||||
}
|
||||
14
src/admin/components/elements/IDLabel/index.tsx
Normal file
14
src/admin/components/elements/IDLabel/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'id-label';
|
||||
|
||||
const IDLabel: React.FC<{ id: string }> = ({ id }) => (
|
||||
<div className={baseClass}>
|
||||
ID:
|
||||
{id}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default IDLabel;
|
||||
@@ -1,12 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import { Props } from './types';
|
||||
import useTitle from '../../../hooks/useTitle';
|
||||
|
||||
import './index.scss';
|
||||
import IDLabel from '../IDLabel';
|
||||
|
||||
const baseClass = 'render-title';
|
||||
|
||||
@@ -25,18 +24,14 @@ const RenderTitle : React.FC<Props> = (props) => {
|
||||
|
||||
const idAsTitle = title === data?.id;
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
idAsTitle && `${baseClass}--id-as-title`,
|
||||
].filter(Boolean).join(' ');
|
||||
if (idAsTitle) {
|
||||
return (
|
||||
<IDLabel id={data?.id} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={classes}>
|
||||
{idAsTitle && (
|
||||
<Fragment>
|
||||
ID:
|
||||
</Fragment>
|
||||
)}
|
||||
<span className={baseClass}>
|
||||
{title}
|
||||
</span>
|
||||
);
|
||||
|
||||
64
src/admin/components/views/collections/Revisions/columns.tsx
Normal file
64
src/admin/components/views/collections/Revisions/columns.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { Link, useRouteMatch } from 'react-router-dom';
|
||||
import { useConfig } from '@payloadcms/config-provider';
|
||||
import format from 'date-fns/format';
|
||||
import { Column } from '../../../elements/Table/types';
|
||||
import SortColumn from '../../../elements/SortColumn';
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
|
||||
type CreatedAtCellProps = {
|
||||
id: string
|
||||
date: string
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
|
||||
const CreatedAtCell: React.FC<CreatedAtCellProps> = ({ collection, id, date }) => {
|
||||
const { routes: { admin }, admin: { dateFormat } } = useConfig();
|
||||
const { params: { id: docID } } = useRouteMatch<{ id: string }>();
|
||||
|
||||
return (
|
||||
<Link to={`${admin}/collections/${collection.slug}/${docID}/revisions/${id}`}>
|
||||
{date && format(new Date(date), dateFormat)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const IDCell: React.FC = ({ children }) => (
|
||||
<span>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const getColumns = (collection: SanitizedCollectionConfig): Column[] => [
|
||||
{
|
||||
accessor: 'createdAt',
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
label="Created At"
|
||||
name="createdAt"
|
||||
/>
|
||||
),
|
||||
renderCell: (row, data) => (
|
||||
<CreatedAtCell
|
||||
collection={collection}
|
||||
id={row?.id}
|
||||
date={data}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: 'id',
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
label="ID"
|
||||
disable
|
||||
name="id"
|
||||
/>
|
||||
),
|
||||
renderCell: (row, data) => <IDCell>{data}</IDCell>,
|
||||
},
|
||||
},
|
||||
];
|
||||
65
src/admin/components/views/collections/Revisions/index.scss
Normal file
65
src/admin/components/views/collections/Revisions/index.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
@import '../../../../scss/styles';
|
||||
|
||||
.collection-revisions {
|
||||
width: 100%;
|
||||
margin-bottom: base(2);
|
||||
|
||||
&__wrap {
|
||||
padding: base(3);
|
||||
margin-right: base(2);
|
||||
background: white;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.table {
|
||||
table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__page-controls {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.paginator {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-right: base(1);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__wrap {
|
||||
padding: $baseline 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&__header,
|
||||
.table,
|
||||
&__page-controls {
|
||||
padding-left: $baseline;
|
||||
padding-right: $baseline;
|
||||
}
|
||||
|
||||
&__page-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.paginator {
|
||||
width: 100%;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useConfig } from '@payloadcms/config-provider';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import usePayloadAPI from '../../../../hooks/usePayloadAPI';
|
||||
import Eyebrow from '../../../elements/Eyebrow';
|
||||
@@ -8,25 +8,30 @@ import { useStepNav } from '../../../elements/StepNav';
|
||||
import { StepNavItem } from '../../../elements/StepNav/types';
|
||||
import Meta from '../../../utilities/Meta';
|
||||
import { Props } from './types';
|
||||
import IDLabel from '../../../elements/IDLabel';
|
||||
import { getColumns } from './columns';
|
||||
import Table from '../../../elements/Table';
|
||||
import Paginator from '../../../elements/Paginator';
|
||||
import PerPage from '../../../elements/PerPage';
|
||||
|
||||
const baseClass = 'revisions';
|
||||
import './index.scss';
|
||||
import { useSearchParams } from '../../../utilities/SearchParams';
|
||||
|
||||
const baseClass = 'collection-revisions';
|
||||
|
||||
const Revisions: React.FC<Props> = ({ collection }) => {
|
||||
const { serverURL, routes: { admin } } = useConfig();
|
||||
const { serverURL, routes: { admin, api } } = useConfig();
|
||||
const { setStepNav } = useStepNav();
|
||||
const { params: { id } } = useRouteMatch<{ id: string }>();
|
||||
const [tableColumns] = useState(() => getColumns(collection));
|
||||
const [fetchURL, setFetchURL] = useState('');
|
||||
const { page, sort, limit } = useSearchParams();
|
||||
|
||||
const [{ data: doc }] = usePayloadAPI(`${serverURL}/api/${collection.slug}/${id}`);
|
||||
|
||||
const [{ data: revisionsData, isLoading: isLoadingRevisions }] = usePayloadAPI(`${serverURL}/api/${collection.slug}/revisions`, {
|
||||
initialParams: {
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const [{ data: revisionsData, isLoading: isLoadingRevisions }, { setParams }] = usePayloadAPI(fetchURL);
|
||||
|
||||
const useAsTitle = collection.admin.useAsTitle || 'id';
|
||||
|
||||
useEffect(() => {
|
||||
const nav: StepNavItem[] = [
|
||||
@@ -35,7 +40,7 @@ const Revisions: React.FC<Props> = ({ collection }) => {
|
||||
label: collection.labels.plural,
|
||||
},
|
||||
{
|
||||
label: doc ? doc[collection.admin.useAsTitle || 'id'] : '',
|
||||
label: doc ? doc[useAsTitle] : '',
|
||||
url: `${admin}/collections/${collection.slug}/${id}`,
|
||||
},
|
||||
{
|
||||
@@ -44,26 +49,94 @@ const Revisions: React.FC<Props> = ({ collection }) => {
|
||||
];
|
||||
|
||||
setStepNav(nav);
|
||||
}, [setStepNav, collection, doc, admin, id]);
|
||||
}, [setStepNav, collection, useAsTitle, doc, admin, id]);
|
||||
|
||||
useEffect(() => {
|
||||
const params = {
|
||||
depth: 1,
|
||||
page: undefined,
|
||||
sort: undefined,
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
limit,
|
||||
};
|
||||
|
||||
if (page) params.page = page;
|
||||
if (sort) params.sort = sort;
|
||||
|
||||
// Performance enhancement
|
||||
// Setting the Fetch URL this way
|
||||
// prevents a double-fetch
|
||||
setFetchURL(`${serverURL}${api}/${collection.slug}/revisions`);
|
||||
setParams(params);
|
||||
}, [setParams, page, sort, collection, limit, serverURL, api, id]);
|
||||
|
||||
const useIDLabel = doc[useAsTitle] === doc?.id;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Meta
|
||||
title={`Revisions - ${doc[collection.admin.useAsTitle] ? doc[collection.admin.useAsTitle] : doc?.id} - ${collection.labels.singular}`}
|
||||
description={`Viewing revisions for the ${collection.labels.singular} ${doc[collection.admin.useAsTitle] ? doc[collection.admin.useAsTitle] : doc?.id}`}
|
||||
title={`Revisions - ${doc[useAsTitle]} - ${collection.labels.singular}`}
|
||||
description={`Viewing revisions for the ${collection.labels.singular} ${doc[useAsTitle]}`}
|
||||
keywords={`Revisions, ${collection.labels.singular}, Payload, CMS`}
|
||||
/>
|
||||
<Eyebrow />
|
||||
|
||||
{isLoadingRevisions && (
|
||||
<Loading />
|
||||
)}
|
||||
{revisionsData?.docs && (
|
||||
<React.Fragment>
|
||||
<h1>Revisions</h1>
|
||||
{revisionsData?.docs?.length}
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
Showing revisions for:
|
||||
{useIDLabel && (
|
||||
<IDLabel id={doc?.id} />
|
||||
)}
|
||||
{!useIDLabel && (
|
||||
<h1>
|
||||
{doc[useAsTitle]}
|
||||
</h1>
|
||||
)}
|
||||
</header>
|
||||
{isLoadingRevisions && (
|
||||
<Loading />
|
||||
)}
|
||||
{revisionsData?.docs && (
|
||||
<React.Fragment>
|
||||
<Table
|
||||
data={revisionsData?.docs}
|
||||
columns={tableColumns}
|
||||
/>
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Paginator
|
||||
limit={revisionsData.limit}
|
||||
totalPages={revisionsData.totalPages}
|
||||
page={revisionsData.page}
|
||||
hasPrevPage={revisionsData.hasPrevPage}
|
||||
hasNextPage={revisionsData.hasNextPage}
|
||||
prevPage={revisionsData.prevPage}
|
||||
nextPage={revisionsData.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
/>
|
||||
{revisionsData?.totalDocs > 0 && (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{(revisionsData.page * revisionsData.limit) - (revisionsData.limit - 1)}
|
||||
-
|
||||
{revisionsData.totalPages > 1 && revisionsData.totalPages !== revisionsData.page ? (revisionsData.limit * revisionsData.page) : revisionsData.totalDocs}
|
||||
{' '}
|
||||
of
|
||||
{' '}
|
||||
{revisionsData.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
collection={collection}
|
||||
limit={limit ? Number(limit) : 10}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user