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 Aug 30, 2023
1 parent 29c9eeb commit 6dc9dbf
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 23 deletions.
10 changes: 7 additions & 3 deletions packages/live-preview-sdk/src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { sendMessageToEditor, isInsideIframe } from '../helpers';
import { ContentfulLivePreview } from '../index';
import { InspectorMode } from '../inspectorMode';
import { LiveUpdates } from '../liveUpdates';
import { TagAttributes } from '../types';
import { SubscriptionEvent, TagAttributes } from '../types';

vi.mock('../inspectorMode');
vi.mock('../liveUpdates');
Expand Down 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(SubscriptionEvent.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
66 changes: 55 additions & 11 deletions packages/live-preview-sdk/src/__tests__/liveUpdates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LIVE_PREVIEW_EDITOR_SOURCE } from '../constants';
import * as helpers from '../helpers';
import { LiveUpdates } from '../liveUpdates';
import { LivePreviewPostMessageMethods } from '../messages';
import { ContentType } from '../types';
import { ContentType, SubscriptionEvent } from '../types';
import assetFromEntryEditor from './fixtures/assetFromEntryEditor.json';
import landingPageContentType from './fixtures/landingPageContentType.json';
import nestedCollectionFromPreviewApp from './fixtures/nestedCollectionFromPreviewApp.json';
Expand Down Expand Up @@ -58,7 +58,7 @@ describe('LiveUpdates', () => {
const liveUpdates = new LiveUpdates({ locale });
const data = { sys: { id: '1' }, title: 'Data 1', __typename: 'Demo' };
const callback = vi.fn();
liveUpdates.subscribe({ data, callback });
liveUpdates.subscribe(SubscriptionEvent.Edit, { data, callback });

await liveUpdates.receiveMessage({
entity: updateFromEntryEditor1,
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('LiveUpdates', () => {
const data = { title: 'Data 1', __typename: 'Demo' };
const callback = vi.fn();

liveUpdates.subscribe({ data, callback });
liveUpdates.subscribe(SubscriptionEvent.Edit, { data, callback });

expect(helpers.debug.error).toHaveBeenCalledWith(
'Live Updates requires the "sys.id" to be present on the provided data',
Expand All @@ -112,7 +112,7 @@ describe('LiveUpdates', () => {
const liveUpdates = new LiveUpdates({ locale });
const data = { sys: { id: '1' }, title: 'Data 1', __typename: 'Demo' };
const callback = vi.fn();
const unsubscribe = liveUpdates.subscribe({ data, callback });
const unsubscribe = liveUpdates.subscribe(SubscriptionEvent.Edit, { data, callback });

await liveUpdates.receiveMessage({
entity: updateFromEntryEditor1,
Expand Down Expand Up @@ -145,7 +145,7 @@ describe('LiveUpdates', () => {
const liveUpdates = new LiveUpdates({ locale });
const data = { sys: { id: '1' }, title: 'Data 1', __typename: 'Demo' };
const callback = vi.fn();
liveUpdates.subscribe({ data, callback });
liveUpdates.subscribe(SubscriptionEvent.Edit, { data, callback });

await liveUpdates.receiveMessage({
isInspectorActive: false,
Expand All @@ -162,7 +162,7 @@ describe('LiveUpdates', () => {
const liveUpdates = new LiveUpdates({ locale });
const data = { sys: { id: '99' }, title: 'Data 1', __typename: 'Demo' };
const callback = vi.fn();
liveUpdates.subscribe({ data, locale, callback });
liveUpdates.subscribe(SubscriptionEvent.Edit, { data, locale, callback });

liveUpdates.receiveMessage({
entity: updateFromEntryEditor1,
Expand All @@ -180,7 +180,7 @@ describe('LiveUpdates', () => {
it('merges nested field updates', async () => {
const liveUpdates = new LiveUpdates({ locale });
const callback = vi.fn();
liveUpdates.subscribe({ data: nestedDataFromPreviewApp, callback });
liveUpdates.subscribe(SubscriptionEvent.Edit, { data: nestedDataFromPreviewApp, callback });
await liveUpdates.receiveMessage({
entity: assetFromEntryEditor as unknown as Asset,
action: LivePreviewPostMessageMethods.ENTRY_UPDATED,
Expand All @@ -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(SubscriptionEvent.Edit, {
data: nestedCollectionFromPreviewApp,
callback,
});
await liveUpdates.receiveMessage({
entity: pageInsideCollectionFromEntryEditor as unknown as Entry,
contentType: landingPageContentType as unknown as ContentType,
Expand Down Expand Up @@ -240,29 +243,31 @@ describe('LiveUpdates', () => {
const liveUpdates = new LiveUpdates({ locale });
const data = { sys: { id: '1' }, title: 'Data 1', __typename: 'Demo' };
const callback = vi.fn();
liveUpdates.subscribe({ data, callback });
liveUpdates.subscribe(SubscriptionEvent.Edit, { data, callback });

expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(LivePreviewPostMessageMethods.SUBSCRIBED, {
action: LivePreviewPostMessageMethods.SUBSCRIBED,
type: 'GQL',
locale,
entryId: '1',
event: 'edit',
});
});

it('sends a message to the editor for a subscription with REST data', () => {
const liveUpdates = new LiveUpdates({ locale });
const data = { sys: { id: '1' }, fields: { title: 'Data 1' } };
const callback = vi.fn();
liveUpdates.subscribe({ data, callback });
liveUpdates.subscribe(SubscriptionEvent.Edit, { data, callback });

expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(LivePreviewPostMessageMethods.SUBSCRIBED, {
action: LivePreviewPostMessageMethods.SUBSCRIBED,
type: 'REST',
locale,
entryId: '1',
event: 'edit',
});
});
});
Expand All @@ -274,11 +279,13 @@ describe('LiveUpdates', () => {
data,
locale,
callback: vi.fn(),
event: SubscriptionEvent.Edit,
sysId: id,
};
const liveUpdates = new LiveUpdates({ locale });

beforeEach(() => {
liveUpdates.subscribe(subscription);
liveUpdates.subscribe(SubscriptionEvent.Edit, subscription);
vi.clearAllMocks();
});

Expand All @@ -297,4 +304,41 @@ describe('LiveUpdates', () => {
expect(subscription.callback).not.toHaveBeenCalled();
});
});

describe('save event subscription', () => {
it('should call only the save event subscriptions with the saved entry', async () => {
const liveUpdates = new LiveUpdates({ locale });

const data1 = {
sys: { id: updateFromEntryEditor1.sys.id },
title: 'Title',
__typename: 'Foo',
};
const callback1 = vi.fn();
liveUpdates.subscribe(SubscriptionEvent.Save, { data: data1, callback: callback1 });

const data2 = { sys: { id: '2' }, title: 'Title', __typename: 'Foo' };
const callback2 = vi.fn();
liveUpdates.subscribe(SubscriptionEvent.Save, { data: data2, callback: callback2 });

const data3 = { sys: { id: '3' }, title: 'Title', __typename: 'Foo' };
const callback3 = vi.fn();
liveUpdates.subscribe(SubscriptionEvent.Save, { data: data3, callback: callback3 });

await liveUpdates.receiveMessage({
entity: updateFromEntryEditor1,
contentType,
from: 'live-preview',
method: LivePreviewPostMessageMethods.ENTRY_SAVED,
source: LIVE_PREVIEW_EDITOR_SOURCE,
entityReferenceMap: new Map(),
});

expect(callback1).toHaveBeenCalledTimes(1);
expect(callback1).toHaveBeenCalledWith(updateFromEntryEditor1);

expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
});
});
});
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
Loading

0 comments on commit 6dc9dbf

Please sign in to comment.