feat: add prePublish,postPublish events (#3172)
This commit is contained in:
parent
fd9e2c89f2
commit
0d7e36ba79
@ -6,7 +6,12 @@ import { ThunkDispatch } from 'redux-thunk';
|
|||||||
import { Map, List } from 'immutable';
|
import { Map, List } from 'immutable';
|
||||||
import { serializeValues } from '../lib/serializeEntryValues';
|
import { serializeValues } from '../lib/serializeEntryValues';
|
||||||
import { currentBackend } from '../backend';
|
import { currentBackend } from '../backend';
|
||||||
import { selectPublishedSlugs, selectUnpublishedSlugs, selectEntry } from '../reducers';
|
import {
|
||||||
|
selectPublishedSlugs,
|
||||||
|
selectUnpublishedSlugs,
|
||||||
|
selectEntry,
|
||||||
|
selectUnpublishedEntry,
|
||||||
|
} from '../reducers';
|
||||||
import { selectFields } from '../reducers/collections';
|
import { selectFields } from '../reducers/collections';
|
||||||
import { EDITORIAL_WORKFLOW, status, Status } from '../constants/publishModes';
|
import { EDITORIAL_WORKFLOW, status, Status } from '../constants/publishModes';
|
||||||
import { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
|
import { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
|
||||||
@ -513,9 +518,10 @@ export function publishUnpublishedEntry(collection: string, slug: string) {
|
|||||||
const collections = state.collections;
|
const collections = state.collections;
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
const transactionID = uuid();
|
const transactionID = uuid();
|
||||||
|
const entry = selectUnpublishedEntry(state, collection, slug);
|
||||||
dispatch(unpublishedEntryPublishRequest(collection, slug, transactionID));
|
dispatch(unpublishedEntryPublishRequest(collection, slug, transactionID));
|
||||||
return backend
|
return backend
|
||||||
.publishUnpublishedEntry(collection, slug)
|
.publishUnpublishedEntry(entry)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// re-load media after entry was published
|
// re-load media after entry was published
|
||||||
dispatch(loadMedia());
|
dispatch(loadMedia());
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
} from './reducers/collections';
|
} from './reducers/collections';
|
||||||
import { createEntry, EntryValue } from './valueObjects/Entry';
|
import { createEntry, EntryValue } from './valueObjects/Entry';
|
||||||
import { sanitizeChar } from './lib/urlHelper';
|
import { sanitizeChar } from './lib/urlHelper';
|
||||||
import { getBackend } from './lib/registry';
|
import { getBackend, invokeEvent } from './lib/registry';
|
||||||
import { commitMessageFormatter, slugFormatter, previewUrlFormatter } from './lib/formatters';
|
import { commitMessageFormatter, slugFormatter, previewUrlFormatter } from './lib/formatters';
|
||||||
import {
|
import {
|
||||||
localForage,
|
localForage,
|
||||||
@ -737,7 +737,27 @@ export class Backend {
|
|||||||
...updatedOptions,
|
...updatedOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.implementation.persistEntry(entryObj, assetProxies, opts).then(() => entryObj.slug);
|
if (!useWorkflow) {
|
||||||
|
await this.invokePrePublishEvent(entryDraft.get('entry'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.implementation.persistEntry(entryObj, assetProxies, opts);
|
||||||
|
|
||||||
|
if (!useWorkflow) {
|
||||||
|
await this.invokePostPublishEvent(entryDraft.get('entry'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entryObj.slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
async invokePrePublishEvent(entry: EntryMap) {
|
||||||
|
const { login, name } = (await this.currentUser()) as User;
|
||||||
|
await invokeEvent({ name: 'prePublish', data: { entry, author: { login, name } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async invokePostPublishEvent(entry: EntryMap) {
|
||||||
|
const { login, name } = (await this.currentUser()) as User;
|
||||||
|
await invokeEvent({ name: 'postPublish', data: { entry, author: { login, name } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async persistMedia(config: Config, file: AssetProxy) {
|
async persistMedia(config: Config, file: AssetProxy) {
|
||||||
@ -803,8 +823,13 @@ export class Backend {
|
|||||||
return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus);
|
return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
publishUnpublishedEntry(collection: string, slug: string) {
|
async publishUnpublishedEntry(entry: EntryMap) {
|
||||||
return this.implementation.publishUnpublishedEntry!(collection, slug);
|
const collection = entry.get('collection');
|
||||||
|
const slug = entry.get('slug');
|
||||||
|
|
||||||
|
await this.invokePrePublishEvent(entry);
|
||||||
|
await this.implementation.publishUnpublishedEntry!(collection, slug);
|
||||||
|
await this.invokePostPublishEvent(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { registerLocale, getLocale } from '../registry';
|
|
||||||
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
describe('registry', () => {
|
describe('registry', () => {
|
||||||
@ -10,6 +8,8 @@ describe('registry', () => {
|
|||||||
|
|
||||||
describe('registerLocale', () => {
|
describe('registerLocale', () => {
|
||||||
it('should log error when name is empty', () => {
|
it('should log error when name is empty', () => {
|
||||||
|
const { registerLocale } = require('../registry');
|
||||||
|
|
||||||
registerLocale();
|
registerLocale();
|
||||||
expect(console.error).toHaveBeenCalledTimes(1);
|
expect(console.error).toHaveBeenCalledTimes(1);
|
||||||
expect(console.error).toHaveBeenCalledWith(
|
expect(console.error).toHaveBeenCalledWith(
|
||||||
@ -18,6 +18,8 @@ describe('registry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should log error when phrases are undefined', () => {
|
it('should log error when phrases are undefined', () => {
|
||||||
|
const { registerLocale } = require('../registry');
|
||||||
|
|
||||||
registerLocale('fr');
|
registerLocale('fr');
|
||||||
expect(console.error).toHaveBeenCalledTimes(1);
|
expect(console.error).toHaveBeenCalledTimes(1);
|
||||||
expect(console.error).toHaveBeenCalledWith(
|
expect(console.error).toHaveBeenCalledWith(
|
||||||
@ -26,6 +28,8 @@ describe('registry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should register locale', () => {
|
it('should register locale', () => {
|
||||||
|
const { registerLocale, getLocale } = require('../registry');
|
||||||
|
|
||||||
const phrases = {
|
const phrases = {
|
||||||
app: {
|
app: {
|
||||||
header: {
|
header: {
|
||||||
@ -39,4 +43,108 @@ describe('registry', () => {
|
|||||||
expect(getLocale('de')).toBe(phrases);
|
expect(getLocale('de')).toBe(phrases);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('eventHandlers', () => {
|
||||||
|
const events = ['prePublish', 'postPublish'];
|
||||||
|
|
||||||
|
describe('registerEventListener', () => {
|
||||||
|
it('should throw error on invalid event', () => {
|
||||||
|
const { registerEventListener } = require('../registry');
|
||||||
|
|
||||||
|
expect(() => registerEventListener({ name: 'unknown' })).toThrow(
|
||||||
|
new Error("Invalid event name 'unknown'"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.forEach(name => {
|
||||||
|
it(`should register '${name}' event`, () => {
|
||||||
|
const { registerEventListener, getEventListeners } = require('../registry');
|
||||||
|
|
||||||
|
const handler = jest.fn();
|
||||||
|
registerEventListener({ name, handler });
|
||||||
|
|
||||||
|
expect(getEventListeners(name)).toEqual([{ handler, options: {} }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeEventListener', () => {
|
||||||
|
it('should throw error on invalid event', () => {
|
||||||
|
const { removeEventListener } = require('../registry');
|
||||||
|
|
||||||
|
expect(() => removeEventListener({ name: 'unknown' })).toThrow(
|
||||||
|
new Error("Invalid event name 'unknown'"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.forEach(name => {
|
||||||
|
it(`should remove '${name}' event by handler`, () => {
|
||||||
|
const {
|
||||||
|
registerEventListener,
|
||||||
|
getEventListeners,
|
||||||
|
removeEventListener,
|
||||||
|
} = require('../registry');
|
||||||
|
|
||||||
|
const handler1 = jest.fn();
|
||||||
|
const handler2 = jest.fn();
|
||||||
|
registerEventListener({ name, handler: handler1 });
|
||||||
|
registerEventListener({ name, handler: handler2 });
|
||||||
|
|
||||||
|
expect(getEventListeners(name)).toHaveLength(2);
|
||||||
|
|
||||||
|
removeEventListener({ name, handler: handler1 });
|
||||||
|
|
||||||
|
expect(getEventListeners(name)).toEqual([{ handler: handler2, options: {} }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
events.forEach(name => {
|
||||||
|
it(`should remove '${name}' event by name`, () => {
|
||||||
|
const {
|
||||||
|
registerEventListener,
|
||||||
|
getEventListeners,
|
||||||
|
removeEventListener,
|
||||||
|
} = require('../registry');
|
||||||
|
|
||||||
|
const handler1 = jest.fn();
|
||||||
|
const handler2 = jest.fn();
|
||||||
|
registerEventListener({ name, handler: handler1 });
|
||||||
|
registerEventListener({ name, handler: handler2 });
|
||||||
|
|
||||||
|
expect(getEventListeners(name)).toHaveLength(2);
|
||||||
|
|
||||||
|
removeEventListener({ name });
|
||||||
|
|
||||||
|
expect(getEventListeners(name)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('invokeEvent', () => {
|
||||||
|
it('should throw error on invalid event', async () => {
|
||||||
|
const { invokeEvent } = require('../registry');
|
||||||
|
|
||||||
|
await expect(invokeEvent({ name: 'unknown', data: {} })).rejects.toThrow(
|
||||||
|
new Error("Invalid event name 'unknown'"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
events.forEach(name => {
|
||||||
|
it(`should invoke '${name}' event with data`, async () => {
|
||||||
|
const { registerEventListener, invokeEvent } = require('../registry');
|
||||||
|
|
||||||
|
const options = { hello: 'world' };
|
||||||
|
const handler = jest.fn();
|
||||||
|
|
||||||
|
registerEventListener({ name, handler }, options);
|
||||||
|
|
||||||
|
const data = { entry: {} };
|
||||||
|
await invokeEvent({ name, data });
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handler).toHaveBeenCalledWith(data, options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,12 @@ import produce from 'immer';
|
|||||||
import { oneLine } from 'common-tags';
|
import { oneLine } from 'common-tags';
|
||||||
import EditorComponent from 'ValueObjects/EditorComponent';
|
import EditorComponent from 'ValueObjects/EditorComponent';
|
||||||
|
|
||||||
|
const allowedEvents = ['prePublish', 'postPublish'];
|
||||||
|
const eventHandlers = {};
|
||||||
|
allowedEvents.forEach(e => {
|
||||||
|
eventHandlers[e] = [];
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global Registry Object
|
* Global Registry Object
|
||||||
*/
|
*/
|
||||||
@ -15,6 +21,7 @@ const registry = {
|
|||||||
widgetValueSerializers: {},
|
widgetValueSerializers: {},
|
||||||
mediaLibraries: [],
|
mediaLibraries: [],
|
||||||
locales: {},
|
locales: {},
|
||||||
|
eventHandlers,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -36,6 +43,10 @@ export default {
|
|||||||
getMediaLibrary,
|
getMediaLibrary,
|
||||||
registerLocale,
|
registerLocale,
|
||||||
getLocale,
|
getLocale,
|
||||||
|
registerEventListener,
|
||||||
|
removeEventListener,
|
||||||
|
getEventListeners,
|
||||||
|
invokeEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,6 +192,45 @@ export function getMediaLibrary(name) {
|
|||||||
return registry.mediaLibraries.find(ml => ml.name === name);
|
return registry.mediaLibraries.find(ml => ml.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateEventName(name) {
|
||||||
|
if (!allowedEvents.includes(name)) {
|
||||||
|
throw new Error(`Invalid event name '${name}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEventListeners(name) {
|
||||||
|
validateEventName(name);
|
||||||
|
return [...registry.eventHandlers[name]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerEventListener({ name, handler }, options = {}) {
|
||||||
|
validateEventName(name);
|
||||||
|
registry.eventHandlers[name].push({ handler, options });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invokeEvent({ name, data }) {
|
||||||
|
validateEventName(name);
|
||||||
|
const handlers = registry.eventHandlers[name];
|
||||||
|
for (const { handler, options } of handlers) {
|
||||||
|
try {
|
||||||
|
await handler(data, options);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed running handler for event ${name} with message: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeEventListener({ name, handler }) {
|
||||||
|
validateEventName(name);
|
||||||
|
if (handler) {
|
||||||
|
registry.eventHandlers[name] = registry.eventHandlers[name].filter(
|
||||||
|
item => item.handler !== handler,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
registry.eventHandlers[name] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Locales
|
* Locales
|
||||||
*/
|
*/
|
||||||
|
@ -27,7 +27,7 @@ backend:
|
|||||||
|
|
||||||
> `netlify-cms-proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development.
|
> `netlify-cms-proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development.
|
||||||
|
|
||||||
## GitLab and BitBucket editorial workflow support
|
## GitLab and BitBucket Editorial Workflow Support
|
||||||
|
|
||||||
You can enable the Editorial Workflow with the following line in your Netlify CMS `config.yml` file:
|
You can enable the Editorial Workflow with the following line in your Netlify CMS `config.yml` file:
|
||||||
|
|
||||||
@ -377,3 +377,23 @@ Example config:
|
|||||||
config:
|
config:
|
||||||
max_file_size: 512000 # in bytes, only for default media library
|
max_file_size: 512000 # in bytes, only for default media library
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Registering to CMS Events
|
||||||
|
|
||||||
|
You can execute a function when a specific CMS event occurs.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
CMS.registerEventListener({
|
||||||
|
name: 'prePublish',
|
||||||
|
handler: ({ author, entry }) => console.log(JSON.stringify({ author, data: entry.get('data') })),
|
||||||
|
});
|
||||||
|
|
||||||
|
CMS.registerEventListener({
|
||||||
|
name: 'postPublish',
|
||||||
|
handler: ({ author, entry }) => console.log(JSON.stringify({ author, data: entry.get('data') })),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> Supported events are `prePublish` and `postPublish`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user