Skip to content

Commit

Permalink
feat(live-preview): add functionality to subscribe to the save event …
Browse files Browse the repository at this point in the history
…of an entity
  • Loading branch information
chrishelgert committed Sep 4, 2023
1 parent 29c9eeb commit ac9a8bc
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 12 deletions.
8 changes: 6 additions & 2 deletions packages/live-preview-sdk/src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ describe('ContentfulLivePreview', () => {

// Check that the LiveUpdates.subscribe was called correctly
expect(subscribe).toHaveBeenCalledOnce();
expect(subscribe).toHaveBeenCalledWith({ data, locale: 'en-US', callback });
expect(subscribe).toHaveBeenCalledWith('edit', {
data,
locale: 'en-US',
callback,
});

// Updates from the subscribe fn will trigger the callback
subscribe.mock.lastCall?.[0].callback({ entity: { title: 'Hello' } });
subscribe.mock.lastCall?.[1].callback({ entity: { title: 'Hello' } });

expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith({ entity: { title: 'Hello' } });
Expand Down
9 changes: 8 additions & 1 deletion packages/live-preview-sdk/src/__tests__/liveUpdates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,10 @@ describe('LiveUpdates', () => {
it('merges nested collections', async () => {
const liveUpdates = new LiveUpdates({ locale });
const callback = vi.fn();
liveUpdates.subscribe({ data: nestedCollectionFromPreviewApp, callback });
liveUpdates.subscribe({
data: nestedCollectionFromPreviewApp,
callback,
});
await liveUpdates.receiveMessage({
entity: pageInsideCollectionFromEntryEditor as unknown as Entry,
contentType: landingPageContentType as unknown as ContentType,
Expand Down Expand Up @@ -248,6 +251,7 @@ describe('LiveUpdates', () => {
type: 'GQL',
locale,
entryId: '1',
event: 'edit',
});
});

Expand All @@ -263,6 +267,7 @@ describe('LiveUpdates', () => {
type: 'REST',
locale,
entryId: '1',
event: 'edit',
});
});
});
Expand All @@ -274,6 +279,8 @@ describe('LiveUpdates', () => {
data,
locale,
callback: vi.fn(),
event: 'edit',
sysId: id,
};
const liveUpdates = new LiveUpdates({ locale });

Expand Down
31 changes: 25 additions & 6 deletions packages/live-preview-sdk/src/helpers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,51 @@ function validation(d: Argument): { isGQL: boolean; sysId: string | null; isREST
}
}

type ValidationResult = (
| {
isValid: true;
sysId: string;
}
| {
isValid: false;
sysId: string | null;
}
) & { isGQL: boolean; isREST: boolean };

/**
* **Basic** validating of the subscribed data
* Is it GraphQL or REST and does it contain the sys information
*/
export function validateDataForLiveUpdates(data: Argument) {
let isValid = true;

export function validateDataForLiveUpdates(data: Argument): ValidationResult {
const { isGQL, sysId, isREST } = validation(data);

if (!sysId) {
isValid = false;
debug.error('Live Updates requires the "sys.id" to be present on the provided data', data);
return {
isValid: false,
sysId,
isGQL,
isREST,
};
}

if (!isGQL && !isREST) {
isValid = false;
debug.error(
'For live updates as a basic requirement the provided data must include the "fields" property for REST or "__typename" for Graphql, otherwise the data will be treated as invalid and live updates are not applied.',
data
);
return {
isValid: false,
sysId,
isGQL,
isREST,
};
}

return {
isGQL,
isREST,
sysId,
isValid,
isValid: true,
};
}
38 changes: 35 additions & 3 deletions packages/live-preview-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
SubscribeCallback,
TagAttributes,
} from './types';
import { getEntryList } from './utils';
import { SaveEvent } from './saveEvent';

export interface ContentfulLivePreviewInitConfig {
locale: string;
Expand All @@ -45,6 +47,7 @@ export class ContentfulLivePreview {
static initialized = false;
static inspectorMode: InspectorMode | null = null;
static liveUpdates: LiveUpdates | null = null;
static saveEvent: SaveEvent | null = null;
static inspectorModeEnabled = true;
static liveUpdatesEnabled = true;
static locale: string;
Expand Down Expand Up @@ -96,6 +99,7 @@ export class ContentfulLivePreview {

if (this.liveUpdatesEnabled) {
ContentfulLivePreview.liveUpdates = new LiveUpdates({ locale });
ContentfulLivePreview.saveEvent = new SaveEvent({ locale });
}

// bind event listeners for interactivity
Expand Down Expand Up @@ -150,20 +154,41 @@ export class ContentfulLivePreview {
}
}

static subscribe(config: ContentfulSubscribeConfig): VoidFunction {
static subscribe(config: ContentfulSubscribeConfig): VoidFunction;
static subscribe(
event: 'save',
config: Pick<ContentfulSubscribeConfig, 'callback'>
): VoidFunction;
static subscribe(event: 'edit', config: ContentfulSubscribeConfig): VoidFunction;
static subscribe(
configOrEvent: 'save' | 'edit' | ContentfulSubscribeConfig,
config?: ContentfulSubscribeConfig | Pick<ContentfulSubscribeConfig, 'callback'>
): VoidFunction {
if (!this.liveUpdatesEnabled) {
return () => {
/* noop */
};
}

const event = typeof configOrEvent === 'string' ? configOrEvent : 'edit';
const subscribeConfig = typeof configOrEvent === 'object' ? configOrEvent : config!;

if (event === 'save') {
if (!this.saveEvent) {
throw new Error(
'Save event is not initialized, please call `ContentfulLivePreview.init()` first.'
);
}
return this.saveEvent.subscribe(subscribeConfig.callback);
}

if (!this.liveUpdates) {
throw new Error(
'Live Updates are not initialized, please call `ContentfulLivePreview.init()` first.'
'Live updates are not initialized, please call `ContentfulLivePreview.init()` first.'
);
}

return this.liveUpdates.subscribe(config);
return this.liveUpdates.subscribe(subscribeConfig as ContentfulSubscribeConfig);
}

// Static method to render live preview data-attributes to HTML element output
Expand Down Expand Up @@ -201,6 +226,13 @@ export class ContentfulLivePreview {

openEntryInEditorUtility(fieldId, entryId, locale || this.locale);
}

/**
* Returns a list of tagged entries on the page
*/
static getEntryList(): string[] {
return getEntryList();
}
}

export { LIVE_PREVIEW_EDITOR_SOURCE, LIVE_PREVIEW_SDK_SOURCE } from './constants';
Expand Down
3 changes: 3 additions & 0 deletions packages/live-preview-sdk/src/liveUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Asset, Entry } from 'contentful';

import type {
ContentfulSubscribeConfig,
EntrySavedMessage,
EntryUpdatedMessage,
ErrorMessage,
MessageFromEditor,
Expand Down Expand Up @@ -296,6 +297,7 @@ export class LiveUpdates {

this.subscriptions.set(id, {
...config,
sysId,
gqlParams: config.query ? parseGraphQLParams(config.query) : undefined,
});

Expand All @@ -314,6 +316,7 @@ export class LiveUpdates {
type: isGQL ? 'GQL' : 'REST',
locale,
entryId: sysId,
event: 'edit',
} as SubscribedMessage);

return () => {
Expand Down
1 change: 1 addition & 0 deletions packages/live-preview-sdk/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export type SubscribedMessage = {
type: 'GQL' | 'REST';
entryId: string;
locale: string;
event: 'edit' | 'save';
};

export type ErrorMessage = {
Expand Down
47 changes: 47 additions & 0 deletions packages/live-preview-sdk/src/saveEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { debug } from './helpers';
import { EntrySavedMessage, LivePreviewPostMessageMethods, MessageFromEditor } from './messages';
import { SubscribeCallback } from './types';
import { getEntryList } from './utils';

export class SaveEvent {
locale: string;
subscription: SubscribeCallback | undefined;

constructor({ locale }: { locale: string }) {
this.locale = locale;
}

public subscribe(cb: SubscribeCallback): VoidFunction {
if (this.subscription) {
debug.log(
'There is already a subscription for the save event, the existing will be replaced.'
);
}

this.subscription = cb;

// TODO: How would we like to handle this for the subscribe event?
// sendMessageToEditor(LivePreviewPostMessageMethods.SUBSCRIBED, {
// action: LivePreviewPostMessageMethods.SUBSCRIBED,
// type: isGQL ? 'GQL' : 'REST', // we don't know that
// locale,
// entryId: sysId, // we don't have that
// event: 'save',
// } as SubscribedMessage);

return () => {
this.subscription = undefined;
};
}

public receiveMessage(message: MessageFromEditor): void {
if (message.method === LivePreviewPostMessageMethods.ENTRY_SAVED && this.subscription) {
const { entity } = message as EntrySavedMessage;
const entries = getEntryList();

if (entries.includes(entity.sys.id)) {
this.subscription(entity);
}
}
}
}
1 change: 1 addition & 0 deletions packages/live-preview-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,5 @@ export interface Subscription {
locale?: string;
callback: SubscribeCallback;
gqlParams?: GraphQLParams;
sysId: string;
}
14 changes: 14 additions & 0 deletions packages/live-preview-sdk/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TagAttributes } from './types';

/**
* Returns a list of tagged entries on the page
*/
export function getEntryList(): string[] {
return [
...new Set(
[...document.querySelectorAll(TagAttributes.ENTRY_ID)]
.map((element) => element.getAttribute(TagAttributes.ENTRY_ID))
.filter(Boolean) as string[]
),
];
}

0 comments on commit ac9a8bc

Please sign in to comment.