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 { serializeValues } from '../lib/serializeEntryValues';
|
||||
import { currentBackend } from '../backend';
|
||||
import { selectPublishedSlugs, selectUnpublishedSlugs, selectEntry } from '../reducers';
|
||||
import {
|
||||
selectPublishedSlugs,
|
||||
selectUnpublishedSlugs,
|
||||
selectEntry,
|
||||
selectUnpublishedEntry,
|
||||
} from '../reducers';
|
||||
import { selectFields } from '../reducers/collections';
|
||||
import { EDITORIAL_WORKFLOW, status, Status } from '../constants/publishModes';
|
||||
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 backend = currentBackend(state.config);
|
||||
const transactionID = uuid();
|
||||
const entry = selectUnpublishedEntry(state, collection, slug);
|
||||
dispatch(unpublishedEntryPublishRequest(collection, slug, transactionID));
|
||||
return backend
|
||||
.publishUnpublishedEntry(collection, slug)
|
||||
.publishUnpublishedEntry(entry)
|
||||
.then(() => {
|
||||
// re-load media after entry was published
|
||||
dispatch(loadMedia());
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
} from './reducers/collections';
|
||||
import { createEntry, EntryValue } from './valueObjects/Entry';
|
||||
import { sanitizeChar } from './lib/urlHelper';
|
||||
import { getBackend } from './lib/registry';
|
||||
import { getBackend, invokeEvent } from './lib/registry';
|
||||
import { commitMessageFormatter, slugFormatter, previewUrlFormatter } from './lib/formatters';
|
||||
import {
|
||||
localForage,
|
||||
@ -737,7 +737,27 @@ export class Backend {
|
||||
...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) {
|
||||
@ -803,8 +823,13 @@ export class Backend {
|
||||
return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus);
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.implementation.publishUnpublishedEntry!(collection, slug);
|
||||
async publishUnpublishedEntry(entry: EntryMap) {
|
||||
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) {
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { registerLocale, getLocale } from '../registry';
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('registry', () => {
|
||||
@ -10,6 +8,8 @@ describe('registry', () => {
|
||||
|
||||
describe('registerLocale', () => {
|
||||
it('should log error when name is empty', () => {
|
||||
const { registerLocale } = require('../registry');
|
||||
|
||||
registerLocale();
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
@ -18,6 +18,8 @@ describe('registry', () => {
|
||||
});
|
||||
|
||||
it('should log error when phrases are undefined', () => {
|
||||
const { registerLocale } = require('../registry');
|
||||
|
||||
registerLocale('fr');
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
@ -26,6 +28,8 @@ describe('registry', () => {
|
||||
});
|
||||
|
||||
it('should register locale', () => {
|
||||
const { registerLocale, getLocale } = require('../registry');
|
||||
|
||||
const phrases = {
|
||||
app: {
|
||||
header: {
|
||||
@ -39,4 +43,108 @@ describe('registry', () => {
|
||||
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 EditorComponent from 'ValueObjects/EditorComponent';
|
||||
|
||||
const allowedEvents = ['prePublish', 'postPublish'];
|
||||
const eventHandlers = {};
|
||||
allowedEvents.forEach(e => {
|
||||
eventHandlers[e] = [];
|
||||
});
|
||||
|
||||
/**
|
||||
* Global Registry Object
|
||||
*/
|
||||
@ -15,6 +21,7 @@ const registry = {
|
||||
widgetValueSerializers: {},
|
||||
mediaLibraries: [],
|
||||
locales: {},
|
||||
eventHandlers,
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -36,6 +43,10 @@ export default {
|
||||
getMediaLibrary,
|
||||
registerLocale,
|
||||
getLocale,
|
||||
registerEventListener,
|
||||
removeEventListener,
|
||||
getEventListeners,
|
||||
invokeEvent,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -181,6 +192,45 @@ export function getMediaLibrary(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
|
||||
*/
|
||||
|
@ -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.
|
||||
|
||||
## 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:
|
||||
|
||||
@ -377,3 +377,23 @@ Example config:
|
||||
config:
|
||||
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