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 29, 2023
1 parent 29c9eeb commit 67672c8
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 66 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,
};
}
13 changes: 11 additions & 2 deletions packages/live-preview-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
InspectorModeTags,
LivePreviewProps,
SubscribeCallback,
SubscriptionEvent,
TagAttributes,
} from './types';

Expand Down Expand Up @@ -150,7 +151,12 @@ export class ContentfulLivePreview {
}
}

static subscribe(config: ContentfulSubscribeConfig): VoidFunction {
static subscribe(config: ContentfulSubscribeConfig): VoidFunction;
static subscribe(event: SubscriptionEvent, config: ContentfulSubscribeConfig): VoidFunction;
static subscribe(
configOrEvent: SubscriptionEvent | ContentfulSubscribeConfig,
config?: ContentfulSubscribeConfig
): VoidFunction {
if (!this.liveUpdatesEnabled) {
return () => {
/* noop */
Expand All @@ -163,7 +169,10 @@ export class ContentfulLivePreview {
);
}

return this.liveUpdates.subscribe(config);
const event = typeof configOrEvent === 'string' ? configOrEvent : SubscriptionEvent.Edit;
const subscribeConfig = typeof configOrEvent === 'object' ? configOrEvent : config!;

return this.liveUpdates.subscribe(event, subscribeConfig);
}

// Static method to render live preview data-attributes to HTML element output
Expand Down
Loading

0 comments on commit 67672c8

Please sign in to comment.