feat: progress to drafts

This commit is contained in:
James
2021-12-30 11:21:53 -05:00
parent 13add5885d
commit be1da8507a
21 changed files with 206 additions and 25 deletions

View File

@@ -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: [
{

View File

@@ -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),

View File

@@ -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(),

View File

@@ -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
}

View File

@@ -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 },

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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
// /////////////////////////////////////

View File

@@ -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',

View File

@@ -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,
});
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -55,6 +55,7 @@ export type GlobalConfig = {
}
access?: {
read?: Access;
readDrafts?: Access;
readVersions?: Access;
update?: Access;
}

View File

@@ -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 },

View File

@@ -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);

View File

@@ -10,6 +10,7 @@ async function findOne(args) {
slug,
depth,
showHiddenFields,
draft = false,
} = args;
// /////////////////////////////////////

View File

@@ -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',

View File

@@ -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);

View File

@@ -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;
}

View 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;