feat: progress to drafts
This commit is contained in:
@@ -53,7 +53,18 @@ const LocalizedPosts: CollectionConfig = {
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
read: ({ req: { user } }) => {
|
||||
if (user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
};
|
||||
},
|
||||
readVersions: ({ req: { user } }) => Boolean(user),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useWatchForm, useFormModified } from '../../forms/Form/context';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { Props } from './types';
|
||||
@@ -18,11 +18,18 @@ 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 fieldRef = useRef(fields);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [lastSaved, setLastSaved] = useState<number>();
|
||||
|
||||
// Store fields in ref so the autosave func
|
||||
// can always retrieve the most to date copies
|
||||
// after the timeout has executed
|
||||
fieldRef.current = fields;
|
||||
|
||||
const interval = collection.versions.drafts && collection.versions.drafts.autosave ? collection.versions.drafts.autosave.interval : 5;
|
||||
|
||||
const createDoc = useCallback(async () => {
|
||||
@@ -113,15 +120,10 @@ const Autosave: React.FC<Props> = ({ collection, global, id, updatedAt }) => {
|
||||
}, 1000);
|
||||
|
||||
const body = {
|
||||
...reduceFieldsToValues(fields),
|
||||
...reduceFieldsToValues(fieldRef.current),
|
||||
_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),
|
||||
|
||||
@@ -10,6 +10,8 @@ const collectionSchema = joi.object().keys({
|
||||
access: joi.object({
|
||||
create: joi.func(),
|
||||
read: joi.func(),
|
||||
readVersions: joi.func(),
|
||||
readDrafts: joi.func(),
|
||||
update: joi.func(),
|
||||
delete: joi.func(),
|
||||
unlock: joi.func(),
|
||||
|
||||
@@ -190,11 +190,12 @@ export type CollectionConfig = {
|
||||
access?: {
|
||||
create?: Access;
|
||||
read?: Access;
|
||||
readDrafts?: Access;
|
||||
readVersions?: Access;
|
||||
update?: Access;
|
||||
delete?: Access;
|
||||
admin?: (args?: any) => boolean;
|
||||
unlock?: Access;
|
||||
readVersions?: Access;
|
||||
};
|
||||
/**
|
||||
* Collection login options
|
||||
@@ -230,3 +231,9 @@ export type AuthCollection = {
|
||||
export type TypeWithID = {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export type TypeWithTimestamps = {
|
||||
id: string | number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ function registerCollections(): void {
|
||||
type: collection.graphQL.type,
|
||||
args: {
|
||||
id: { type: idType },
|
||||
draft: { type: GraphQLBoolean },
|
||||
...(this.config.localization ? {
|
||||
locale: { type: this.types.localeInputType },
|
||||
fallbackLocale: { type: this.types.fallbackLocaleInputType },
|
||||
@@ -138,6 +139,7 @@ function registerCollections(): void {
|
||||
type: buildPaginatedListType(pluralLabel, collection.graphQL.type),
|
||||
args: {
|
||||
where: { type: collection.graphQL.whereInputType },
|
||||
draft: { type: GraphQLBoolean },
|
||||
...(this.config.localization ? {
|
||||
locale: { type: this.types.localeInputType },
|
||||
fallbackLocale: { type: this.types.fallbackLocaleInputType },
|
||||
|
||||
@@ -11,6 +11,7 @@ export default function find(collection) {
|
||||
page: args.page,
|
||||
sort: args.sort,
|
||||
req: context.req,
|
||||
draft: args.draft,
|
||||
};
|
||||
|
||||
const results = await this.operations.collections.find(options);
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function findByID(collection) {
|
||||
collection,
|
||||
id: args.id,
|
||||
req: context.req,
|
||||
draft: args.draft,
|
||||
};
|
||||
|
||||
const result = await this.operations.collections.findByID(options);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { PaginatedDocs } from '../../mongoose/types';
|
||||
import { hasWhereAccessResult } from '../../auth/types';
|
||||
import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
|
||||
import { buildSortParam } from '../../mongoose/buildSortParam';
|
||||
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable';
|
||||
import { AccessResult } from '../../config/types';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -18,6 +20,7 @@ export type Arguments = {
|
||||
req?: PayloadRequest
|
||||
overrideAccess?: boolean
|
||||
showHiddenFields?: boolean
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promise<PaginatedDocs<T>> {
|
||||
@@ -41,6 +44,7 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
|
||||
page,
|
||||
limit,
|
||||
depth,
|
||||
draft: draftsEnabled,
|
||||
collection: {
|
||||
Model,
|
||||
config: collectionConfig,
|
||||
@@ -78,22 +82,32 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
|
||||
useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'));
|
||||
}
|
||||
|
||||
if (!overrideAccess) {
|
||||
const accessResults = await executeAccess({ req }, collectionConfig.access.read);
|
||||
let accessResult: AccessResult;
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
if (!overrideAccess) {
|
||||
accessResult = await executeAccess({ req }, collectionConfig.access.read);
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
if (!where) {
|
||||
queryToBuild.where = {
|
||||
and: [
|
||||
accessResults,
|
||||
accessResult,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
(queryToBuild.where.and as Where[]).push(accessResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collectionConfig.versions?.drafts && !draftsEnabled) {
|
||||
queryToBuild.where.and.push({
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
|
||||
// /////////////////////////////////////
|
||||
@@ -114,12 +128,33 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
|
||||
|
||||
const paginatedDocs = await Model.paginate(query, optionsToExecute);
|
||||
|
||||
let result = {
|
||||
...paginatedDocs,
|
||||
} as PaginatedDocs<T>;
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Replace documents with drafts if available
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (collectionConfig.versions?.drafts && draftsEnabled) {
|
||||
result = {
|
||||
...result,
|
||||
docs: await Promise.all(result.docs.map(async (doc) => replaceWithDraftIfAvailable({
|
||||
accessResult,
|
||||
payload: this,
|
||||
collection: collectionConfig,
|
||||
doc,
|
||||
locale,
|
||||
}))),
|
||||
};
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
let result = {
|
||||
...paginatedDocs,
|
||||
result = {
|
||||
...result,
|
||||
docs: await Promise.all(paginatedDocs.docs.map(async (doc) => {
|
||||
const docString = JSON.stringify(doc);
|
||||
let docRef = JSON.parse(docString);
|
||||
@@ -132,7 +167,7 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
|
||||
|
||||
return docRef;
|
||||
})),
|
||||
} as PaginatedDocs<T>;
|
||||
};
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Fields
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import memoize from 'micro-memoize';
|
||||
import { Payload } from '../..';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Collection, TypeWithID } from '../config/types';
|
||||
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
|
||||
@@ -7,7 +8,7 @@ import { Forbidden, NotFound } from '../../errors';
|
||||
import executeAccess from '../../auth/executeAccess';
|
||||
import { Where } from '../../types';
|
||||
import { hasWhereAccessResult } from '../../auth/types';
|
||||
import { Payload } from '../..';
|
||||
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -18,6 +19,7 @@ export type Arguments = {
|
||||
overrideAccess?: boolean
|
||||
showHiddenFields?: boolean
|
||||
depth?: number
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
async function findByID<T extends TypeWithID = any>(this: Payload, incomingArgs: Arguments): Promise<T> {
|
||||
@@ -51,18 +53,19 @@ async function findByID<T extends TypeWithID = any>(this: Payload, incomingArgs:
|
||||
currentDepth,
|
||||
overrideAccess = false,
|
||||
showHiddenFields,
|
||||
draft: draftEnabled = false,
|
||||
} = args;
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const accessResults = !overrideAccess ? await executeAccess({ req, disableErrors, id }, collectionConfig.access.read) : true;
|
||||
const accessResult = !overrideAccess ? await executeAccess({ req, disableErrors, id }, collectionConfig.access.read) : true;
|
||||
|
||||
// If errors are disabled, and access returns false, return null
|
||||
if (accessResults === false) return null;
|
||||
if (accessResult === false) return null;
|
||||
|
||||
const hasWhereAccess = typeof accessResults === 'object';
|
||||
const hasWhereAccess = typeof accessResult === 'object';
|
||||
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
@@ -76,8 +79,16 @@ async function findByID<T extends TypeWithID = any>(this: Payload, incomingArgs:
|
||||
},
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResults)) {
|
||||
(queryToBuild.where.and as Where[]).push(accessResults);
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
queryToBuild.where.and.push(accessResult);
|
||||
}
|
||||
|
||||
if (collectionConfig.versions?.drafts && !draftEnabled) {
|
||||
queryToBuild.where.and.push({
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery(queryToBuild, locale);
|
||||
@@ -117,6 +128,20 @@ async function findByID<T extends TypeWithID = any>(this: Payload, incomingArgs:
|
||||
|
||||
result = sanitizeInternalFields(result);
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Replace document with draft if available
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (collectionConfig.versions?.drafts && draftEnabled) {
|
||||
result = await replaceWithDraftIfAvailable({
|
||||
payload: this,
|
||||
collection: collectionConfig,
|
||||
doc: result,
|
||||
accessResult,
|
||||
locale,
|
||||
});
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -14,6 +14,7 @@ export type Options = {
|
||||
showHiddenFields?: boolean
|
||||
sort?: string
|
||||
where?: Where
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
export default async function find<T extends TypeWithID = any>(options: Options): Promise<PaginatedDocs<T>> {
|
||||
@@ -29,6 +30,7 @@ export default async function find<T extends TypeWithID = any>(options: Options)
|
||||
overrideAccess = true,
|
||||
showHiddenFields,
|
||||
sort,
|
||||
draft = false,
|
||||
} = options;
|
||||
|
||||
const collection = this.collections[collectionSlug];
|
||||
@@ -42,6 +44,7 @@ export default async function find<T extends TypeWithID = any>(options: Options)
|
||||
collection,
|
||||
overrideAccess,
|
||||
showHiddenFields,
|
||||
draft,
|
||||
req: {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -14,6 +14,7 @@ export type Options = {
|
||||
showHiddenFields?: boolean
|
||||
disableErrors?: boolean
|
||||
req?: PayloadRequest
|
||||
draft?: boolean
|
||||
}
|
||||
|
||||
export default async function findByID<T extends TypeWithID = any>(options: Options): Promise<T> {
|
||||
@@ -29,6 +30,7 @@ export default async function findByID<T extends TypeWithID = any>(options: Opti
|
||||
disableErrors = false,
|
||||
showHiddenFields,
|
||||
req = {},
|
||||
draft = false,
|
||||
} = options;
|
||||
|
||||
const collection = this.collections[collectionSlug];
|
||||
@@ -53,5 +55,6 @@ export default async function findByID<T extends TypeWithID = any>(options: Opti
|
||||
disableErrors,
|
||||
showHiddenFields,
|
||||
req: reqToUse,
|
||||
draft,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default async function find<T extends TypeWithID = any>(req: PayloadReque
|
||||
limit: req.query.limit,
|
||||
sort: req.query.sort,
|
||||
depth: req.query.depth,
|
||||
draft: req.query.draft === 'true',
|
||||
};
|
||||
|
||||
const result = await this.operations.collections.find(options);
|
||||
|
||||
@@ -13,6 +13,7 @@ export default async function findByID(req: PayloadRequest, res: Response, next:
|
||||
collection: req.collection,
|
||||
id: req.params.id,
|
||||
depth: req.query.depth,
|
||||
draft: req.query.draft === 'true',
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -55,6 +55,7 @@ export type GlobalConfig = {
|
||||
}
|
||||
access?: {
|
||||
read?: Access;
|
||||
readDrafts?: Access;
|
||||
readVersions?: Access;
|
||||
update?: Access;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ function registerGlobals() {
|
||||
this.Query.fields[formattedLabel] = {
|
||||
type: global.graphQL.type,
|
||||
args: {
|
||||
draft: { type: GraphQLBoolean },
|
||||
...(this.config.localization ? {
|
||||
locale: { type: this.types.localeInputType },
|
||||
fallbackLocale: { type: this.types.fallbackLocaleInputType },
|
||||
|
||||
@@ -15,6 +15,7 @@ function findOne(globalConfig: SanitizedGlobalConfig): Document {
|
||||
slug,
|
||||
depth: 0,
|
||||
req: context.req,
|
||||
draft: args.draft,
|
||||
};
|
||||
|
||||
const result = await this.operations.globals.findOne(options);
|
||||
|
||||
@@ -10,6 +10,7 @@ async function findOne(args) {
|
||||
slug,
|
||||
depth,
|
||||
showHiddenFields,
|
||||
draft = false,
|
||||
} = args;
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -7,6 +7,7 @@ async function findOne(options) {
|
||||
user,
|
||||
overrideAccess = true,
|
||||
showHiddenFields,
|
||||
draft = false,
|
||||
} = options;
|
||||
|
||||
const globalConfig = this.globals.config.find((config) => config.slug === globalSlug);
|
||||
@@ -17,6 +18,7 @@ async function findOne(options) {
|
||||
globalConfig,
|
||||
overrideAccess,
|
||||
showHiddenFields,
|
||||
draft,
|
||||
req: {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function findOne(globalConfig: SanitizedGlobalConfig): FindOneGlo
|
||||
globalConfig,
|
||||
slug,
|
||||
depth: req.query.depth,
|
||||
draft: req.query.draft === 'true',
|
||||
});
|
||||
|
||||
return res.status(httpStatus.OK).json(result);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Document as MongooseDocument } from 'mongoose';
|
||||
import { TypeWithID, TypeWithTimestamps } from '../collections/config/types';
|
||||
import { FileData } from '../uploads/types';
|
||||
|
||||
export type Operator = 'equals'
|
||||
@@ -33,3 +34,7 @@ export interface PayloadMongooseDocument extends MongooseDocument {
|
||||
}
|
||||
|
||||
export type Operation = 'create' | 'read' | 'update' | 'delete'
|
||||
|
||||
export function docHasTimestamps(doc: any): doc is TypeWithTimestamps {
|
||||
return doc?.createdAt && doc?.updatedAt;
|
||||
}
|
||||
|
||||
75
src/versions/drafts/replaceWithDraftIfAvailable.ts
Normal file
75
src/versions/drafts/replaceWithDraftIfAvailable.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Payload } from '../..';
|
||||
import { docHasTimestamps, Where } from '../../types';
|
||||
import { hasWhereAccessResult } from '../../auth';
|
||||
import { AccessResult } from '../../config/types';
|
||||
import { CollectionModel, SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types';
|
||||
import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
|
||||
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
|
||||
|
||||
type Arguments<T> = {
|
||||
payload: Payload
|
||||
collection: SanitizedCollectionConfig
|
||||
doc: T
|
||||
locale: string
|
||||
accessResult: AccessResult
|
||||
}
|
||||
|
||||
const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
||||
payload,
|
||||
collection,
|
||||
doc,
|
||||
locale,
|
||||
accessResult,
|
||||
}: Arguments<T>): Promise<T> => {
|
||||
if (docHasTimestamps(doc)) {
|
||||
const VersionModel = payload.versions[collection.slug] as CollectionModel;
|
||||
|
||||
let useEstimatedCount = false;
|
||||
const queryToBuild: { where: Where } = {
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
parent: {
|
||||
equals: doc.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: doc.updatedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (hasWhereAccessResult(accessResult)) {
|
||||
queryToBuild.where.and.push(accessResult);
|
||||
}
|
||||
|
||||
const constraints = flattenWhereConstraints(queryToBuild);
|
||||
useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'));
|
||||
const query = await VersionModel.buildQuery(queryToBuild, locale);
|
||||
|
||||
let draft = await VersionModel.findOne(query, {}, {
|
||||
lean: true,
|
||||
leanWithId: true,
|
||||
useEstimatedCount,
|
||||
});
|
||||
|
||||
if (!draft) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
draft = JSON.parse(JSON.stringify(draft));
|
||||
draft = sanitizeInternalFields(draft);
|
||||
|
||||
// Disregard all other draft content at this point,
|
||||
// Only interested in the version itself.
|
||||
// Operations will handle firing hooks, etc.
|
||||
return draft.version;
|
||||
}
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
export default replaceWithDraftIfAvailable;
|
||||
Reference in New Issue
Block a user