feat: builds group and iterable diffs
This commit is contained in:
@@ -2,4 +2,5 @@
|
||||
|
||||
.field-diff-label {
|
||||
margin-bottom: base(.25);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
|
||||
.iterable-diff {
|
||||
margin-bottom: base(2);
|
||||
|
||||
&__locale-label {
|
||||
margin-right: base(.25);
|
||||
background: $color-background-gray;
|
||||
padding: base(.25);
|
||||
border-radius: $style-radius-m;
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
margin: base(.5) 0;
|
||||
padding-left: base(.5);
|
||||
border-left: $style-stroke-width-s solid $color-light-gray;
|
||||
}
|
||||
|
||||
&__no-rows {
|
||||
font-family: monospace;
|
||||
background-color: #fafbfc;
|
||||
padding: base(.125) 0;
|
||||
margin: base(.125) 0;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,103 @@
|
||||
import React from 'react';
|
||||
import RenderFieldsToDiff from '../..';
|
||||
import { Props } from '../types';
|
||||
import Label from '../../Label';
|
||||
import { ArrayField, BlockField, Field, fieldAffectsData } from '../../../../../../../fields/config/types';
|
||||
import getUniqueListBy from '../../../../../../../utilities/getUniqueListBy';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'iterable-diff';
|
||||
|
||||
type Props = {
|
||||
revision: string
|
||||
comparison: string
|
||||
}
|
||||
const Iterable: React.FC<Props & { field: ArrayField | BlockField }> = ({
|
||||
revision,
|
||||
comparison,
|
||||
permissions,
|
||||
field,
|
||||
locale,
|
||||
locales,
|
||||
fieldComponents,
|
||||
}) => {
|
||||
const revisionRowCount = Array.isArray(revision) ? revision.length : 0;
|
||||
const comparisonRowCount = Array.isArray(comparison) ? comparison.length : 0;
|
||||
const maxRows = Math.max(revisionRowCount, comparisonRowCount);
|
||||
|
||||
const Iterable: React.FC<Props> = ({ revision, comparison }) => (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__revision`}>
|
||||
{revision}
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{field.label && (
|
||||
<Label>
|
||||
{locale && (
|
||||
<span className={`${baseClass}__locale-label`}>{locale}</span>
|
||||
)}
|
||||
{field.label}
|
||||
</Label>
|
||||
)}
|
||||
{maxRows > 0 && (
|
||||
<React.Fragment>
|
||||
{Array.from(Array(maxRows).keys()).map((row, i) => {
|
||||
const revisionRow = revision?.[i] || {};
|
||||
const comparisonRow = comparison?.[i] || {};
|
||||
|
||||
let subFields: Field[] = [];
|
||||
|
||||
if (field.type === 'array') subFields = field.fields;
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
subFields = [
|
||||
{
|
||||
name: 'blockType',
|
||||
label: 'Block Type',
|
||||
type: 'text',
|
||||
},
|
||||
];
|
||||
|
||||
if (revisionRow?.blockType === comparisonRow?.blockType) {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === revisionRow?.blockType) || { fields: [] };
|
||||
subFields = [
|
||||
...subFields,
|
||||
...matchedBlock.fields,
|
||||
];
|
||||
} else {
|
||||
const matchedRevisionBlock = field.blocks.find((block) => block.slug === revisionRow?.blockType) || { fields: [] };
|
||||
const matchedComparisonBlock = field.blocks.find((block) => block.slug === comparisonRow?.blockType) || { fields: [] };
|
||||
|
||||
subFields = getUniqueListBy<Field>([
|
||||
...subFields,
|
||||
...matchedRevisionBlock.fields,
|
||||
...matchedComparisonBlock.fields,
|
||||
], 'name');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${baseClass}__wrap`}
|
||||
key={i}
|
||||
>
|
||||
<RenderFieldsToDiff
|
||||
locales={locales}
|
||||
revision={revisionRow}
|
||||
comparison={comparisonRow}
|
||||
fieldPermissions={permissions}
|
||||
fields={subFields.filter((subField) => !(fieldAffectsData(subField) && subField.name === 'id'))}
|
||||
fieldComponents={fieldComponents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{maxRows === 0 && (
|
||||
<div className={`${baseClass}__no-rows`}>
|
||||
No
|
||||
{' '}
|
||||
{field.labels.plural}
|
||||
{' '}
|
||||
found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__comparison`}>
|
||||
{comparison}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Iterable;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
|
||||
.nested-diff {
|
||||
&__wrap--gutter {
|
||||
padding-left: base(1);
|
||||
border-left: $style-stroke-width-s solid $color-light-gray;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,45 @@
|
||||
import React from 'react';
|
||||
import RenderFieldsToDiff from '../..';
|
||||
import { Props } from '../types';
|
||||
import Label from '../../Label';
|
||||
import { FieldWithSubFields } from '../../../../../../../fields/config/types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'nested-diff';
|
||||
|
||||
type Props = {
|
||||
revision: string
|
||||
comparison: string
|
||||
}
|
||||
|
||||
const Nested: React.FC<Props> = ({ revision, comparison }) => (
|
||||
const Nested: React.FC<Props & { field: FieldWithSubFields}> = ({
|
||||
revision,
|
||||
comparison,
|
||||
permissions,
|
||||
field,
|
||||
locale,
|
||||
locales,
|
||||
fieldComponents,
|
||||
disableGutter = false,
|
||||
}) => (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__revision`}>
|
||||
{revision}
|
||||
</div>
|
||||
<div className={`${baseClass}__comparison`}>
|
||||
{comparison}
|
||||
{field.label && (
|
||||
<Label>
|
||||
{locale && (
|
||||
<span className={`${baseClass}__locale-label`}>{locale}</span>
|
||||
)}
|
||||
{field.label}
|
||||
</Label>
|
||||
)}
|
||||
<div className={[
|
||||
`${baseClass}__wrap`,
|
||||
!disableGutter && `${baseClass}__wrap--gutter`,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<RenderFieldsToDiff
|
||||
locales={locales}
|
||||
revision={revision}
|
||||
comparison={comparison}
|
||||
fieldPermissions={permissions}
|
||||
fields={field.fields}
|
||||
fieldComponents={fieldComponents}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDiffViewer from 'react-diff-viewer';
|
||||
import { Props } from '../types';
|
||||
import Label from '../../Label';
|
||||
import { Props } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'text-diff';
|
||||
|
||||
const Text: React.FC<Props> = ({ field, locale, revision, comparison }) => {
|
||||
let placeholder = '';
|
||||
|
||||
if (revision === comparison) placeholder = '[no value]';
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Label>
|
||||
@@ -17,8 +21,8 @@ const Text: React.FC<Props> = ({ field, locale, revision, comparison }) => {
|
||||
{field.label}
|
||||
</Label>
|
||||
<ReactDiffViewer
|
||||
oldValue={String(revision)}
|
||||
newValue={String(comparison)}
|
||||
oldValue={typeof revision !== 'undefined' ? String(revision) : placeholder}
|
||||
newValue={typeof comparison !== 'undefined' ? String(comparison) : placeholder}
|
||||
splitView
|
||||
hideLineNumbers
|
||||
showDiffOnly={false}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Text from './Text';
|
||||
import Iterable from './Iterable';
|
||||
import Nested from './Nested';
|
||||
import Iterable from './Iterable';
|
||||
// import Point from './Point';
|
||||
// import Relationship from './Relationship';
|
||||
// import Date from './Date';
|
||||
|
||||
export default {
|
||||
text: Text,
|
||||
@@ -8,15 +11,15 @@ export default {
|
||||
number: Text,
|
||||
email: Text,
|
||||
code: Text,
|
||||
// group: Nested,
|
||||
// row: Nested,
|
||||
// array: Iterable,
|
||||
blocks: Iterable,
|
||||
checkbox: Text,
|
||||
date: Text,
|
||||
radio: Text,
|
||||
select: Text,
|
||||
relationship: Text,
|
||||
upload: Text,
|
||||
point: Text,
|
||||
row: Nested,
|
||||
group: Nested,
|
||||
array: Iterable,
|
||||
blocks: Iterable,
|
||||
// date: Text,
|
||||
// select: Text,
|
||||
// relationship: Relationship,
|
||||
// upload: Relationship,
|
||||
// point: Point,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Field } from '../../../../../../fields/config/types';
|
||||
import { FieldPermissions } from '../../../../../../auth';
|
||||
|
||||
export type Props = {
|
||||
revision: string
|
||||
comparison: string
|
||||
fieldComponents: Record<string, React.FC<Props>>
|
||||
revision: any
|
||||
comparison: any
|
||||
field: Field
|
||||
permissions?: Record<string, FieldPermissions>
|
||||
locale?: string
|
||||
locales?: string[]
|
||||
disableGutter?: boolean
|
||||
}
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Props } from './types';
|
||||
import fieldComponents from './fields';
|
||||
import { fieldAffectsData } from '../../../../../fields/config/types';
|
||||
import Label from './Label';
|
||||
import { fieldAffectsData, fieldHasSubFields } from '../../../../../fields/config/types';
|
||||
import Nested from './fields/Nested';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'render-field-diffs';
|
||||
|
||||
const RenderFieldsToDiff: React.FC<Props> = ({ fields, fieldPermissions, revision, comparison, locales }) => (
|
||||
const RenderFieldsToDiff: React.FC<Props> = ({
|
||||
fields,
|
||||
fieldComponents,
|
||||
fieldPermissions,
|
||||
revision,
|
||||
comparison,
|
||||
locales,
|
||||
}) => (
|
||||
<div className={baseClass}>
|
||||
{fields.map((field, i) => {
|
||||
const Component = fieldComponents[field.type];
|
||||
|
||||
if (Component) {
|
||||
if (fieldAffectsData(field)) {
|
||||
const revisionValue = revision[field.name];
|
||||
const comparisonValue = comparison?.[field.name];
|
||||
const hasPermission = fieldPermissions?.[field.name]?.read?.permission;
|
||||
const subFieldPermissions = fieldPermissions?.[field.name]?.fields;
|
||||
|
||||
if (!hasPermission) return null;
|
||||
if (hasPermission === false) return null;
|
||||
|
||||
if (field.localized) {
|
||||
return (
|
||||
@@ -37,9 +45,12 @@ const RenderFieldsToDiff: React.FC<Props> = ({ fields, fieldPermissions, revisio
|
||||
<div className={`${baseClass}__locale-value`}>
|
||||
<Component
|
||||
locale={locale}
|
||||
locales={locales}
|
||||
field={field}
|
||||
fieldComponents={fieldComponents}
|
||||
revision={revisionLocaleValue}
|
||||
comparison={comparisonLocaleValue}
|
||||
permissions={subFieldPermissions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,13 +66,32 @@ const RenderFieldsToDiff: React.FC<Props> = ({ fields, fieldPermissions, revisio
|
||||
key={i}
|
||||
>
|
||||
<Component
|
||||
locales={locales}
|
||||
field={field}
|
||||
fieldComponents={fieldComponents}
|
||||
revision={revisionValue}
|
||||
comparison={comparisonValue}
|
||||
permissions={subFieldPermissions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, we are dealing with a `row` or similar
|
||||
if (fieldHasSubFields(field)) {
|
||||
return (
|
||||
<Nested
|
||||
key={i}
|
||||
locales={locales}
|
||||
disableGutter
|
||||
field={field}
|
||||
fieldComponents={fieldComponents}
|
||||
revision={revision}
|
||||
comparison={comparison}
|
||||
permissions={fieldPermissions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import { FieldPermissions } from '../../../../../auth';
|
||||
import { Field } from '../../../../../fields/config/types';
|
||||
import { Props as FieldProps } from './fields/types';
|
||||
|
||||
export type Props = {
|
||||
fields: Field[]
|
||||
fieldComponents: Record<string, React.FC<FieldProps>>
|
||||
fieldPermissions: Record<string, FieldPermissions>
|
||||
revision: Record<string, any>
|
||||
comparison: Record<string, any>
|
||||
|
||||
180
src/admin/components/views/Revision/Revision.tsx
Normal file
180
src/admin/components/views/Revision/Revision.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAuth, useConfig } from '@payloadcms/config-provider';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import format from 'date-fns/format';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
import Eyebrow from '../../elements/Eyebrow';
|
||||
import Loading from '../../elements/Loading';
|
||||
import { useStepNav } from '../../elements/StepNav';
|
||||
import { StepNavItem } from '../../elements/StepNav/types';
|
||||
import Meta from '../../utilities/Meta';
|
||||
import { LocaleOption, CompareOption, Props } from './types';
|
||||
import CompareRevision from './Compare';
|
||||
import { publishedVersionOption } from './shared';
|
||||
import Restore from './Restore';
|
||||
import SelectLocales from './SelectLocales';
|
||||
import RenderFieldsToDiff from './RenderFieldsToDiff';
|
||||
import fieldComponents from './RenderFieldsToDiff/fields';
|
||||
|
||||
import { Field } from '../../../../fields/config/types';
|
||||
import { FieldPermissions } from '../../../../auth';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'view-revision';
|
||||
|
||||
const RevisionView: React.FC<Props> = ({ collection, global }) => {
|
||||
const { serverURL, routes: { admin, api }, admin: { dateFormat }, localization } = useConfig();
|
||||
const { setStepNav } = useStepNav();
|
||||
const { params: { id, revisionID } } = useRouteMatch<{ id?: string, revisionID: string }>();
|
||||
const [compareValue, setCompareValue] = useState<CompareOption>(publishedVersionOption);
|
||||
const [localeOptions] = useState<LocaleOption[]>(() => (localization?.locales ? localization.locales.map((locale) => ({ label: locale, value: locale })) : []));
|
||||
const [locales, setLocales] = useState<LocaleOption[]>(localeOptions);
|
||||
const { permissions } = useAuth();
|
||||
|
||||
let originalDocFetchURL: string;
|
||||
let revisionFetchURL: string;
|
||||
let entityLabel: string;
|
||||
let fields: Field[];
|
||||
let fieldPermissions: Record<string, FieldPermissions>;
|
||||
let compareBaseURL: string;
|
||||
let slug: string;
|
||||
let parentID: string;
|
||||
|
||||
if (collection) {
|
||||
({ slug } = collection);
|
||||
originalDocFetchURL = `${serverURL}${api}/${slug}/${id}`;
|
||||
revisionFetchURL = `${serverURL}${api}/${slug}/revisions/${revisionID}`;
|
||||
compareBaseURL = `${serverURL}${api}/${slug}/revisions`;
|
||||
entityLabel = collection.labels.singular;
|
||||
parentID = id;
|
||||
fields = collection.fields;
|
||||
fieldPermissions = permissions.collections[collection.slug].fields;
|
||||
}
|
||||
|
||||
if (global) {
|
||||
({ slug } = global);
|
||||
originalDocFetchURL = `${serverURL}${api}/globals/${slug}`;
|
||||
revisionFetchURL = `${serverURL}${api}/globals/${slug}/revisions/${revisionID}`;
|
||||
compareBaseURL = `${serverURL}${api}/globals/${slug}/revisions`;
|
||||
entityLabel = global.label;
|
||||
fields = global.fields;
|
||||
fieldPermissions = permissions.globals[global.slug].fields;
|
||||
}
|
||||
|
||||
const useAsTitle = collection?.admin?.useAsTitle || 'id';
|
||||
const compareFetchURL = compareValue?.value === 'published' ? originalDocFetchURL : `${compareBaseURL}/${compareValue.value}`;
|
||||
|
||||
const [{ data: doc, isLoading }] = usePayloadAPI(revisionFetchURL, { initialParams: { locale: '*', depth: 1 } });
|
||||
const [{ data: originalDoc }] = usePayloadAPI(originalDocFetchURL, { initialParams: { depth: 1 } });
|
||||
const [{ data: compareDoc }] = usePayloadAPI(compareFetchURL, { initialParams: { locale: '*', depth: 1 } });
|
||||
|
||||
useEffect(() => {
|
||||
let nav: StepNavItem[] = [];
|
||||
|
||||
if (collection) {
|
||||
nav = [
|
||||
{
|
||||
url: `${admin}/collections/${collection.slug}`,
|
||||
label: collection.labels.plural,
|
||||
},
|
||||
{
|
||||
label: originalDoc ? originalDoc[useAsTitle] : '',
|
||||
url: `${admin}/collections/${collection.slug}/${id}`,
|
||||
},
|
||||
{
|
||||
label: 'Revisions',
|
||||
url: `${admin}/collections/${collection.slug}/${id}/revisions`,
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt ? format(new Date(doc.createdAt), dateFormat) : '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (global) {
|
||||
nav = [
|
||||
{
|
||||
url: `${admin}/globals/${global.slug}`,
|
||||
label: global.label,
|
||||
},
|
||||
{
|
||||
label: 'Revisions',
|
||||
url: `${admin}/globals/${global.slug}/revisions`,
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt ? format(new Date(doc.createdAt), dateFormat) : '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
setStepNav(nav);
|
||||
}, [setStepNav, collection, global, useAsTitle, dateFormat, doc, originalDoc, admin, id]);
|
||||
|
||||
let metaTitle: string;
|
||||
let metaDesc: string;
|
||||
const formattedCreatedAt = doc?.createdAt ? format(new Date(doc.createdAt), dateFormat) : '';
|
||||
|
||||
if (collection) {
|
||||
metaTitle = `Revision - ${formattedCreatedAt} - ${doc[useAsTitle]} - ${entityLabel}`;
|
||||
metaDesc = `Viewing revision for the ${entityLabel} ${doc[useAsTitle]}`;
|
||||
}
|
||||
|
||||
if (global) {
|
||||
metaTitle = `Revision - ${formattedCreatedAt} - ${entityLabel}`;
|
||||
metaDesc = `Viewing revision for the global ${entityLabel}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Meta
|
||||
title={metaTitle}
|
||||
description={metaDesc}
|
||||
/>
|
||||
<Eyebrow />
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<div className={`${baseClass}__intro`}>Revision created on:</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h2>
|
||||
{formattedCreatedAt}
|
||||
</h2>
|
||||
<Restore
|
||||
className={`${baseClass}__restore`}
|
||||
collection={collection}
|
||||
global={global}
|
||||
/>
|
||||
</header>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
{localization && (
|
||||
<SelectLocales
|
||||
onChange={setLocales}
|
||||
options={localeOptions}
|
||||
value={locales}
|
||||
/>
|
||||
)}
|
||||
<CompareRevision
|
||||
baseURL={compareBaseURL}
|
||||
parentID={parentID}
|
||||
value={compareValue}
|
||||
onChange={setCompareValue}
|
||||
/>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<Loading />
|
||||
)}
|
||||
{doc?.revision && (
|
||||
<RenderFieldsToDiff
|
||||
locales={locales.map((locale) => locale.value)}
|
||||
fields={fields}
|
||||
fieldComponents={fieldComponents}
|
||||
fieldPermissions={fieldPermissions}
|
||||
revision={doc?.revision}
|
||||
comparison={compareValue?.value === 'published' ? compareDoc : compareDoc?.revision}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RevisionView;
|
||||
@@ -40,20 +40,18 @@
|
||||
|
||||
@include mid-break {
|
||||
&__wrap {
|
||||
padding: $baseline 0;
|
||||
padding: $baseline;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&__intro,
|
||||
&__header {
|
||||
padding-left: $baseline;
|
||||
padding-right: $baseline;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: block;
|
||||
margin: 0 base(.5);
|
||||
margin: 0 base(-.5) base(2);
|
||||
|
||||
> * {
|
||||
margin-bottom: base(.5);
|
||||
|
||||
@@ -1,177 +1,13 @@
|
||||
import { useAuth, useConfig } from '@payloadcms/config-provider';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import format from 'date-fns/format';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
import Eyebrow from '../../elements/Eyebrow';
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import Loading from '../../elements/Loading';
|
||||
import { useStepNav } from '../../elements/StepNav';
|
||||
import { StepNavItem } from '../../elements/StepNav/types';
|
||||
import Meta from '../../utilities/Meta';
|
||||
import { LocaleOption, CompareOption, Props } from './types';
|
||||
import CompareRevision from './Compare';
|
||||
import { publishedVersionOption } from './shared';
|
||||
import Restore from './Restore';
|
||||
import SelectLocales from './SelectLocales';
|
||||
import RenderFieldsToDiff from './RenderFieldsToDiff';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
import { Field } from '../../../../fields/config/types';
|
||||
import { FieldPermissions } from '../../../../auth';
|
||||
const RevisionView = lazy(() => import('./Revision'));
|
||||
|
||||
const baseClass = 'view-revision';
|
||||
const Revision: React.FC<Props> = (props) => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<RevisionView {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const ViewRevision: React.FC<Props> = ({ collection, global }) => {
|
||||
const { serverURL, routes: { admin, api }, admin: { dateFormat }, localization } = useConfig();
|
||||
const { setStepNav } = useStepNav();
|
||||
const { params: { id, revisionID } } = useRouteMatch<{ id?: string, revisionID: string }>();
|
||||
const [compareValue, setCompareValue] = useState<CompareOption>(publishedVersionOption);
|
||||
const [localeOptions] = useState<LocaleOption[]>(() => (localization?.locales ? localization.locales.map((locale) => ({ label: locale, value: locale })) : []));
|
||||
const [locales, setLocales] = useState<LocaleOption[]>(localeOptions);
|
||||
const { permissions } = useAuth();
|
||||
|
||||
let originalDocFetchURL: string;
|
||||
let revisionFetchURL: string;
|
||||
let entityLabel: string;
|
||||
let fields: Field[];
|
||||
let fieldPermissions: Record<string, FieldPermissions>;
|
||||
let compareBaseURL: string;
|
||||
let slug: string;
|
||||
let parentID: string;
|
||||
|
||||
if (collection) {
|
||||
({ slug } = collection);
|
||||
originalDocFetchURL = `${serverURL}${api}/${slug}/${id}`;
|
||||
revisionFetchURL = `${serverURL}${api}/${slug}/revisions/${revisionID}`;
|
||||
compareBaseURL = `${serverURL}${api}/${slug}/revisions`;
|
||||
entityLabel = collection.labels.singular;
|
||||
parentID = id;
|
||||
fields = collection.fields;
|
||||
fieldPermissions = permissions.collections[collection.slug].fields;
|
||||
}
|
||||
|
||||
if (global) {
|
||||
({ slug } = global);
|
||||
originalDocFetchURL = `${serverURL}${api}/globals/${slug}`;
|
||||
revisionFetchURL = `${serverURL}${api}/globals/${slug}/revisions/${revisionID}`;
|
||||
compareBaseURL = `${serverURL}${api}/globals/${slug}/revisions`;
|
||||
entityLabel = global.label;
|
||||
fields = global.fields;
|
||||
fieldPermissions = permissions.globals[global.slug].fields;
|
||||
}
|
||||
|
||||
const useAsTitle = collection?.admin?.useAsTitle || 'id';
|
||||
const compareFetchURL = compareValue?.value === 'published' ? originalDocFetchURL : `${compareBaseURL}/${compareValue.value}`;
|
||||
|
||||
const [{ data: doc, isLoading }] = usePayloadAPI(revisionFetchURL, { initialParams: { locale: '*' } });
|
||||
const [{ data: originalDoc }] = usePayloadAPI(originalDocFetchURL);
|
||||
const [{ data: compareDoc }] = usePayloadAPI(compareFetchURL, { initialParams: { locale: '*' } });
|
||||
|
||||
useEffect(() => {
|
||||
let nav: StepNavItem[] = [];
|
||||
|
||||
if (collection) {
|
||||
nav = [
|
||||
{
|
||||
url: `${admin}/collections/${collection.slug}`,
|
||||
label: collection.labels.plural,
|
||||
},
|
||||
{
|
||||
label: originalDoc ? originalDoc[useAsTitle] : '',
|
||||
url: `${admin}/collections/${collection.slug}/${id}`,
|
||||
},
|
||||
{
|
||||
label: 'Revisions',
|
||||
url: `${admin}/collections/${collection.slug}/${id}/revisions`,
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt ? format(new Date(doc.createdAt), dateFormat) : '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (global) {
|
||||
nav = [
|
||||
{
|
||||
url: `${admin}/globals/${global.slug}`,
|
||||
label: global.label,
|
||||
},
|
||||
{
|
||||
label: 'Revisions',
|
||||
url: `${admin}/globals/${global.slug}/revisions`,
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt ? format(new Date(doc.createdAt), dateFormat) : '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
setStepNav(nav);
|
||||
}, [setStepNav, collection, global, useAsTitle, dateFormat, doc, originalDoc, admin, id]);
|
||||
|
||||
let metaTitle: string;
|
||||
let metaDesc: string;
|
||||
const formattedCreatedAt = doc?.createdAt ? format(new Date(doc.createdAt), dateFormat) : '';
|
||||
|
||||
if (collection) {
|
||||
metaTitle = `Revision - ${formattedCreatedAt} - ${doc[useAsTitle]} - ${entityLabel}`;
|
||||
metaDesc = `Viewing revision for the ${entityLabel} ${doc[useAsTitle]}`;
|
||||
}
|
||||
|
||||
if (global) {
|
||||
metaTitle = `Revision - ${formattedCreatedAt} - ${entityLabel}`;
|
||||
metaDesc = `Viewing revision for the global ${entityLabel}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Meta
|
||||
title={metaTitle}
|
||||
description={metaDesc}
|
||||
/>
|
||||
<Eyebrow />
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<div className={`${baseClass}__intro`}>Revision created on:</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h2>
|
||||
{formattedCreatedAt}
|
||||
</h2>
|
||||
<Restore
|
||||
className={`${baseClass}__restore`}
|
||||
collection={collection}
|
||||
global={global}
|
||||
/>
|
||||
</header>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
{localization && (
|
||||
<SelectLocales
|
||||
onChange={setLocales}
|
||||
options={localeOptions}
|
||||
value={locales}
|
||||
/>
|
||||
)}
|
||||
<CompareRevision
|
||||
baseURL={compareBaseURL}
|
||||
parentID={parentID}
|
||||
value={compareValue}
|
||||
onChange={setCompareValue}
|
||||
/>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<Loading />
|
||||
)}
|
||||
{doc?.revision && (
|
||||
<RenderFieldsToDiff
|
||||
locales={locales.map((locale) => locale.value)}
|
||||
fields={fields}
|
||||
fieldPermissions={fieldPermissions}
|
||||
revision={doc?.revision}
|
||||
comparison={compareValue?.value === 'published' ? compareDoc : compareDoc?.revision}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewRevision;
|
||||
export default Revision;
|
||||
|
||||
3
src/utilities/getUniqueListBy.ts
Normal file
3
src/utilities/getUniqueListBy.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function getUniqueListBy<T>(arr: T[], key: string): T[] {
|
||||
return [...new Map(arr.map((item) => [item[key], item])).values()];
|
||||
}
|
||||
Reference in New Issue
Block a user