feat: progress to Autosave

This commit is contained in:
James
2021-12-29 21:32:16 -05:00
parent aaab8b036c
commit 13add5885d
17 changed files with 262 additions and 59 deletions

View File

@@ -0,0 +1,5 @@
@import '../../../scss/styles.scss';
.autosave {
min-height: $baseline;
}

View File

@@ -0,0 +1,156 @@
import { useConfig } from '@payloadcms/config-provider';
import { formatDistance } from 'date-fns';
import { useHistory } from 'react-router-dom';
import { toast } from 'react-toastify';
import React, { useCallback, useEffect, useState } from 'react';
import { useWatchForm, useFormModified } from '../../forms/Form/context';
import { useLocale } from '../../utilities/Locale';
import { Props } from './types';
import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues';
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema';
import { Field } from '../../../../fields/config/types';
import './index.scss';
const baseClass = 'autosave';
const Autosave: React.FC<Props> = ({ collection, global, id, updatedAt }) => {
const { serverURL, routes: { api, admin } } = useConfig();
const { fields, dispatchFields } = useWatchForm();
const modified = useFormModified();
const [saving, setSaving] = useState(false);
const [lastSaved, setLastSaved] = useState<number>();
const locale = useLocale();
const { push } = useHistory();
const interval = collection.versions.drafts && collection.versions.drafts.autosave ? collection.versions.drafts.autosave.interval : 5;
const createDoc = useCallback(async () => {
const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&autosave=true`, {
method: 'POST',
body: JSON.stringify({}),
});
if (res.status === 201) {
const json = await res.json();
push(`${admin}/collections/${collection.slug}/${json.doc.id}`);
} else {
toast.error('There was a problem while autosaving this document.');
}
}, [collection, serverURL, api, admin, locale, push]);
const getLastSaved = useCallback(async () => {
let url: string;
if (collection && id) {
url = `${serverURL}${api}/${collection.slug}/versions?where[parent][equals]=${id}&depth=0`;
}
if (global) {
url = `${serverURL}${api}/globals/${global.slug}/versions?depth=0`;
}
if (url) {
const res = await fetch(url);
if (res.status === 200) {
const json = await res.json();
if (json.docs[0]) {
return setLastSaved(new Date(json.docs[0].updatedAt).getTime());
}
}
}
if (updatedAt) return setLastSaved(new Date(updatedAt).getTime());
return null;
}, [collection, global, id, api, serverURL, updatedAt]);
// On mount, check for a recent autosave
// Need it to store the lastSaved date
useEffect(() => {
getLastSaved();
}, [getLastSaved]);
useEffect(() => {
// If no ID, but this is used for a collection doc,
// Immediately save it and set lastSaved
if (!id && collection) {
createDoc();
}
}, [id, collection, global, createDoc]);
// When fields change, autosave
useEffect(() => {
if (lastSaved && modified && !saving) {
const lastSavedDate = new Date(lastSaved);
lastSavedDate.setSeconds(lastSavedDate.getSeconds() + interval);
const timeToSaveAgain = lastSavedDate.getTime();
if (Date.now() >= timeToSaveAgain) {
setSaving(true);
setTimeout(async () => {
let url: string;
let method: string;
let entityFields: Field[] = [];
if (collection && id) {
url = `${serverURL}${api}/${collection.slug}/${id}?autosave=true`;
method = 'PUT';
entityFields = collection.fields;
}
if (global) {
url = `${serverURL}${api}/globals/${global.slug}?autosave=true`;
method = 'POST';
entityFields = global.fields;
}
if (url) {
setTimeout(() => {
setSaving(false);
}, 1000);
const body = {
...reduceFieldsToValues(fields),
_status: 'draft',
};
// TODO:
// Determine why field values are not present
// even though we are using useWatchForm
console.log(body);
const res = await fetch(url, {
method,
body: JSON.stringify(body),
});
if (res.status === 200) {
const json = await res.json();
const state = await buildStateFromSchema(entityFields, json.doc);
dispatchFields({ type: 'REPLACE_STATE', state });
setLastSaved(new Date().getTime());
}
}
}, 1000);
}
}
}, [fields, modified, interval, lastSaved, serverURL, api, collection, global, id, saving, dispatchFields]);
return (
<div className={baseClass}>
{saving && 'Saving...'}
{(!saving && lastSaved) && (
<React.Fragment>
Last saved&nbsp;
{formatDistance(new Date(), new Date(lastSaved))}
&nbsp;ago
</React.Fragment>
)}
</div>
);
};
export default Autosave;

View File

@@ -0,0 +1,9 @@
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
export type Props = {
collection?: SanitizedCollectionConfig,
global?: SanitizedGlobalConfig,
id?: string | number
updatedAt: string
}

View File

@@ -73,13 +73,27 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
let nav: StepNavItem[] = [];
if (collection) {
let docLabel = '';
if (originalDoc) {
if (useAsTitle) {
if (originalDoc[useAsTitle]) {
docLabel = originalDoc[useAsTitle];
} else {
docLabel = '[Untitled]';
}
} else {
docLabel = originalDoc.id;
}
}
nav = [
{
url: `${admin}/collections/${collection.slug}`,
label: collection.labels.plural,
},
{
label: originalDoc ? originalDoc[useAsTitle] : '',
label: docLabel,
url: `${admin}/collections/${collection.slug}/${id}`,
},
{

View File

@@ -51,13 +51,27 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
let nav: StepNavItem[] = [];
if (collection) {
let docLabel = '';
if (doc) {
if (useAsTitle) {
if (doc[useAsTitle]) {
docLabel = doc[useAsTitle];
} else {
docLabel = '[Untitled]';
}
} else {
docLabel = doc.id;
}
}
nav = [
{
url: `${admin}/collections/${collection.slug}`,
label: collection.labels.plural,
},
{
label: doc ? doc[useAsTitle] : '',
label: docLabel,
url: `${admin}/collections/${collection.slug}/${id}`,
},
{

View File

@@ -1,32 +0,0 @@
import React, { useEffect, useState } from 'react';
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
import { useWatchForm, useFormModified } from '../../../../forms/Form/context';
const Autosave: React.FC<{ collection: SanitizedCollectionConfig}> = ({ collection }) => {
const { fields } = useWatchForm();
const modified = useFormModified();
const [lastSaved, setLastSaved] = useState(() => {
const date = new Date();
date.setSeconds(date.getSeconds() - 2);
return date.getTime();
});
const interval = collection.versions.drafts && collection.versions.drafts.autosave ? collection.versions.drafts.autosave.interval : 5;
useEffect(() => {
const lastSavedDate = new Date(lastSaved);
lastSavedDate.setSeconds(lastSavedDate.getSeconds() + interval);
const timeToSaveAgain = lastSavedDate.getTime();
if (Date.now() >= timeToSaveAgain && modified) {
setTimeout(() => {
console.log('Autosaving');
}, 1000);
setLastSaved(new Date().getTime());
}
}, [modified, fields, interval, lastSaved]);
return null;
};
export default Autosave;

View File

@@ -19,7 +19,7 @@ import Auth from './Auth';
import VersionsCount from '../../../elements/VersionsCount';
import Upload from './Upload';
import { Props } from './types';
import Autosave from './Autosave';
import Autosave from '../../../elements/Autosave';
import './index.scss';
@@ -159,14 +159,13 @@ const DefaultEditView: React.FC<Props> = (props) => {
{!isLoading && (
<React.Fragment>
<div className={`${baseClass}__sidebar-fields`}>
{/* {collection.versions?.drafts && (
<Select
label="Status"
path="_status"
name="_status"
options={statuses}
{(collection.versions?.drafts && collection.versions.drafts.autosave && hasSavePermission) && (
<Autosave
updatedAt={data.updatedAt}
collection={collection}
id={id}
/>
)} */}
)}
<RenderFields
operation={isEditing ? 'update' : 'create'}
readOnly={!hasSavePermission}
@@ -232,9 +231,6 @@ const DefaultEditView: React.FC<Props> = (props) => {
</div>
</div>
</div>
{(collection.versions?.drafts && collection.versions.drafts.autosave && hasSavePermission) && (
<Autosave collection={collection} />
)}
</Form>
</div>
);

View File

@@ -15,7 +15,7 @@ import { IndexProps } from './types';
import { StepNavItem } from '../../../elements/StepNav/types';
const EditView: React.FC<IndexProps> = (props) => {
const { collection, isEditing } = props;
const { collection: incomingCollection, isEditing } = props;
const {
slug,
@@ -30,8 +30,10 @@ const EditView: React.FC<IndexProps> = (props) => {
} = {},
} = {},
} = {},
} = collection;
const [fields] = useState(() => formatFields(collection, isEditing));
} = incomingCollection;
const [fields] = useState(() => formatFields(incomingCollection, isEditing));
const [collection] = useState(() => ({ ...incomingCollection, fields }));
const [submissionCount, setSubmissionCount] = useState(0);
const locale = useLocale();
@@ -73,8 +75,22 @@ const EditView: React.FC<IndexProps> = (props) => {
}];
if (isEditing) {
let label = '';
if (dataToRender) {
if (useAsTitle) {
if (dataToRender[useAsTitle]) {
label = dataToRender[useAsTitle];
} else {
label = '[Untitled]';
}
} else {
label = dataToRender.id;
}
}
nav.push({
label: dataToRender ? dataToRender[useAsTitle || 'id'] : '',
label,
});
} else {
nav.push({
@@ -120,7 +136,7 @@ const EditView: React.FC<IndexProps> = (props) => {
submissionCount,
isLoading,
data: dataToRender,
collection: { ...collection, fields },
collection,
permissions: collectionPermissions,
isEditing,
onSave,

View File

@@ -12,6 +12,7 @@ import { defaults, authDefaults } from './defaults';
import { Config } from '../../config/types';
import { versionCollectionDefaults } from '../../versions/defaults';
import baseVersionFields from '../../versions/baseFields';
import TimestampsRequired from '../../errors/TimestampsRequired';
const mergeBaseFields = (fields, baseFields) => {
const mergedFields = [];
@@ -69,16 +70,18 @@ const sanitizeCollection = (config: Config, collection: CollectionConfig): Sanit
if (sanitized.versions) {
if (sanitized.versions === true) sanitized.versions = {};
// const defaultsToMerge = { ... };
if (sanitized.timestamps === false) {
throw new TimestampsRequired(collection);
}
// if (sanitized.versions.drafts === false) {
// defaultsToMerge.drafts = false;
// } else {
// sanitized.fields = [
// ...sanitized.fields,
// ...baseVersionFields,
// ];
// }
if (sanitized.versions.drafts) {
const versionFields = mergeBaseFields(sanitized.fields, baseVersionFields);
sanitized.fields = [
...versionFields,
...sanitized.fields,
];
}
sanitized.versions = merge(versionCollectionDefaults, sanitized.versions);
}

View File

@@ -153,6 +153,7 @@ function registerCollections(): void {
type: collection.graphQL.type,
args: {
data: { type: collection.graphQL.mutationInputType },
autosave: { type: GraphQLBoolean },
},
resolve: create(collection),
};

View File

@@ -23,6 +23,7 @@ export default function create(collection: Collection): Resolver {
collection,
data: args.data,
req: context.req,
autosave: args.autosave,
};
const result = await this.operations.collections.create(options);

View File

@@ -22,6 +22,7 @@ export type Arguments = {
showHiddenFields?: boolean
data: Record<string, unknown>
overwriteExistingFiles?: boolean
autosave?: boolean
}
async function create(this: Payload, incomingArgs: Arguments): Promise<Document> {
@@ -54,6 +55,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
overrideAccess,
showHiddenFields,
overwriteExistingFiles = false,
autosave = false,
} = args;
let { data } = args;
@@ -142,6 +144,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
req,
overrideAccess,
unflattenLocales: true,
skipValidation: autosave,
});
// /////////////////////////////////////

View File

@@ -15,6 +15,7 @@ export type Options<T> = {
filePath?: string
overwriteExistingFiles?: boolean
req: PayloadRequest
autosave?: boolean
}
export default async function create<T = any>(options: Options<T>): Promise<T> {
@@ -31,6 +32,7 @@ export default async function create<T = any>(options: Options<T>): Promise<T> {
filePath,
overwriteExistingFiles = false,
req,
autosave,
} = options;
const collection = this.collections[collectionSlug];
@@ -43,6 +45,7 @@ export default async function create<T = any>(options: Options<T>): Promise<T> {
disableVerificationEmail,
showHiddenFields,
overwriteExistingFiles,
autosave,
req: {
...req,
user,

View File

@@ -188,6 +188,7 @@ async function update(this: Payload, incomingArgs: Arguments): Promise<Document>
overrideAccess,
unflattenLocales: true,
docWithLocales,
skipValidation: autosave,
});
// /////////////////////////////////////

View File

@@ -16,6 +16,7 @@ export default async function create(req: PayloadRequest, res: Response, next: N
collection: req.collection,
data: req.body,
depth: req.query.depth,
autosave: req.query.autosave === 'true',
});
return res.status(httpStatus.CREATED).json({

View File

@@ -0,0 +1,10 @@
import { CollectionConfig } from '../collections/config/types';
import APIError from './APIError';
class TimestampsRequired extends APIError {
constructor(collection: CollectionConfig) {
super(`Timestamps are required in the collection ${collection.slug} because you have opted in to Versions.`);
}
}
export default TimestampsRequired;