feat: implements versions in global ui
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user