feat: scaffolds admin revisions
This commit is contained in:
@@ -8,6 +8,9 @@ const RichText: CollectionConfig = {
|
||||
singular: 'Rich Text',
|
||||
plural: 'Rich Texts',
|
||||
},
|
||||
admin: {
|
||||
hideAPIURL: true,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import { requests } from '../api';
|
||||
import Loading from './elements/Loading';
|
||||
import StayLoggedIn from './modals/StayLoggedIn';
|
||||
import Unlicensed from './views/Unlicensed';
|
||||
import Revisions from './views/collections/Revisions';
|
||||
|
||||
const Dashboard = lazy(() => import('./views/Dashboard'));
|
||||
const ForgotPassword = lazy(() => import('./views/ForgotPassword'));
|
||||
@@ -177,6 +178,32 @@ const Routes = () => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{collections.map((collection) => {
|
||||
if (collection.revisions) {
|
||||
return (
|
||||
<Route
|
||||
key={`${collection.slug}-revisions`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id/revisions`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.readRevisions?.permission) {
|
||||
return (
|
||||
<Revisions
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
{globals && globals.map((global) => (
|
||||
<Route
|
||||
key={`${global.slug}`}
|
||||
|
||||
@@ -16,6 +16,7 @@ import fieldTypes from '../../../forms/field-types';
|
||||
import RenderTitle from '../../../elements/RenderTitle';
|
||||
import LeaveWithoutSaving from '../../../modals/LeaveWithoutSaving';
|
||||
import Auth from './Auth';
|
||||
import Revisions from './Revisions';
|
||||
import Upload from './Upload';
|
||||
import { Props } from './types';
|
||||
|
||||
@@ -38,6 +39,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
apiURL,
|
||||
action,
|
||||
hasSavePermission,
|
||||
submissionCount,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@@ -47,7 +49,9 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
useAsTitle,
|
||||
disableDuplicate,
|
||||
preview,
|
||||
hideAPIURL,
|
||||
},
|
||||
revisions,
|
||||
timestamps,
|
||||
auth,
|
||||
upload,
|
||||
@@ -165,6 +169,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
{isEditing && (
|
||||
<ul className={`${baseClass}__meta`}>
|
||||
{!hideAPIURL && (
|
||||
<li className={`${baseClass}__api-url`}>
|
||||
<span className={`${baseClass}__label`}>
|
||||
API URL
|
||||
@@ -179,10 +184,21 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
{apiURL}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>ID</div>
|
||||
<div>{id}</div>
|
||||
</li>
|
||||
{revisions && (
|
||||
<li>
|
||||
<div className={`${baseClass}__label`}>Revisions</div>
|
||||
<Revisions
|
||||
submissionCount={submissionCount}
|
||||
collection={collection}
|
||||
id={id}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
{timestamps && (
|
||||
<React.Fragment>
|
||||
{data.updatedAt && (
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@import '../../../../../scss/styles.scss';
|
||||
|
||||
.revisions-count__button {
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useConfig } from '@payloadcms/config-provider';
|
||||
import React, { useEffect } from 'react';
|
||||
import Button from '../../../../elements/Button';
|
||||
import usePayloadAPI from '../../../../../hooks/usePayloadAPI';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'revisions-count';
|
||||
|
||||
const Revisions: React.FC<Props> = ({ collection, id, submissionCount }) => {
|
||||
const { serverURL, routes: { admin } } = useConfig();
|
||||
|
||||
const [{ data, isLoading }, { setParams }] = usePayloadAPI(`${serverURL}/api/${collection.slug}/revisions`, {
|
||||
initialParams: {
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (submissionCount) {
|
||||
setParams({
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
c: submissionCount,
|
||||
});
|
||||
}
|
||||
}, [setParams, submissionCount, id]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{(!isLoading && data?.docs) && (
|
||||
<React.Fragment>
|
||||
{data.docs.length === 0 && (
|
||||
<React.Fragment>
|
||||
No revisions found
|
||||
</React.Fragment>
|
||||
)}
|
||||
{data?.docs?.length > 0 && (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
className={`${baseClass}__button`}
|
||||
buttonStyle="none"
|
||||
el="link"
|
||||
to={`${admin}/collections/${collection.slug}/${id}/revisions`}
|
||||
>
|
||||
{data.docs.length}
|
||||
{' '}
|
||||
revision
|
||||
{data.docs.length > 1 && 's'}
|
||||
{' '}
|
||||
found
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{isLoading && (
|
||||
<React.Fragment>
|
||||
Loading revisions...
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Revisions;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
|
||||
export type Props = {
|
||||
collection: SanitizedCollectionConfig,
|
||||
id: string | number
|
||||
submissionCount: number
|
||||
}
|
||||
@@ -32,6 +32,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
} = {},
|
||||
} = collection;
|
||||
const [fields] = useState(() => formatFields(collection, isEditing));
|
||||
const [submissionCount, setSubmissionCount] = useState(0);
|
||||
|
||||
const locale = useLocale();
|
||||
const { serverURL, routes: { admin, api } } = useConfig();
|
||||
@@ -48,6 +49,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
} else {
|
||||
const state = await buildStateFromSchema(fields, json.doc);
|
||||
setInitialState(state);
|
||||
setSubmissionCount((count) => count + 1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,6 +111,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
DefaultComponent={DefaultEdit}
|
||||
CustomComponent={CustomEdit}
|
||||
componentProps={{
|
||||
submissionCount,
|
||||
isLoading,
|
||||
data: dataToRender,
|
||||
collection: { ...collection, fields },
|
||||
|
||||
@@ -17,4 +17,5 @@ export type Props = IndexProps & {
|
||||
apiURL: string
|
||||
action: string
|
||||
hasSavePermission: boolean
|
||||
submissionCount: number
|
||||
}
|
||||
|
||||
39
src/admin/components/views/collections/Revisions/index.tsx
Normal file
39
src/admin/components/views/collections/Revisions/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useConfig } from '@payloadcms/config-provider';
|
||||
import React from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import usePayloadAPI from '../../../../hooks/usePayloadAPI';
|
||||
import Loading from '../../../elements/Loading';
|
||||
import { Props } from './types';
|
||||
|
||||
const baseClass = 'revisions';
|
||||
|
||||
const Revisions: React.FC<Props> = ({ collection }) => {
|
||||
const { serverURL } = useConfig();
|
||||
const { params: { id } } = useRouteMatch<{ id: string }>();
|
||||
|
||||
const [{ data, isLoading }] = usePayloadAPI(`${serverURL}/api/${collection.slug}/revisions`, {
|
||||
initialParams: {
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{isLoading && (
|
||||
<Loading />
|
||||
)}
|
||||
{data?.docs && (
|
||||
<React.Fragment>
|
||||
<h1>Revisions</h1>
|
||||
{data?.docs?.length}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Revisions;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
|
||||
export type Props = {
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
@@ -36,6 +36,7 @@ const collectionSchema = joi.object().keys({
|
||||
}),
|
||||
preview: joi.func(),
|
||||
disableDuplicate: joi.bool(),
|
||||
hideAPIURL: joi.bool(),
|
||||
}),
|
||||
fields: joi.array(),
|
||||
hooks: joi.object({
|
||||
|
||||
@@ -128,6 +128,10 @@ export type CollectionAdminOptions = {
|
||||
*/
|
||||
description?: string | (() => string) | React.FC;
|
||||
disableDuplicate?: boolean;
|
||||
/**
|
||||
* Hide the API URL within the Edit view
|
||||
*/
|
||||
hideAPIURL?: boolean
|
||||
/**
|
||||
* Custom admin components
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ type Args = {
|
||||
maxPerDoc: number
|
||||
entityLabel: string
|
||||
entityType: 'global' | 'collection'
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export const enforceMaxRevisions = async ({
|
||||
@@ -16,9 +17,14 @@ export const enforceMaxRevisions = async ({
|
||||
maxPerDoc,
|
||||
entityLabel,
|
||||
entityType,
|
||||
id,
|
||||
}: Args): Promise<void> => {
|
||||
try {
|
||||
const oldestAllowedDoc = await Model.find().limit(1).skip(maxPerDoc).sort({ createdAt: -1 });
|
||||
const query: { parent?: string | number } = {};
|
||||
|
||||
if (id) query.parent = id;
|
||||
|
||||
const oldestAllowedDoc = await Model.find(query).limit(1).skip(maxPerDoc).sort({ createdAt: -1 });
|
||||
|
||||
if (oldestAllowedDoc?.[0]?.createdAt) {
|
||||
const deleteLessThan = oldestAllowedDoc[0].createdAt;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const saveCollectionRevision = async ({
|
||||
|
||||
if (config.revisions.maxPerDoc) {
|
||||
enforceMaxRevisions({
|
||||
id,
|
||||
payload: this,
|
||||
Model: RevisionsModel,
|
||||
entityLabel: config.labels.plural,
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"noEmit": false, /* Do not emit outputs. */
|
||||
"strict": false, /* Enable all strict type-checking options. */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": "",
|
||||
"paths": {
|
||||
"payload/config": [
|
||||
"src/config/types.ts"
|
||||
|
||||
Reference in New Issue
Block a user