Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/decap-cms-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,12 +516,20 @@ declare module 'decap-cms-core' {
handler: ({
entry,
author,
context,
}: {
entry: Map<string, any>;
author: { login: string; name: string };
context?: HookContext;
}) => any;
}

export interface HookContext {
publishStack?: boolean;
actions?: Record<string, Function>;
[key: string]: any;
}

export type CmsEventListenerOptions = any; // TODO: type properly

export type CmsLocalePhrases = any; // TODO: type properly
Expand Down
8 changes: 4 additions & 4 deletions packages/decap-cms-core/src/actions/__tests__/entries.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

import {
createEmptyDraft,
createLocalEmptyDraft,
createEmptyDraftData,
retrieveLocalBackup,
persistLocalBackup,
Expand Down Expand Up @@ -40,7 +40,7 @@ describe('entries', () => {
fields: [{ name: 'title' }],
});

return store.dispatch(createEmptyDraft(collection, '')).then(() => {
return store.dispatch(createLocalEmptyDraft(collection, '')).then(() => {
const actions = store.getActions();
expect(actions).toHaveLength(1);

Expand Down Expand Up @@ -73,7 +73,7 @@ describe('entries', () => {
fields: [{ name: 'title' }, { name: 'boolean' }],
});

return store.dispatch(createEmptyDraft(collection, '?title=title&boolean=True')).then(() => {
return store.dispatch(createLocalEmptyDraft(collection, '?title=title&boolean=True')).then(() => {
const actions = store.getActions();
expect(actions).toHaveLength(1);

Expand Down Expand Up @@ -107,7 +107,7 @@ describe('entries', () => {
});

return store
.dispatch(createEmptyDraft(collection, "?title=<script>alert('hello')</script>"))
.dispatch(createLocalEmptyDraft(collection, "?title=<script>alert('hello')</script>"))
.then(() => {
const actions = store.getActions();
expect(actions).toHaveLength(1);
Expand Down
106 changes: 75 additions & 31 deletions packages/decap-cms-core/src/actions/editorialWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type { AnyAction } from 'redux';
import type { EntryValue } from '../valueObjects/Entry';
import type { Status } from '../constants/publishModes';
import type { ThunkDispatch } from 'redux-thunk';
import type { HookContext } from '../backend';

/*
* Constant Declarations
Expand Down Expand Up @@ -67,6 +68,8 @@ export const UNPUBLISHED_ENTRY_DELETE_REQUEST = 'UNPUBLISHED_ENTRY_DELETE_REQUES
export const UNPUBLISHED_ENTRY_DELETE_SUCCESS = 'UNPUBLISHED_ENTRY_DELETE_SUCCESS';
export const UNPUBLISHED_ENTRY_DELETE_FAILURE = 'UNPUBLISHED_ENTRY_DELETE_FAILURE';

export const UNPUBLISHED_ENTRY_DISMISS_ERROR = 'UNPUBLISHED_ENTRY_DISMISS_ERROR';

/*
* Simple Action Creators (Internal)
*/
Expand Down Expand Up @@ -271,6 +274,7 @@ export function loadUnpublishedEntry(collection: Collection, slug: string) {
dispatch(unpublishedEntryLoaded(collection, entry));
dispatch(createDraftFromEntry(entry));
} catch (error) {
if (error.name === UNPUBLISHED_ENTRY_DISMISS_ERROR) return;
if (error.name === EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) {
dispatch(unpublishedEntryRedirected(collection, slug));
dispatch(loadEntry(collection, slug));
Expand Down Expand Up @@ -301,7 +305,7 @@ export function loadUnpublishedEntries(collections: Collections) {
}

dispatch(unpublishedEntriesLoading());
backend
return backend
.unpublishedEntries(collections)
.then(response => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)))
.catch((error: Error) => {
Expand All @@ -321,21 +325,26 @@ export function loadUnpublishedEntries(collections: Collections) {
};
}

export function persistUnpublishedEntry(collection: Collection, existingUnpublishedEntry: boolean) {
export function persistUnpublishedEntry(
collection: Collection,
existingUnpublishedEntry: boolean,
context: HookContext,
customEntryDraft?: EntryDraft,
) {
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const entryDraft = state.entryDraft;
const entryDraft = customEntryDraft || state.entryDraft;
const isCustomEntry = customEntryDraft && entryDraft.getIn(['entry', 'isCustomEntry'], true);
const status = customEntryDraft && customEntryDraft.getIn(['entry', 'status']);
const fieldsErrors = entryDraft.get('fieldsErrors');
const unpublishedSlugs = selectUnpublishedSlugs(state, collection.get('name'));
const publishedSlugs = selectPublishedSlugs(state, collection.get('name'));
const usedSlugs = publishedSlugs.concat(unpublishedSlugs) as List<string>;
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);

//load unpublishedEntries
!entriesLoaded && dispatch(loadUnpublishedEntries(state.collections));

// Early return if draft contains validation errors
if (!fieldsErrors.isEmpty()) {
if (fieldsErrors && !fieldsErrors.isEmpty()) {
const hasPresenceErrors = fieldsErrors.some(errors =>
errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE),
);
Expand Down Expand Up @@ -375,6 +384,8 @@ export function persistUnpublishedEntry(collection: Collection, existingUnpublis
entryDraft: serializedEntryDraft,
assetProxies,
usedSlugs,
context,
status,
});
dispatch(
addNotification({
Expand All @@ -385,12 +396,16 @@ export function persistUnpublishedEntry(collection: Collection, existingUnpublis
dismissAfter: 4000,
}),
);
dispatch(unpublishedEntryPersisted(collection, serializedEntry));

if (entry.get('slug') !== newSlug) {
await dispatch(loadUnpublishedEntry(collection, newSlug));
navigateToEntry(collection.get('name'), newSlug);
if (!isCustomEntry) {
dispatch(unpublishedEntryPersisted(collection, serializedEntry));
if (entry.get('slug') !== newSlug) {
await dispatch(loadUnpublishedEntry(collection, newSlug));
navigateToEntry(collection.get('name'), newSlug);
}
}

return newSlug;
} catch (error) {
dispatch(
addNotification({
Expand Down Expand Up @@ -483,17 +498,19 @@ export function deleteUnpublishedEntry(collection: string, slug: string) {
export function publishUnpublishedEntry(
collectionName: string,
slug: string,
publishStack: boolean,
context: HookContext,
customEntry?: EntryMap,
) {
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const collections = state.collections;
const backend = currentBackend(state.config);
const entry = selectUnpublishedEntry(state, collectionName, slug);
const entry = customEntry || selectUnpublishedEntry(state, collectionName, slug);
const isCustomEntry = customEntry && customEntry.get('isCustomEntry', true);
const isDeleteWorkflow = entry.get('isDeleteWorkflow');
dispatch(unpublishedEntryPublishRequest(collectionName, slug));
try {
if (!publishStack && state.stack.status.status) {
if (!context.publishStack && state.stack.status.status) {
dispatch(
addNotification({
message: {
Expand All @@ -507,7 +524,7 @@ export function publishUnpublishedEntry(
return dispatch(unpublishedEntryPublishError(collectionName, slug));
}

await backend.publishUnpublishedEntry(entry, publishStack);
await backend.publishUnpublishedEntry(entry, context);

await dispatch(checkStackStatus());

Expand All @@ -522,21 +539,24 @@ export function publishUnpublishedEntry(
}),
);
dispatch(unpublishedEntryPublished(collectionName, slug));
const collection = collections.get(collectionName);
if (collection.has('nested')) {
dispatch(loadEntries(collection));
const newSlug = slugFromCustomPath(collection, entry.get('path'));
loadEntry(collection, newSlug);
if (slug !== newSlug && selectEditingDraft(state.entryDraft)) {
navigateToEntry(collection.get('name'), newSlug);
if (!isCustomEntry) {
const collection = collections.get(collectionName);
if (!collection.has('nested')) {
dispatch(loadEntries(collection));
const newSlug = slugFromCustomPath(collection, entry.get('path'));
loadEntry(collection, newSlug);
if (slug !== newSlug && selectEditingDraft(state.entryDraft)) {
navigateToEntry(collection.get('name'), newSlug);
}
} else if (isDeleteWorkflow) {
dispatch(unpublishedEntryDeleted(collectionName, slug));
return navigateToCollection(collectionName);
} else {
return dispatch(loadEntry(collection, slug));
}
} else if (isDeleteWorkflow) {
dispatch(unpublishedEntryDeleted(collectionName, slug));
return navigateToCollection(collectionName);
} else {
return dispatch(loadEntry(collection, slug));
}
} catch (error) {
if (error.name === UNPUBLISHED_ENTRY_DISMISS_ERROR) return;
dispatch(
addNotification({
message: { key: 'ui.toast.onFailToPublishEntry', details: error },
Expand All @@ -549,11 +569,16 @@ export function publishUnpublishedEntry(
};
}

export function unpublishPublishedEntry(collection: Collection, slug: string) {
export function unpublishPublishedEntry(
collection: Collection,
slug: string,
customEntry?: EntryMap,
) {
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const backend = currentBackend(state.config);
const entry = selectEntry(state, collection.get('name'), slug);
const entry = customEntry || selectEntry(state, collection.get('name'), slug);
const isCustomEntry = customEntry && customEntry.get('isCustomEntry', true);
const entryDraft = Map().set('entry', entry) as unknown as EntryDraft;
dispatch(unpublishedEntryPersisting(collection, slug));
return backend
Expand All @@ -566,14 +591,16 @@ export function unpublishPublishedEntry(collection: Collection, slug: string) {
entryDraft,
assetProxies: [],
usedSlugs: List(),
status: status.get('PENDING_PUBLISH'),
status: customEntry ? customEntry.get('status') : status.get('PENDING_PUBLISH'),
});
}
})
.then(() => {
dispatch(unpublishedEntryPersisted(collection, entry));
dispatch(entryDeleted(collection, slug));
dispatch(loadUnpublishedEntry(collection, slug));
if (!isCustomEntry) {
dispatch(unpublishedEntryPersisted(collection, entry));
dispatch(loadUnpublishedEntry(collection, slug));
}
dispatch(
addNotification({
message: {
Expand All @@ -598,3 +625,20 @@ export function unpublishPublishedEntry(collection: Collection, slug: string) {
});
};
}

export function getUnpublishedEntries(collectionName?: string) {
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);

if (!entriesLoaded) {
await dispatch(loadUnpublishedEntries(state.collections));
}

const unpublishedEntries = state.editorialWorkflow.get('entities');
const entries = collectionName
? unpublishedEntries.filter(entry => entry.get('collection') === collectionName)
: unpublishedEntries;
return List(entries.valueSeq());
};
}
Loading
Loading