feat: implements versions in global ui

This commit is contained in:
James
2022-02-08 11:15:26 -05:00
parent a59b14bd8c
commit eb4f9572b8
11 changed files with 130 additions and 79 deletions

View File

@@ -15,7 +15,7 @@ When enabled, Payload will automatically scaffold a new Collection in your datab
**With Versions, you can:**
- Maintain a history of every change ever made to a document, including monitoring for what user made which change
- Maintain an audit log / history of every change ever made to a document, including monitoring for what user made which change
- Restore documents and globals to prior states in case you need to roll back changes
- Build a true [/docs/versions/drafts](Draft Preview)) mode for your data
- Manage who can see Drafts, and who can only see Published documents via [Access Control](/docs/access-control/overview)
@@ -34,7 +34,7 @@ Versions support a few different levels of functionality that each come with the
If you enable versions but keep draft mode disabled, Payload will simply create a new version of a document each time you update a document. This is great for use cases where you need to retain a history of all document updates over time, but always want to treat the newest document version as the version that is "published".
For example, a use case for "versions enabled, drafts disabled" could be on a collection of users, where you might want to keep a version history of all changes ever made to users - but any changes to users should _always_ be treated as "published" and you have no need to maintain a "draft" version of a user.
For example, a use case for "versions enabled, drafts disabled" could be on a collection of users, where you might want to keep a version history (or audit log) of all changes ever made to users - but any changes to users should _always_ be treated as "published" and you have no need to maintain a "draft" version of a user.
##### Versions and drafts enabled

View File

@@ -30,9 +30,17 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
// after the timeout has executed
fieldRef.current = fields;
const interval = collection.versions.drafts && collection.versions.drafts.autosave ? collection.versions.drafts.autosave.interval : 5;
let interval = 5;
const createDoc = useCallback(async () => {
if (collection) {
interval = collection.versions.drafts && collection.versions.drafts.autosave ? collection.versions.drafts.autosave.interval : 5;
}
if (global) {
interval = global.versions.drafts && global.versions.drafts.autosave ? global.versions.drafts.autosave.interval : 5;
}
const createCollectionDoc = useCallback(async () => {
const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true`, {
method: 'POST',
headers: {
@@ -53,9 +61,9 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
// If no ID, but this is used for a collection doc,
// Immediately save it and set lastSaved
if (!id && collection) {
createDoc();
createCollectionDoc();
}
}, [id, collection, global, createDoc]);
}, [id, collection, createCollectionDoc]);
// When fields change, autosave
useEffect(() => {

View File

@@ -12,9 +12,13 @@ import fieldTypes from '../../forms/field-types';
import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving';
import VersionsCount from '../../elements/VersionsCount';
import { Props } from './types';
import ViewDescription from '../../elements/ViewDescription';
import Loading from '../../elements/Loading';
import { useDocumentInfo } from '../../utilities/DocumentInfo';
import SaveDraft from '../../elements/SaveDraft';
import Publish from '../../elements/Publish';
import Status from '../../elements/Status';
import Autosave from '../../elements/Autosave';
import './index.scss';
@@ -22,8 +26,10 @@ const baseClass = 'global-edit';
const DefaultGlobalView: React.FC<Props> = (props) => {
const { admin: { dateFormat } } = useConfig();
const { publishedDoc } = useDocumentInfo();
const {
global, data, onSave, permissions, action, apiURL, initialState, isLoading,
global, data, onSave, permissions, action, apiURL, initialState, isLoading, autosaveEnabled,
} = props;
const {
@@ -89,16 +95,47 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__document-actions${preview ? ` ${baseClass}__document-actions--with-preview` : ''}`}>
<PreviewButton
generatePreviewURL={preview}
data={data}
/>
<div className={`${baseClass}__document-actions${(!autosaveEnabled || preview) ? ` ${baseClass}__document-actions--has-2` : ''}`}>
{(preview && autosaveEnabled) && (
<PreviewButton
generatePreviewURL={preview}
data={data}
/>
)}
{hasSavePermission && (
<FormSubmit>Save</FormSubmit>
<React.Fragment>
{global.versions?.drafts && (
<React.Fragment>
{!global.versions.drafts.autosave && (
<SaveDraft />
)}
<Publish />
</React.Fragment>
)}
{!global.versions?.drafts && (
<FormSubmit>Save</FormSubmit>
)}
</React.Fragment>
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>
{(preview && !autosaveEnabled) && (
<PreviewButton
generatePreviewURL={preview}
data={data}
/>
)}
{global.versions?.drafts && (
<React.Fragment>
<Status />
{(global.versions.drafts.autosave && hasSavePermission) && (
<Autosave
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
global={global}
/>
)}
</React.Fragment>
)}
<RenderFields
operation="update"
readOnly={!hasSavePermission}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useConfig, useAuth } from '@payloadcms/config-provider';
import { useStepNav } from '../../elements/StepNav';
@@ -11,7 +11,7 @@ import DefaultGlobal from './Default';
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema';
import { NegativeFieldGutterProvider } from '../../forms/FieldTypeGutter/context';
import { IndexProps } from './types';
import { DocumentInfoProvider } from '../../utilities/DocumentInfo';
import { useDocumentInfo } from '../../utilities/DocumentInfo';
const GlobalView: React.FC<IndexProps> = (props) => {
const { state: locationState } = useLocation<{data?: Record<string, unknown>}>();
@@ -19,6 +19,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const { setStepNav } = useStepNav();
const { permissions } = useAuth();
const [initialState, setInitialState] = useState({});
const { getVersions } = useDocumentInfo();
const {
serverURL,
@@ -42,14 +43,15 @@ const GlobalView: React.FC<IndexProps> = (props) => {
} = {},
} = global;
const onSave = async (json) => {
const state = await buildStateFromSchema(fields, json.doc);
const onSave = useCallback(async (json) => {
getVersions();
const state = await buildStateFromSchema(fields, json.result);
setInitialState(state);
};
}, [getVersions, fields]);
const [{ data, isLoading }] = usePayloadAPI(
`${serverURL}${api}/globals/${slug}`,
{ initialParams: { 'fallback-locale': 'null', depth: 0 } },
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
);
const dataToRender = locationState?.data || data;
@@ -72,28 +74,26 @@ const GlobalView: React.FC<IndexProps> = (props) => {
}, [dataToRender, fields]);
const globalPermissions = permissions?.globals?.[slug];
const autosaveEnabled = global.versions?.drafts && global.versions.drafts.autosave;
return (
<DocumentInfoProvider
global={global}
>
<NegativeFieldGutterProvider allow>
<RenderCustomComponent
DefaultComponent={DefaultGlobal}
CustomComponent={CustomEdit}
componentProps={{
isLoading,
data: dataToRender,
permissions: globalPermissions,
initialState,
global,
onSave,
apiURL: `${serverURL}${api}/globals/${slug}?depth=0`,
action: `${serverURL}${api}/globals/${slug}?locale=${locale}&depth=0&fallback-locale=null`,
}}
/>
</NegativeFieldGutterProvider>
</DocumentInfoProvider>
<NegativeFieldGutterProvider allow>
<RenderCustomComponent
DefaultComponent={DefaultGlobal}
CustomComponent={CustomEdit}
componentProps={{
isLoading,
data: dataToRender,
permissions: globalPermissions,
initialState,
global,
onSave,
apiURL: `${serverURL}${api}/globals/${slug}${global.versions?.drafts ? '?draft=true' : ''}`,
action: `${serverURL}${api}/globals/${slug}?locale=${locale}&depth=0&fallback-locale=null`,
autosaveEnabled,
}}
/>
</NegativeFieldGutterProvider>
);
};
export default GlobalView;

View File

@@ -1,6 +1,7 @@
import { GlobalPermission } from '../../../../auth/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
import { Fields } from '../../forms/Form/types';
import { Document } from '../../../../types';
export type IndexProps = {
global: SanitizedGlobalConfig
@@ -8,11 +9,12 @@ export type IndexProps = {
export type Props = {
global: SanitizedGlobalConfig
data: Record<string, unknown>
data: Document
onSave: () => void
permissions: GlobalPermission
action: string
apiURL: string
initialState: Fields
isLoading: boolean
autosaveEnabled: boolean
}

View File

@@ -148,48 +148,48 @@ const DefaultEditView: React.FC<Props> = (props) => {
</li>
)}
</ul>
<div className={`${baseClass}__document-actions${(autosaveEnabled || (isEditing && preview)) ? ` ${baseClass}__document-actions--has-2` : ''}`}>
{(preview && !autosaveEnabled) && (
<PreviewButton
generatePreviewURL={preview}
data={data}
/>
<div className={`${baseClass}__document-actions${(!autosaveEnabled || (isEditing && preview)) ? ` ${baseClass}__document-actions--has-2` : ''}`}>
{(preview && autosaveEnabled) && (
<PreviewButton
generatePreviewURL={preview}
data={data}
/>
)}
{hasSavePermission && (
<React.Fragment>
{collection.versions.drafts && (
<React.Fragment>
{!collection.versions.drafts.autosave && (
<SaveDraft />
)}
<Publish />
</React.Fragment>
)}
{!collection.versions.drafts && (
<FormSubmit>Save</FormSubmit>
)}
</React.Fragment>
<React.Fragment>
{collection.versions?.drafts && (
<React.Fragment>
{!collection.versions.drafts.autosave && (
<SaveDraft />
)}
<Publish />
</React.Fragment>
)}
{!collection.versions?.drafts && (
<FormSubmit>Save</FormSubmit>
)}
</React.Fragment>
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>
{(isEditing && preview && autosaveEnabled) && (
<PreviewButton
generatePreviewURL={preview}
data={data}
/>
{(isEditing && preview && !autosaveEnabled) && (
<PreviewButton
generatePreviewURL={preview}
data={data}
/>
)}
{collection.versions?.drafts && (
<React.Fragment>
<Status />
{(collection.versions.drafts.autosave && hasSavePermission) && (
<Autosave
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
collection={collection}
id={id}
/>
)}
</React.Fragment>
)}
<React.Fragment>
<Status />
{(collection.versions?.drafts.autosave && hasSavePermission) && (
<Autosave
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
collection={collection}
id={id}
/>
)}
</React.Fragment>
)}
<RenderFields
operation={isEditing ? 'update' : 'create'}
readOnly={!hasSavePermission}

View File

@@ -120,7 +120,7 @@ const EditView: React.FC<IndexProps> = (props) => {
const apiURL = `${serverURL}${api}/${slug}/${id}${collection.versions.drafts ? '?draft=true' : ''}`;
const action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`;
const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission);
const autosaveEnabled = collection.versions?.drafts && !collection.versions.drafts.autosave;
const autosaveEnabled = collection.versions?.drafts && collection.versions.drafts.autosave;
return (
<NegativeFieldGutterProvider allow>

View File

@@ -33,6 +33,9 @@ const sanitizeCollection = (config: Config, collection: CollectionConfig): Sanit
}
if (sanitized.versions.drafts) {
if (sanitized.versions.drafts === true) sanitized.versions.drafts = {};
if (sanitized.versions.drafts.autosave === true) sanitized.versions.drafts.autosave = {};
const versionFields = mergeBaseFields(sanitized.fields, baseVersionFields);
sanitized.fields = [

View File

@@ -266,7 +266,6 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
: error;
}
result = result.toJSON({ virtuals: true });
result = JSON.stringify(result);
result = JSON.parse(result);

View File

@@ -35,6 +35,9 @@ const sanitizeGlobals = (collections: CollectionConfig[], globals: GlobalConfig[
if (sanitizedGlobal.versions === true) sanitizedGlobal.versions = {};
if (sanitizedGlobal.versions.drafts) {
if (sanitizedGlobal.versions.drafts === true) sanitizedGlobal.versions.drafts = {};
if (sanitizedGlobal.versions.drafts.autosave === true) sanitizedGlobal.versions.drafts.autosave = {};
const versionFields = mergeBaseFields(sanitizedGlobal.fields, baseVersionFields);
sanitizedGlobal.fields = [

View File

@@ -172,7 +172,6 @@ async function update<T extends TypeWithID = any>(this: Payload, args): Promise<
}
}
global = global.toJSON({ virtuals: true });
global = JSON.stringify(global);
global = JSON.parse(global);
global = sanitizeInternalFields(global);