feat: improve cms event types (#769)

This commit is contained in:
Daniel Lautzenheiser 2023-04-26 15:12:17 -04:00 committed by GitHub
parent b967b61e51
commit 4444dd791c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 86 additions and 30 deletions

View File

@ -59,6 +59,7 @@ import type {
Entry, Entry,
EntryData, EntryData,
EntryDraft, EntryDraft,
EventData,
FilterRule, FilterRule,
ImplementationEntry, ImplementationEntry,
MediaField, MediaField,
@ -68,7 +69,6 @@ import type {
UnknownField, UnknownField,
User, User,
} from './interface'; } from './interface';
import type { AllowedEvent } from './lib/registry';
import type { AsyncLock } from './lib/util'; import type { AsyncLock } from './lib/util';
import type { RootState } from './store'; import type { RootState } from './store';
import type AssetProxy from './valueObjects/AssetProxy'; import type AssetProxy from './valueObjects/AssetProxy';
@ -908,17 +908,19 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
return slug; return slug;
} }
async invokeEventWithEntry(event: AllowedEvent, entry: Entry) { async getEventData(entry: Entry): Promise<EventData> {
const { login, name = '' } = (await this.currentUser()) as User; const { login, name = '' } = (await this.currentUser()) as User;
return await invokeEvent({ name: event, data: { entry, author: { login, name } } }); return { entry, author: { login, name } };
} }
async invokePreSaveEvent(entry: Entry) { async invokePreSaveEvent(entry: Entry): Promise<EntryData> {
return await this.invokeEventWithEntry('preSave', entry); const eventData = await this.getEventData(entry);
return await invokeEvent('preSave', eventData);
} }
async invokePostSaveEvent(entry: Entry) { async invokePostSaveEvent(entry: Entry): Promise<void> {
await this.invokeEventWithEntry('postSave', entry); const eventData = await this.getEventData(entry);
await invokeEvent('postSave', eventData);
} }
async persistMedia(config: Config, file: AssetProxy) { async persistMedia(config: Config, file: AssetProxy) {

View File

@ -232,7 +232,7 @@ const App = ({
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
invokeEvent({ name: 'mounted' }); invokeEvent('mounted');
}); });
}, []); }, []);

View File

@ -825,16 +825,32 @@ export interface EventData {
author: { login: string | undefined; name: string }; author: { login: string | undefined; name: string };
} }
export type EventListenerOptions = Record<string, unknown>; export type PreSaveEventHandler<O extends Record<string, unknown> = Record<string, unknown>> = (
data: EventData,
options: O,
) => EntryData | undefined | null | void | Promise<EntryData | undefined | null | void>;
export type EventListenerHandler = ( export type PostSaveEventHandler<O extends Record<string, unknown> = Record<string, unknown>> = (
data: EventData | undefined, data: EventData,
options: EventListenerOptions, options: O,
) => Promise<EntryData | undefined | null | void>; ) => void | Promise<void>;
export interface EventListener { export type MountedEventHandler<O extends Record<string, unknown> = Record<string, unknown>> = (
name: AllowedEvent; options: O,
handler: EventListenerHandler; ) => void | Promise<void>;
export type EventHandlers<O extends Record<string, unknown> = Record<string, unknown>> = {
preSave: PreSaveEventHandler<O>;
postSave: PostSaveEventHandler<O>;
mounted: MountedEventHandler<O>;
};
export interface EventListener<
E extends AllowedEvent = 'mounted',
O extends Record<string, unknown> = Record<string, unknown>,
> {
name: E;
handler: EventHandlers<O>[E];
} }
export interface AdditionalLinkOptions { export interface AdditionalLinkOptions {

View File

@ -9,11 +9,15 @@ import type {
Config, Config,
CustomIcon, CustomIcon,
Entry, Entry,
EntryData,
EventData, EventData,
EventListener, EventListener,
FieldPreviewComponent, FieldPreviewComponent,
LocalePhrasesRoot, LocalePhrasesRoot,
MountedEventHandler,
ObjectValue, ObjectValue,
PostSaveEventHandler,
PreSaveEventHandler,
PreviewStyle, PreviewStyle,
PreviewStyleOptions, PreviewStyleOptions,
ShortcodeConfig, ShortcodeConfig,
@ -29,10 +33,16 @@ import type {
export const allowedEvents = ['mounted', 'preSave', 'postSave'] as const; export const allowedEvents = ['mounted', 'preSave', 'postSave'] as const;
export type AllowedEvent = (typeof allowedEvents)[number]; export type AllowedEvent = (typeof allowedEvents)[number];
type EventHandlerRegistry = {
preSave: { handler: PreSaveEventHandler; options: Record<string, unknown> }[];
postSave: { handler: PostSaveEventHandler; options: Record<string, unknown> }[];
mounted: { handler: MountedEventHandler; options: Record<string, unknown> }[];
};
const eventHandlers = allowedEvents.reduce((acc, e) => { const eventHandlers = allowedEvents.reduce((acc, e) => {
acc[e] = []; acc[e] = [];
return acc; return acc;
}, {} as Record<AllowedEvent, { handler: EventListener['handler']; options: Record<string, unknown> }[]>); }, {} as EventHandlerRegistry);
interface Registry { interface Registry {
backends: Record<string, BackendInitializer>; backends: Record<string, BackendInitializer>;
@ -44,7 +54,7 @@ interface Registry {
additionalLinks: Record<string, AdditionalLink>; additionalLinks: Record<string, AdditionalLink>;
widgetValueSerializers: Record<string, WidgetValueSerializer>; widgetValueSerializers: Record<string, WidgetValueSerializer>;
locales: Record<string, LocalePhrasesRoot>; locales: Record<string, LocalePhrasesRoot>;
eventHandlers: typeof eventHandlers; eventHandlers: EventHandlerRegistry;
previewStyles: PreviewStyle[]; previewStyles: PreviewStyle[];
/** Markdown editor */ /** Markdown editor */
@ -309,9 +319,8 @@ export function getBackend<EF extends BaseField = UnknownField>(
/** /**
* Event Handlers * Event Handlers
*/ */
function validateEventName(name: string) { function validateEventName(name: AllowedEvent) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (!allowedEvents.includes(name)) {
if (!allowedEvents.includes(name as any)) {
throw new Error(`Invalid event name '${name}'`); throw new Error(`Invalid event name '${name}'`);
} }
} }
@ -321,21 +330,48 @@ export function getEventListeners(name: AllowedEvent) {
return [...registry.eventHandlers[name]]; return [...registry.eventHandlers[name]];
} }
export function registerEventListener( export function registerEventListener<
{ name, handler }: EventListener, E extends AllowedEvent,
options: Record<string, unknown> = {}, O extends Record<string, unknown> = Record<string, unknown>,
) { >({ name, handler }: EventListener<E, O>, options?: O) {
validateEventName(name); validateEventName(name);
registry.eventHandlers[name].push({ handler, options }); registry.eventHandlers[name].push({
handler: handler as MountedEventHandler & PreSaveEventHandler & PostSaveEventHandler,
options: options ?? {},
});
} }
export async function invokeEvent({ name, data }: { name: AllowedEvent; data?: EventData }) { export async function invokeEvent(name: 'preSave', data: EventData): Promise<EntryData>;
export async function invokeEvent(name: 'postSave', data: EventData): Promise<void>;
export async function invokeEvent(name: 'mounted'): Promise<void>;
export async function invokeEvent(name: AllowedEvent, data?: EventData): Promise<void | EntryData> {
validateEventName(name); validateEventName(name);
if (name === 'mounted') {
console.info('[StaticCMS] Firing mounted event');
const handlers = registry.eventHandlers[name];
for (const { handler, options } of handlers) {
handler(options);
}
return;
}
if (name === 'postSave') {
console.info(`[StaticCMS] Firing post save event`, data);
const handlers = registry.eventHandlers[name];
for (const { handler, options } of handlers) {
handler(data!, options);
}
return;
}
const handlers = registry.eventHandlers[name]; const handlers = registry.eventHandlers[name];
console.info(`[StaticCMS] Firing event ${name}`, data); console.info(`[StaticCMS] Firing pre save event`, data);
let _data = data ? { ...data } : undefined; let _data = { ...data! };
for (const { handler, options } of handlers) { for (const { handler, options } of handlers) {
const result = await handler(_data, options); const result = await handler(_data, options);
if (_data !== undefined && result !== undefined) { if (_data !== undefined && result !== undefined) {
@ -346,7 +382,8 @@ export async function invokeEvent({ name, data }: { name: AllowedEvent; data?: E
_data = { ..._data, entry }; _data = { ..._data, entry };
} }
} }
return _data?.entry.data;
return _data.entry.data;
} }
export function removeEventListener({ name, handler }: EventListener) { export function removeEventListener({ name, handler }: EventListener) {

View File

@ -17,6 +17,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti
| Name | Type | Default | Description | | Name | Type | Default | Description |
| ---------------- | ------- | ------- | -------------------------------------------------------------------------------------- | | ---------------- | ------- | ------- | -------------------------------------------------------------------------------------- |
| allow_regenerate | boolean | `true` | _Optional_. If set to `false` the `Generate new UUID` button does not appear in the UI | | allow_regenerate | boolean | `true` | _Optional_. If set to `false` the `Generate new UUID` button does not appear in the UI |
| prefix | string | | _Optional_. A value to add at the beginning of the UUID |
## Example ## Example