From cf339bb8f3256d235bd1b70a9947925fab7986cc Mon Sep 17 00:00:00 2001 From: Chris Helgert Date: Tue, 14 Mar 2023 16:11:36 +0100 Subject: [PATCH 1/6] feat: receive live updates from the editor [TOL-1011] --- README.md | 10 ++- package.json | 5 +- src/field-tagging.ts | 14 ++-- src/index.ts | 30 +++++++- src/live-updates.ts | 50 ++++++++++++ src/react.ts | 26 +++++++ src/tests/field-tagging.spec.ts | 29 +++++++ src/tests/getProps.spec.ts | 23 ------ src/tests/index.spec.ts | 96 ++++++++++++++++++++++++ src/tests/init.spec.ts | 6 +- src/tests/live-updates.spec.ts | 43 +++++++++++ src/tests/react.spec.ts | 72 ++++++++++++++++++ src/tests/resolveIncomingMessage.spec.ts | 36 --------- vite.config.ts | 7 +- yarn.lock | 50 ++++++++++-- 15 files changed, 411 insertions(+), 86 deletions(-) create mode 100644 src/live-updates.ts create mode 100644 src/react.ts create mode 100644 src/tests/field-tagging.spec.ts delete mode 100644 src/tests/getProps.spec.ts create mode 100644 src/tests/index.spec.ts create mode 100644 src/tests/live-updates.spec.ts create mode 100644 src/tests/react.spec.ts delete mode 100644 src/tests/resolveIncomingMessage.spec.ts diff --git a/README.md b/README.md index 4923f6b1..7b6081b6 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,15 @@ import '@contentful/live-preview/dist/style.css'; ### Live Updates -[Documentation on live updates] +Live Updates from the editor to your applications are currently only supported for [React.js](https://reactjs.org/) + +```tsx +import { useContentfulLiveUpdates } from "@contentful/live-previews/dist/react" + +// ... +const updated = useContentfulLiveUpdates(originalData, locale) +// ... +``` ## Code of Conduct diff --git a/package.json b/package.json index d45a58d4..8740c1bc 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "license": "MIT", "description": "Preview SDK for both the field tagging connection + live content updates", "source": "./src/index.tsx", - "main": "./dist/live-preview.cjs", - "module": "dist/live-preview.js", + "main": "./dist/index.cjs", + "module": "dist/index.js", "typings": "dist/index.d.ts", "type": "module", "files": [ @@ -39,6 +39,7 @@ "@commitlint/config-conventional": "^17.4.4", "@contentful/eslint-config-extension": "0.4.3", "@semantic-release/changelog": "^6.0.2", + "@testing-library/react": "14.0.0", "@types/node": "^18.14.0", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", diff --git a/src/field-tagging.ts b/src/field-tagging.ts index 98fe0ddf..6e123315 100644 --- a/src/field-tagging.ts +++ b/src/field-tagging.ts @@ -8,7 +8,7 @@ import { } from './constants'; import { TagAttributes } from './types'; -export default class FieldTagging { +export class ContentfulFieldTagging { private tooltip: HTMLButtonElement | null = null; // this tooltip scrolls to the correct field in the entry editor private currentElementBesideTooltip: HTMLElement | null = null; // this element helps to position the tooltip @@ -16,24 +16,22 @@ export default class FieldTagging { this.tooltip = null; this.currentElementBesideTooltip = null; - this.resolveIncomingMessage = this.resolveIncomingMessage.bind(this); this.updateTooltipPosition = this.updateTooltipPosition.bind(this); this.addTooltipOnHover = this.addTooltipOnHover.bind(this); this.createTooltip = this.createTooltip.bind(this); this.clickHandler = this.clickHandler.bind(this); this.createTooltip(); - window.addEventListener('message', this.resolveIncomingMessage); window.addEventListener('scroll', this.updateTooltipPosition); window.addEventListener('mouseover', this.addTooltipOnHover); } // Handles incoming messages from Contentful - private resolveIncomingMessage(e: MessageEvent) { - if (typeof e.data !== 'object') return; - if (e.data.from !== 'live-preview') return; - // Toggle the contentful-inspector--active class on the body element based on the isInspectorActive boolean - document.body.classList.toggle('contentful-inspector--active', e.data.isInspectorActive); + public receiveMessage(data: Record): void { + if (typeof data.isInspectorActive === 'boolean') { + // Toggle the contentful-inspector--active class on the body element based on the isInspectorActive boolean + document.body.classList.toggle('contentful-inspector--active', data.isInspectorActive); + } } // Updates the position of the tooltip diff --git a/src/index.ts b/src/index.ts index 0ec55ff2..46f23cd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,24 +1,46 @@ import './styles.css'; -import FieldTagging from './field-tagging'; +import { ContentfulFieldTagging } from './field-tagging'; +import { ContentfulLiveUpdates, type Entity, type SubscribeCallback } from './live-updates'; import { LivePreviewProps, TagAttributes } from './types'; export class ContentfulLivePreview { - static fieldTagging: FieldTagging | null = null; + static fieldTagging: ContentfulFieldTagging | null = null; + static liveUpdates: ContentfulLiveUpdates | null = null; // Static method to initialize the LivePreview SDK - static init(): Promise | undefined { + static init(): Promise | undefined { // Check if running in a browser environment if (typeof window !== 'undefined') { if (ContentfulLivePreview.fieldTagging) { console.log('You have already initialized the Live Preview SDK.'); return Promise.resolve(ContentfulLivePreview.fieldTagging); } else { - ContentfulLivePreview.fieldTagging = new FieldTagging(); + ContentfulLivePreview.fieldTagging = new ContentfulFieldTagging(); + ContentfulLivePreview.liveUpdates = new ContentfulLiveUpdates(); + + window.addEventListener('message', (event) => { + if (typeof event.data !== 'object' || !event.data) return; + if (event.data.from !== 'live-preview') return; + + ContentfulLivePreview.fieldTagging?.receiveMessage(event.data); + ContentfulLivePreview.liveUpdates?.receiveMessage(event.data); + }); + return Promise.resolve(ContentfulLivePreview.fieldTagging); } } } + static subscribe(data: Entity, locale: string, callback: SubscribeCallback): VoidFunction { + if (!this.liveUpdates) { + throw new Error( + 'Live Updates are not initialized, please call `ContentfulLivePreview.init()` first.' + ); + } + + return this.liveUpdates.subscribe(data, locale, callback); + } + // Static method to render live preview data-attributes to HTML element output static getProps({ fieldId, diff --git a/src/live-updates.ts b/src/live-updates.ts new file mode 100644 index 00000000..c2720913 --- /dev/null +++ b/src/live-updates.ts @@ -0,0 +1,50 @@ +export type Entity = Record; +export type Argument = Entity | Entity[]; +export type SubscribeCallback = (data: Argument) => void; + +/** + * LiveUpdates for the Contentful Live Preview mode + * receives the updated Entity from the Editor and merges them together with the provided data + */ +export class ContentfulLiveUpdates { + private subscriptions: { id: number; data: Argument; locale: string; cb: SubscribeCallback }[] = + []; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private mergeGraphQL(initial: Argument, locale: string, updated: Entity): Argument { + // TODO: https://contentful.atlassian.net/browse/TOL-1000 + // TODO: https://contentful.atlassian.net/browse/TOL-1022 + return initial; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private mergeRest(initial: Argument, locale: string, updated: Entity): Argument { + // TODO: https://contentful.atlassian.net/browse/TOL-1033 + // TODO: https://contentful.atlassian.net/browse/TOL-1025 + return initial; + } + + private merge(initial: Argument, locale: string, updated: Entity): Argument { + if ('__typename' in initial) { + return this.mergeGraphQL(initial, locale, updated); + } + return this.mergeRest(initial, locale, updated); + } + + /** Receives the data from the message event handler and calls the subscriptions */ + public receiveMessage({ entity }: Record): void { + if (entity && typeof entity === 'object') { + this.subscriptions.forEach((s) => s.cb(this.merge(s.data, s.locale, entity as Entity))); + } + } + + /** Subscribe to data changes from the Editor, returns a function to unsubscribe */ + public subscribe(data: Argument, locale: string, cb: SubscribeCallback): VoidFunction { + const id = this.subscriptions.length + 1; + this.subscriptions.push({ data, locale, cb, id }); + + return () => { + this.subscriptions = this.subscriptions.filter((f) => f.id !== id); + }; + } +} diff --git a/src/react.ts b/src/react.ts new file mode 100644 index 00000000..9891c22f --- /dev/null +++ b/src/react.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from 'react'; + +import { ContentfulLivePreview } from '.'; +import { Entity } from './live-updates'; + +export function useContentfulLiveUpdates( + data: T, + locale: string +): T { + const [state, setState] = useState(data); + + useEffect(() => { + // update content from external + setState(data); + // nothing to merge if there are no data + if (!data) { + return; + } + // or update content through live udates + return ContentfulLivePreview.subscribe(data, locale, (data) => { + setState(data as T); + }); + }, [JSON.stringify(data)]); // eslint-disable-line react-hooks/exhaustive-deps + + return state; +} diff --git a/src/tests/field-tagging.spec.ts b/src/tests/field-tagging.spec.ts new file mode 100644 index 00000000..09c2f211 --- /dev/null +++ b/src/tests/field-tagging.spec.ts @@ -0,0 +1,29 @@ +// @vitest-environment jsdom +import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; +import { ContentfulFieldTagging } from '../field-tagging'; + +describe('FieldTagging', () => { + let fieldTagging: ContentfulFieldTagging; + + beforeEach(() => { + fieldTagging = new ContentfulFieldTagging(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('receiveMessage', () => { + test("shouldn't change anything if the incoming message doesnt contain 'isInspectorActive'", () => { + const spy = vi.spyOn(document.body.classList, 'toggle'); + fieldTagging.receiveMessage({ entitiy: {} }); + expect(spy).not.toHaveBeenCalled(); + }); + + test('should toggle "contentful-inspector--active" class on document.body based on value of isInspectorActive', () => { + const spy = vi.spyOn(document.body.classList, 'toggle'); + fieldTagging.receiveMessage({ from: 'live-preview', isInspectorActive: true }); + expect(spy).toHaveBeenCalledWith('contentful-inspector--active', true); + }); + }); +}); diff --git a/src/tests/getProps.spec.ts b/src/tests/getProps.spec.ts deleted file mode 100644 index 524e9d29..00000000 --- a/src/tests/getProps.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { ContentfulLivePreview } from '../index'; -import { TagAttributes } from '../types'; - -describe('getProps', () => { - it('returns the expected props with a given entryId, fieldId and locale', () => { - const entryId = 'test-entry-id'; - const fieldId = 'test-field-id'; - const locale = 'test-locale'; - - const result = ContentfulLivePreview.getProps({ - entryId, - fieldId, - locale, - }); - - expect(result).toStrictEqual({ - [TagAttributes.FIELD_ID]: fieldId, - [TagAttributes.ENTRY_ID]: entryId, - [TagAttributes.LOCALE]: locale, - }); - }); -}); diff --git a/src/tests/index.spec.ts b/src/tests/index.spec.ts new file mode 100644 index 00000000..ac6ce572 --- /dev/null +++ b/src/tests/index.spec.ts @@ -0,0 +1,96 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, Mock, afterEach, beforeEach, beforeAll } from 'vitest'; +import { ContentfulLivePreview } from '../index'; +import { TagAttributes } from '../types'; + +import { ContentfulFieldTagging } from '../field-tagging'; +import { ContentfulLiveUpdates } from '../live-updates'; + +vi.mock('../field-tagging'); +vi.mock('../live-updates'); + +describe('ContentfulLivePreview', () => { + const receiveMessageTagging = vi.fn(); + const receiveMessageUpdates = vi.fn(); + const subscribe = vi.fn(); + + (ContentfulFieldTagging as Mock).mockImplementation(() => ({ + receiveMessage: receiveMessageTagging, + })); + (ContentfulLiveUpdates as Mock).mockImplementation(() => ({ + receiveMessage: receiveMessageUpdates, + subscribe, + })); + + ContentfulLivePreview.init(); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('init', () => { + describe('should bind the message listeners', () => { + it('provide the data to FieldTagging and LiveUpdates', () => { + const data = { from: 'live-preview', value: 'any' }; + window.dispatchEvent(new MessageEvent('message', { data })); + + expect(receiveMessageTagging).toHaveBeenCalledTimes(1); + expect(receiveMessageTagging).toHaveBeenCalledWith(data); + expect(receiveMessageUpdates).toHaveBeenCalledTimes(1); + expect(receiveMessageUpdates).toHaveBeenCalledWith(data); + }); + + it('doenst call the FieldTagging and LiveUpdates for invalid events', () => { + // Not from live-preview + window.dispatchEvent( + new MessageEvent('message', { data: { from: 'anywhere', value: 'any' } }) + ); + + // Invalid data + window.dispatchEvent(new MessageEvent('message', { data: 'just a string' })); + window.dispatchEvent(new MessageEvent('message', { data: null })); + + expect(receiveMessageTagging).not.toHaveBeenCalled(); + expect(receiveMessageUpdates).not.toHaveBeenCalled(); + }); + }); + }); + + describe('subscribe', () => { + it('should subscribe to changes from ContentfulLiveUpdates', () => { + const callback = vi.fn(); + const data = { entity: {} }; + ContentfulLivePreview.subscribe(data, 'en-US', callback); + + // Check that the ContentfulLiveUpdates.subscribe was called correctly + expect(subscribe).toHaveBeenCalledOnce(); + expect(subscribe).toHaveBeenCalledWith(data, 'en-US', callback); + + // Updates from the subscribe fn will trigger the callback + subscribe.mock.lastCall?.at(-1)({ entity: { title: 'Hello' } }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith({ entity: { title: 'Hello' } }); + }); + }); + + describe('getProps', () => { + it('returns the expected props with a given entryId, fieldId and locale', () => { + const entryId = 'test-entry-id'; + const fieldId = 'test-field-id'; + const locale = 'test-locale'; + + const result = ContentfulLivePreview.getProps({ + entryId, + fieldId, + locale, + }); + + expect(result).toStrictEqual({ + [TagAttributes.FIELD_ID]: fieldId, + [TagAttributes.ENTRY_ID]: entryId, + [TagAttributes.LOCALE]: locale, + }); + }); + }); +}); diff --git a/src/tests/init.spec.ts b/src/tests/init.spec.ts index 2e4fe32d..e9538879 100644 --- a/src/tests/init.spec.ts +++ b/src/tests/init.spec.ts @@ -1,12 +1,12 @@ // @vitest-environment jsdom -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeAll } from 'vitest'; import { ContentfulLivePreview } from '../index'; -import FieldTagging from '../field-tagging'; +import { ContentfulFieldTagging } from '../field-tagging'; describe('init', () => { it('returns a Promise that resolves to a LivePreview instance when running in a browser environment', async () => { const livePreviewInstance = await ContentfulLivePreview.init(); - expect(livePreviewInstance).toBeInstanceOf(FieldTagging); + expect(livePreviewInstance).toBeInstanceOf(ContentfulFieldTagging); }); it('returns undefined when not running in a browser environment', () => { diff --git a/src/tests/live-updates.spec.ts b/src/tests/live-updates.spec.ts new file mode 100644 index 00000000..f98b56eb --- /dev/null +++ b/src/tests/live-updates.spec.ts @@ -0,0 +1,43 @@ +import { describe, it, vi, expect } from 'vitest'; +import { ContentfulLiveUpdates } from '../live-updates'; + +describe('ContentfulLiveUpdates', () => { + it('should listen to changes and calls the subscribed handlers', () => { + const liveUpdates = new ContentfulLiveUpdates(); + const data = { sys: { id: '1' }, title: 'Data 1' }; + const cb = vi.fn(); + liveUpdates.subscribe(data, 'en-US', cb); + + liveUpdates.receiveMessage({ entity: { sys: { id: '1' }, title: 'Data 2' } }); + + expect(cb).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledWith(data); + }); + + it('no longer receives updates after unsubcribing', () => { + const liveUpdates = new ContentfulLiveUpdates(); + const data = { sys: { id: '1' }, title: 'Data 1' }; + const cb = vi.fn(); + const unsubscribe = liveUpdates.subscribe(data, 'en-US', cb); + + liveUpdates.receiveMessage({ entity: { sys: { id: '1' }, title: 'Data 2' } }); + + expect(cb).toHaveBeenCalledTimes(1); + + unsubscribe(); + liveUpdates.receiveMessage({ entity: { sys: { id: '1' }, title: 'Data 3' } }); + + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('ignores invalid messages', () => { + const liveUpdates = new ContentfulLiveUpdates(); + const data = { sys: { id: '1' }, title: 'Data 1' }; + const cb = vi.fn(); + liveUpdates.subscribe(data, 'en-US', cb); + + liveUpdates.receiveMessage({ isInspectorActive: false }); + + expect(cb).not.toHaveBeenCalled(); + }); +}); diff --git a/src/tests/react.spec.ts b/src/tests/react.spec.ts new file mode 100644 index 00000000..19214fa0 --- /dev/null +++ b/src/tests/react.spec.ts @@ -0,0 +1,72 @@ +// @vitest-environment jsdom +import { describe, it, vi, afterEach, expect, beforeEach } from 'vitest'; +import { act, renderHook } from '@testing-library/react'; + +import { useContentfulLiveUpdates } from '../react'; +import { ContentfulLivePreview } from '..'; + +describe('useContentfulLiveUpdates', () => { + const unsubscribe = vi.fn(); + const subscribe = vi.spyOn(ContentfulLivePreview, 'subscribe'); + + const locale = 'en-US'; + const initialData = { sys: { id: '1' }, title: 'Hello' }; + const updatedData = { sys: { id: '1' }, title: 'Hello World' }; + + beforeEach(() => { + subscribe.mockReturnValue(unsubscribe); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return the original data', () => { + const { result } = renderHook((data) => useContentfulLiveUpdates(data, locale), { + initialProps: initialData, + }); + + expect(result.current).toEqual(initialData); + }); + + it('should bind the subscibe fn', () => { + const { unmount } = renderHook((data) => useContentfulLiveUpdates(data, locale), { + initialProps: initialData, + }); + + expect(subscribe).toHaveBeenCalledTimes(1); + expect(subscribe).toHaveBeenCalledWith(initialData, locale, expect.any(Function)); + + unmount(); + + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + + it('should react to updates on the original data', () => { + const { result, rerender } = renderHook((data) => useContentfulLiveUpdates(data, locale), { + initialProps: initialData, + }); + + expect(result.current).toEqual(initialData); + + act(() => { + rerender(updatedData); + }); + + expect(result.current).toEqual(updatedData); + }); + + it('should listen to live updates and returns them instead', () => { + const { result } = renderHook((data) => useContentfulLiveUpdates(data, locale), { + initialProps: initialData, + }); + + expect(result.current).toEqual(initialData); + + act(() => { + subscribe.mock.calls[0][2](updatedData); + }); + + expect(result.current).toEqual(updatedData); + }); +}); diff --git a/src/tests/resolveIncomingMessage.spec.ts b/src/tests/resolveIncomingMessage.spec.ts deleted file mode 100644 index ff94dae3..00000000 --- a/src/tests/resolveIncomingMessage.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @vitest-environment jsdom -import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; -import FieldTagging from '../field-tagging'; - -describe('resolveIncomingMessage', () => { - let fieldTagging: FieldTagging; - - beforeEach(() => { - fieldTagging = new FieldTagging(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - test('should return if incoming data is not an object', () => { - const spy = vi.spyOn(document.body.classList, 'toggle'); - fieldTagging['resolveIncomingMessage']('not an object' as unknown as MessageEvent); - expect(spy).not.toHaveBeenCalled(); - }); - - test('should return if incoming message is not from live preview', () => { - const spy = vi.spyOn(document.body.classList, 'toggle'); - fieldTagging['resolveIncomingMessage']({ - data: { from: 'not-live-preview' }, - } as unknown as MessageEvent); - expect(spy).not.toHaveBeenCalled(); - }); - - test('should toggle "contentful-inspector--active" class on document.body based on value of isInspectorActive', () => { - const spy = vi.spyOn(document.body.classList, 'toggle'); - fieldTagging['resolveIncomingMessage']({ - data: { from: 'live-preview', isInspectorActive: true }, - } as unknown as MessageEvent); - expect(spy).toHaveBeenCalledWith('contentful-inspector--active', true); - }); -}); diff --git a/vite.config.ts b/vite.config.ts index a36badf8..f39cbc92 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,9 +6,10 @@ import dts from 'vite-plugin-dts'; export default defineConfig({ build: { lib: { - entry: resolve(__dirname, 'src/index.ts'), - name: 'LivePreview', - fileName: 'live-preview', + entry: [ + resolve(__dirname, 'src/index.ts'), + resolve(__dirname, 'src/react.ts') + ], formats: ['cjs', 'es'], }, rollupOptions: { diff --git a/yarn.lock b/yarn.lock index 85e6f76e..971a741d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== @@ -77,7 +77,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== -"@babel/runtime@^7.20.7": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.20.7": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -1043,6 +1043,29 @@ "@swc/core-win32-ia32-msvc" "1.3.35" "@swc/core-win32-x64-msvc" "1.3.35" +"@testing-library/dom@^9.0.0": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.0.1.tgz#fb9e3837fe2a662965df1536988f0863f01dbf51" + integrity sha512-fTOVsMY9QLFCCXRHG3Ese6cMH5qIWwSbgxZsgeF5TNsy81HKaZ4kgehnSF8FsR3OF+numlIV2YcU79MzbnhSig== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/react@14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" + integrity sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^9.0.0" + "@types/react-dom" "^18.0.0" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -1083,6 +1106,11 @@ resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9" integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA== +"@types/aria-query@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" + integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== + "@types/chai-subset@^1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" @@ -1135,9 +1163,9 @@ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== -"@types/react-dom@^18.0.10": +"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.10": version "18.0.11" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== dependencies: "@types/react" "*" @@ -1462,7 +1490,7 @@ argv-formatter@~1.0.0: resolved "https://registry.yarnpkg.com/argv-formatter/-/argv-formatter-1.0.0.tgz#a0ca0cbc29a5b73e836eebe1cbf6c5e0e4eb82f9" integrity sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw== -aria-query@^5.1.3: +aria-query@^5.0.0, aria-query@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== @@ -2346,6 +2374,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -4381,6 +4414,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.16.1.tgz#7acea16fecd9ed11430e78443c2bb81a06d3dea9" integrity sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-string@^0.29.0: version "0.29.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.29.0.tgz#f034f79f8c43dba4ae1730ffb5e8c4e084b16cf3" @@ -5413,7 +5451,7 @@ prettier@^2.8.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== -pretty-format@^27.5.1: +pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== From 60e671f829a51f9907d588163db61e1b3d943115 Mon Sep 17 00:00:00 2001 From: Chris Helgert Date: Thu, 16 Mar 2023 13:19:38 +0100 Subject: [PATCH 2/6] chore(live-preview): connected event --- README.md | 2 +- package.json | 4 +++- src/field-tagging.ts | 18 +++++++----------- src/index.ts | 22 ++++++++++++++-------- src/live-updates.ts | 6 ++---- src/react.ts | 12 +++++++----- src/tests/field-tagging.spec.ts | 6 +++--- src/tests/index.spec.ts | 20 +++++++++++++------- src/tests/init.spec.ts | 4 ++-- src/tests/live-updates.spec.ts | 10 +++++----- src/types.ts | 4 ++++ src/utils.ts | 14 ++++++++++++++ yarn.lock | 13 +++++++++++++ 13 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 src/utils.ts diff --git a/README.md b/README.md index 7b6081b6..4b7b938c 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ import '@contentful/live-preview/dist/style.css'; Live Updates from the editor to your applications are currently only supported for [React.js](https://reactjs.org/) ```tsx -import { useContentfulLiveUpdates } from "@contentful/live-previews/dist/react" +import { useContentfulLiveUpdates } from "@contentful/live-preview/dist/react" // ... const updated = useContentfulLiveUpdates(originalData, locale) diff --git a/package.json b/package.json index 8740c1bc..888ae749 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "cm": "git-cz" }, "homepage": "https://github.com/contentful/live-preview#readme", - "dependencies": {}, + "dependencies": { + "use-deep-compare-effect": "1.8.1" + }, "devDependencies": { "@commitlint/cli": "^17.4.4", "@commitlint/config-conventional": "^17.4.4", diff --git a/src/field-tagging.ts b/src/field-tagging.ts index 6e123315..6decebae 100644 --- a/src/field-tagging.ts +++ b/src/field-tagging.ts @@ -7,8 +7,9 @@ import { TOOLTIP_PADDING_LEFT, } from './constants'; import { TagAttributes } from './types'; +import { sendMessageToEditor } from './utils'; -export class ContentfulFieldTagging { +export class FieldTagging { private tooltip: HTMLButtonElement | null = null; // this tooltip scrolls to the correct field in the entry editor private currentElementBesideTooltip: HTMLElement | null = null; // this element helps to position the tooltip @@ -108,15 +109,10 @@ export class ContentfulFieldTagging { const entryId = this.tooltip.getAttribute(DATA_CURR_ENTRY_ID); const locale = this.tooltip.getAttribute(DATA_CURR_LOCALE); - window.top?.postMessage( - { - from: 'live-preview', - fieldId, - entryId, - locale, - }, - //todo: check if there is any security risk with this - '*' - ); + sendMessageToEditor({ + fieldId, + entryId, + locale, + }); } } diff --git a/src/index.ts b/src/index.ts index 46f23cd0..2d3f35e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,23 @@ import './styles.css'; -import { ContentfulFieldTagging } from './field-tagging'; -import { ContentfulLiveUpdates, type Entity, type SubscribeCallback } from './live-updates'; -import { LivePreviewProps, TagAttributes } from './types'; +import { FieldTagging } from './field-tagging'; +import { LiveUpdates } from './live-updates'; +import { Entity, LivePreviewProps, SubscribeCallback, TagAttributes } from './types'; +import { sendMessageToEditor } from './utils'; export class ContentfulLivePreview { - static fieldTagging: ContentfulFieldTagging | null = null; - static liveUpdates: ContentfulLiveUpdates | null = null; + static fieldTagging: FieldTagging | null = null; + static liveUpdates: LiveUpdates | null = null; // Static method to initialize the LivePreview SDK - static init(): Promise | undefined { + static init(): Promise | undefined { // Check if running in a browser environment if (typeof window !== 'undefined') { if (ContentfulLivePreview.fieldTagging) { console.log('You have already initialized the Live Preview SDK.'); return Promise.resolve(ContentfulLivePreview.fieldTagging); } else { - ContentfulLivePreview.fieldTagging = new ContentfulFieldTagging(); - ContentfulLivePreview.liveUpdates = new ContentfulLiveUpdates(); + ContentfulLivePreview.fieldTagging = new FieldTagging(); + ContentfulLivePreview.liveUpdates = new LiveUpdates(); window.addEventListener('message', (event) => { if (typeof event.data !== 'object' || !event.data) return; @@ -26,6 +27,11 @@ export class ContentfulLivePreview { ContentfulLivePreview.liveUpdates?.receiveMessage(event.data); }); + sendMessageToEditor({ + connected: true, + tags: document.querySelectorAll(`[${TagAttributes.ENTRY_ID}]`).length, + }); + return Promise.resolve(ContentfulLivePreview.fieldTagging); } } diff --git a/src/live-updates.ts b/src/live-updates.ts index c2720913..13302859 100644 --- a/src/live-updates.ts +++ b/src/live-updates.ts @@ -1,12 +1,10 @@ -export type Entity = Record; -export type Argument = Entity | Entity[]; -export type SubscribeCallback = (data: Argument) => void; +import { Argument, Entity, SubscribeCallback } from './types'; /** * LiveUpdates for the Contentful Live Preview mode * receives the updated Entity from the Editor and merges them together with the provided data */ -export class ContentfulLiveUpdates { +export class LiveUpdates { private subscriptions: { id: number; data: Argument; locale: string; cb: SubscribeCallback }[] = []; diff --git a/src/react.ts b/src/react.ts index 9891c22f..a5d5a95b 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,7 +1,9 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; + +import useDeepCompareEffect from 'use-deep-compare-effect'; import { ContentfulLivePreview } from '.'; -import { Entity } from './live-updates'; +import { Entity } from './types'; export function useContentfulLiveUpdates( data: T, @@ -9,18 +11,18 @@ export function useContentfulLiveUpdates( ): T { const [state, setState] = useState(data); - useEffect(() => { + useDeepCompareEffect(() => { // update content from external setState(data); // nothing to merge if there are no data if (!data) { return; } - // or update content through live udates + // or update content through live updates return ContentfulLivePreview.subscribe(data, locale, (data) => { setState(data as T); }); - }, [JSON.stringify(data)]); // eslint-disable-line react-hooks/exhaustive-deps + }, [data]); return state; } diff --git a/src/tests/field-tagging.spec.ts b/src/tests/field-tagging.spec.ts index 09c2f211..0ef17fc2 100644 --- a/src/tests/field-tagging.spec.ts +++ b/src/tests/field-tagging.spec.ts @@ -1,12 +1,12 @@ // @vitest-environment jsdom import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; -import { ContentfulFieldTagging } from '../field-tagging'; +import { FieldTagging } from '../field-tagging'; describe('FieldTagging', () => { - let fieldTagging: ContentfulFieldTagging; + let fieldTagging: FieldTagging; beforeEach(() => { - fieldTagging = new ContentfulFieldTagging(); + fieldTagging = new FieldTagging(); }); afterEach(() => { diff --git a/src/tests/index.spec.ts b/src/tests/index.spec.ts index ac6ce572..36828629 100644 --- a/src/tests/index.spec.ts +++ b/src/tests/index.spec.ts @@ -3,26 +3,32 @@ import { describe, it, expect, vi, Mock, afterEach, beforeEach, beforeAll } from import { ContentfulLivePreview } from '../index'; import { TagAttributes } from '../types'; -import { ContentfulFieldTagging } from '../field-tagging'; -import { ContentfulLiveUpdates } from '../live-updates'; +import { FieldTagging } from '../field-tagging'; +import { LiveUpdates } from '../live-updates'; +import { sendMessageToEditor } from '../utils'; vi.mock('../field-tagging'); vi.mock('../live-updates'); +vi.mock('../utils'); describe('ContentfulLivePreview', () => { const receiveMessageTagging = vi.fn(); const receiveMessageUpdates = vi.fn(); const subscribe = vi.fn(); - (ContentfulFieldTagging as Mock).mockImplementation(() => ({ + (FieldTagging as Mock).mockImplementation(() => ({ receiveMessage: receiveMessageTagging, })); - (ContentfulLiveUpdates as Mock).mockImplementation(() => ({ + (LiveUpdates as Mock).mockImplementation(() => ({ receiveMessage: receiveMessageUpdates, subscribe, })); - ContentfulLivePreview.init(); + beforeAll(() => { + ContentfulLivePreview.init(); + // establish the connection, needs to tested here, as we can only init the ContentfulLivePreview once + expect(sendMessageToEditor).toHaveBeenCalledTimes(1); + }); afterEach(() => { vi.clearAllMocks(); @@ -57,12 +63,12 @@ describe('ContentfulLivePreview', () => { }); describe('subscribe', () => { - it('should subscribe to changes from ContentfulLiveUpdates', () => { + it('should subscribe to changes from LiveUpdates', () => { const callback = vi.fn(); const data = { entity: {} }; ContentfulLivePreview.subscribe(data, 'en-US', callback); - // Check that the ContentfulLiveUpdates.subscribe was called correctly + // Check that the LiveUpdates.subscribe was called correctly expect(subscribe).toHaveBeenCalledOnce(); expect(subscribe).toHaveBeenCalledWith(data, 'en-US', callback); diff --git a/src/tests/init.spec.ts b/src/tests/init.spec.ts index e9538879..b828b9b9 100644 --- a/src/tests/init.spec.ts +++ b/src/tests/init.spec.ts @@ -1,12 +1,12 @@ // @vitest-environment jsdom import { describe, it, expect, beforeAll } from 'vitest'; import { ContentfulLivePreview } from '../index'; -import { ContentfulFieldTagging } from '../field-tagging'; +import { FieldTagging } from '../field-tagging'; describe('init', () => { it('returns a Promise that resolves to a LivePreview instance when running in a browser environment', async () => { const livePreviewInstance = await ContentfulLivePreview.init(); - expect(livePreviewInstance).toBeInstanceOf(ContentfulFieldTagging); + expect(livePreviewInstance).toBeInstanceOf(FieldTagging); }); it('returns undefined when not running in a browser environment', () => { diff --git a/src/tests/live-updates.spec.ts b/src/tests/live-updates.spec.ts index f98b56eb..693a37c0 100644 --- a/src/tests/live-updates.spec.ts +++ b/src/tests/live-updates.spec.ts @@ -1,9 +1,9 @@ import { describe, it, vi, expect } from 'vitest'; -import { ContentfulLiveUpdates } from '../live-updates'; +import { LiveUpdates } from '../live-updates'; -describe('ContentfulLiveUpdates', () => { +describe('LiveUpdates', () => { it('should listen to changes and calls the subscribed handlers', () => { - const liveUpdates = new ContentfulLiveUpdates(); + const liveUpdates = new LiveUpdates(); const data = { sys: { id: '1' }, title: 'Data 1' }; const cb = vi.fn(); liveUpdates.subscribe(data, 'en-US', cb); @@ -15,7 +15,7 @@ describe('ContentfulLiveUpdates', () => { }); it('no longer receives updates after unsubcribing', () => { - const liveUpdates = new ContentfulLiveUpdates(); + const liveUpdates = new LiveUpdates(); const data = { sys: { id: '1' }, title: 'Data 1' }; const cb = vi.fn(); const unsubscribe = liveUpdates.subscribe(data, 'en-US', cb); @@ -31,7 +31,7 @@ describe('ContentfulLiveUpdates', () => { }); it('ignores invalid messages', () => { - const liveUpdates = new ContentfulLiveUpdates(); + const liveUpdates = new LiveUpdates(); const data = { sys: { id: '1' }, title: 'Data 1' }; const cb = vi.fn(); liveUpdates.subscribe(data, 'en-US', cb); diff --git a/src/types.ts b/src/types.ts index 32d0b49b..cf22046d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,3 +9,7 @@ export enum TagAttributes { ENTRY_ID = 'data-contentful-entry-id', LOCALE = 'data-contentful-locale', } + +export type Entity = Record; +export type Argument = Entity | Entity[]; +export type SubscribeCallback = (data: Argument) => void; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..f59fa9e8 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,14 @@ +/** + * Sends the given message to the editor + * enhances it with the information necessary to be accepted + */ +export function sendMessageToEditor(data: Record): void { + window.top?.postMessage( + { + from: 'live-preview', + ...data, + }, + // TODO: check if there is any security risk with this + '*' + ); +} diff --git a/yarn.lock b/yarn.lock index 971a741d..b9ea85d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2325,6 +2325,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +dequal@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -6657,6 +6662,14 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +use-deep-compare-effect@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/use-deep-compare-effect/-/use-deep-compare-effect-1.8.1.tgz#ef0ce3b3271edb801da1ec23bf0754ef4189d0c6" + integrity sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q== + dependencies: + "@babel/runtime" "^7.12.5" + dequal "^2.0.2" + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From cdf659657f9f6226b5b6dce6e896ec19778dfb9f Mon Sep 17 00:00:00 2001 From: "Ahmed T. Ali" <12673605+z0al@users.noreply.github.com> Date: Tue, 21 Mar 2023 10:40:49 +0100 Subject: [PATCH 3/6] chore: save changes between merging with main --- src/transform/__tests__/entries.test.ts | 94 ++++++ .../__tests__/fixtures/contentType.json | 289 ++++++++++++++++++ src/transform/__tests__/fixtures/entry.json | 195 ++++++++++++ src/transform/assets.ts | 53 ++++ src/transform/entries.ts | 62 ++++ 5 files changed, 693 insertions(+) create mode 100644 src/transform/__tests__/entries.test.ts create mode 100644 src/transform/__tests__/fixtures/contentType.json create mode 100644 src/transform/__tests__/fixtures/entry.json create mode 100644 src/transform/assets.ts create mode 100644 src/transform/entries.ts diff --git a/src/transform/__tests__/entries.test.ts b/src/transform/__tests__/entries.test.ts new file mode 100644 index 00000000..d61a6f6d --- /dev/null +++ b/src/transform/__tests__/entries.test.ts @@ -0,0 +1,94 @@ +import { EntryProps } from 'contentful-management/types'; +import { describe, it, expect, vi, afterEach } from 'vitest'; + +import { updateGQLEntry } from '../gql'; +import contentType from './fixtures/contentType.json'; +import entry from './fixtures/entry.json'; + +const EN = 'en-US'; +// const DE = 'de'; + +describe('Update GraphQL Entry', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + const updateFn = ({ + data, + update = entry as EntryProps, + locale = EN, + }: { + data: Record; + update?: EntryProps; + locale?: string; + }) => { + return updateGQLEntry(contentType, data, update, locale); + }; + + it('keeps __typename unchanged', () => { + const warn = vi.spyOn(console, 'warn'); + const data = { __typename: 'CT', shortText: 'text' }; + + const update = updateFn({ data }); + + expect(update).toEqual( + expect.objectContaining({ + __typename: 'CT', + }) + ); + expect(warn).not.toHaveBeenCalled(); + }); + + it('warns but keeps unknown fields', () => { + const data = { unknownField: 'text' }; + const warn = vi.spyOn(console, 'warn'); + + const update = updateFn({ data }); + + expect(update).toEqual(data); + expect(warn).toHaveBeenCalledWith(expect.stringMatching(/Unrecognized field 'unknownField'/)); + }); + + it('updates primitive fields', () => { + const data = { + shortText: 'oldValue', + shortTextList: ['oldValue'], + longText: 'oldValue', + boolean: false, + numberInteger: -1, + numberDecimal: -1.0, + dateTime: '1970-1-1T00:00+00:00', + location: { + lon: 0, + lat: 0, + }, + json: { + test: 'oldValue', + }, + }; + + expect(updateFn({ data })).toEqual({ + shortText: entry.fields.shortText[EN], + shortTextList: entry.fields.shortTextList[EN], + longText: entry.fields.longText[EN], + boolean: entry.fields.boolean[EN], + numberInteger: entry.fields.numberInteger[EN], + numberDecimal: entry.fields.numberDecimal[EN], + dateTime: entry.fields.dateTime[EN], + location: entry.fields.location[EN], + json: entry.fields.json[EN], + }); + }); + + it('falls back to null for empty fields', () => { + const data = { + shortText: 'oldValue', + }; + + const update = updateFn({ data, locale: 'n/a' }); + + expect(update).toEqual({ + shortText: null, + }); + }); +}); diff --git a/src/transform/__tests__/fixtures/contentType.json b/src/transform/__tests__/fixtures/contentType.json new file mode 100644 index 00000000..79b64dd4 --- /dev/null +++ b/src/transform/__tests__/fixtures/contentType.json @@ -0,0 +1,289 @@ +{ + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "80q6jb175og9" + } + }, + "id": "fieldTypes", + "type": "ContentType", + "createdAt": "2023-03-14T18:36:32.224Z", + "updatedAt": "2023-03-15T07:45:51.251Z", + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "publishedVersion": 7, + "publishedAt": "2023-03-15T07:45:51.251Z", + "firstPublishedAt": "2023-03-14T18:36:32.695Z", + "createdBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "4LSGWE90WTQF3IXCiTEP74" + } + }, + "updatedBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "4LSGWE90WTQF3IXCiTEP74" + } + }, + "publishedCounter": 4, + "version": 8, + "publishedBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "4LSGWE90WTQF3IXCiTEP74" + } + } + }, + "displayField": "vDOpnJnlacj5Vd6p", + "name": "FieldTypes", + "description": "", + "assembly": false, + "fields": [ + { + "id": "vDOpnJnlacj5Vd6p", + "apiName": "shortText", + "name": "Short Text", + "type": "Symbol", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "phV9Yneb9X0ZLny2", + "apiName": "shortTextList", + "name": "Short Text (List)", + "type": "Array", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false, + "items": { + "type": "Symbol", + "validations": [] + } + }, + { + "id": "j3HFWaem7j8YYOX1", + "apiName": "longText", + "name": "Long Text", + "type": "Text", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "vBGzBcEXwwxIE2hy", + "apiName": "numberInteger", + "name": "Number (Integer)", + "type": "Integer", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "K9qMpAog8ipH2j3h", + "apiName": "numberDecimal", + "name": "Number (Decimal)", + "type": "Integer", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "nbny24EE0Q2JYvAg", + "apiName": "dateTime", + "name": "Date & Time", + "type": "Date", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "JPjeGdLfiAAEXxAo", + "apiName": "location", + "name": "Location", + "type": "Location", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "HV7C9mvymQaHVGex", + "apiName": "json", + "name": "JSON", + "type": "Object", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "vY6Yi341M9Ve2jhv", + "apiName": "media", + "name": "Media", + "type": "Link", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false, + "linkType": "Asset" + }, + { + "id": "n5PCVXCdecdQijDe", + "apiName": "mediaMany", + "name": "Media (Many)", + "type": "Array", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false, + "items": { + "type": "Link", + "validations": [], + "linkType": "Asset" + } + }, + { + "id": "mwfJkx0Y8L1hNkA3", + "apiName": "boolean", + "name": "Boolean", + "type": "Boolean", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false + }, + { + "id": "Z9HGbIbdXDMzcCJz", + "apiName": "richText", + "name": "Rich Text", + "type": "RichText", + "localized": false, + "required": false, + "validations": [ + { + "enabledNodeTypes": [ + "heading-1", + "heading-2", + "heading-3", + "heading-4", + "heading-5", + "heading-6", + "ordered-list", + "unordered-list", + "hr", + "blockquote", + "embedded-entry-block", + "embedded-asset-block", + "table", + "hyperlink", + "entry-hyperlink", + "asset-hyperlink", + "embedded-entry-inline" + ] + }, + { + "enabledMarks": ["bold", "italic", "underline", "code", "superscript", "subscript"] + } + ], + "disabled": false, + "omitted": false + }, + { + "id": "cLWeBMwi5iaPXcI1", + "apiName": "reference", + "name": "Reference", + "type": "Link", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false, + "linkType": "Entry" + }, + { + "id": "Aoz98jYkj8lMnyFi", + "apiName": "referenceMany", + "name": "Reference (Many)", + "type": "Array", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false, + "items": { + "type": "Link", + "validations": [], + "linkType": "Entry" + } + }, + { + "id": "PIh43g0NQmzDYo1j", + "apiName": "crossSpaceReference", + "name": "Cross Space Reference", + "type": "ResourceLink", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false, + "allowedResources": [ + { + "type": "Contentful:Entry", + "source": "crn:contentful:::content:spaces/3yzzz65gh9tw", + "contentTypes": ["crossSpaceContentType"] + } + ] + }, + { + "id": "CCJZmPQ1oYq45nQi", + "apiName": "crossSpaceReferenceMany", + "name": "Cross Space Reference (Many)", + "type": "Array", + "localized": false, + "required": false, + "validations": [], + "disabled": false, + "omitted": false, + "items": { + "type": "ResourceLink", + "validations": [] + }, + "allowedResources": [ + { + "type": "Contentful:Entry", + "source": "crn:contentful:::content:spaces/3yzzz65gh9tw", + "contentTypes": ["crossSpaceContentType"] + } + ] + } + ] +} diff --git a/src/transform/__tests__/fixtures/entry.json b/src/transform/__tests__/fixtures/entry.json new file mode 100644 index 00000000..d371fbcf --- /dev/null +++ b/src/transform/__tests__/fixtures/entry.json @@ -0,0 +1,195 @@ +{ + "metadata": { + "tags": [ + { + "sys": { + "type": "Link", + "linkType": "Tag", + "id": "myTag" + } + } + ] + }, + "sys": { + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "80q6jb175og9" + } + }, + "id": "4g1Xg1YnUCD5GUgndA7NLD", + "type": "Entry", + "createdAt": "2023-03-14T18:36:40.364Z", + "updatedAt": "2023-03-15T09:20:31.607Z", + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "publishedVersion": 19, + "publishedAt": "2023-03-14T19:06:06.365Z", + "firstPublishedAt": "2023-03-14T18:46:51.055Z", + "createdBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "4LSGWE90WTQF3IXCiTEP74" + } + }, + "updatedBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "4LSGWE90WTQF3IXCiTEP74" + } + }, + "publishedCounter": 3, + "version": 25, + "publishedBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "4LSGWE90WTQF3IXCiTEP74" + } + }, + "automationTags": [], + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "fieldTypes" + } + } + }, + "fields": { + "shortText": { + "en-US": "Short Text" + }, + "shortTextList": { + "en-US": ["item 1", "item 2", "item 3"] + }, + "longText": { + "en-US": "Long Text" + }, + "numberInteger": { + "en-US": 1 + }, + "numberDecimal": { + "en-US": 1 + }, + "dateTime": { + "en-US": "2023-03-14T00:00+01:00" + }, + "location": { + "en-US": { + "lon": 13.383788200574154, + "lat": 52.53941258524417 + } + }, + "json": { + "en-US": { + "test": "This is JSON" + } + }, + "media": { + "en-US": { + "sys": { + "type": "Link", + "linkType": "Asset", + "id": "4YRUBbjYKnVAU17retktzF" + } + } + }, + "mediaMany": { + "en-US": [ + { + "sys": { + "type": "Link", + "linkType": "Asset", + "id": "4YRUBbjYKnVAU17retktzF" + } + }, + { + "sys": { + "type": "Link", + "linkType": "Asset", + "id": "2bGsPN0EuQMOCc6sx3adsn" + } + } + ] + }, + "boolean": { + "en-US": true + }, + "richText": { + "en-US": { + "nodeType": "document", + "data": {}, + "content": [ + { + "nodeType": "paragraph", + "data": {}, + "content": [ + { + "nodeType": "text", + "value": "Hello from Rich Text", + "marks": [], + "data": {} + } + ] + } + ] + } + }, + "reference": { + "en-US": { + "sys": { + "type": "Link", + "linkType": "Entry", + "id": "422a9DU6Yb01C8Hh9ISq5c" + } + } + }, + "referenceMany": { + "en-US": [ + { + "sys": { + "type": "Link", + "linkType": "Entry", + "id": "6oD6HTe8N77dQQhBqULQhD" + } + }, + { + "sys": { + "type": "Link", + "linkType": "Entry", + "id": "4BAzEzgu8AR1hVWsvBNlzE" + } + } + ] + }, + "crossSpaceReference": { + "en-US": { + "sys": { + "type": "ResourceLink", + "linkType": "Contentful:Entry", + "urn": "crn:contentful:::content:spaces/3yzzz65gh9tw/entries/1oU53EU4UJjLk73abzyZV3" + } + } + }, + "crossSpaceReferenceMany": { + "en-US": [ + { + "sys": { + "type": "ResourceLink", + "linkType": "Contentful:Entry", + "urn": "crn:contentful:::content:spaces/3yzzz65gh9tw/entries/1oU53EU4UJjLk73abzyZV3" + } + } + ] + } + } +} diff --git a/src/transform/assets.ts b/src/transform/assets.ts new file mode 100644 index 00000000..c83e6037 --- /dev/null +++ b/src/transform/assets.ts @@ -0,0 +1,53 @@ +import { + ContentTypeProps, + EntryProps, + AssetProps, + ContentFields, +} from 'contentful-management/types'; +import { updateGQLEntry } from './entries'; + +const field = (name: string, type = 'Symbol'): ContentFields => ({ + id: name, + name, + type: 'Symbol', + localized: true, + required: false, +}); + +/** + * Hand-coded Asset Content Type to have consistent handling for Entries + * and Assets. + */ +const AssetContentType = { + name: 'Asset', + fields: [ + field('title'), + field('description'), + + // File attributes + field('fileName'), + field('contentType'), + field('url'), + field('size', 'Integer'), + field('width', 'Integer'), + field('height', 'Integer'), + ], +} as ContentTypeProps; + +/** + * Updates GraphQL response data based on CMA Asset object + * + * + * @param data the GraphQL response to be updated + * @param asset CMA Asset object containing the update + * @param locale locale code + */ +export function updateGQLAsset( + data: Record, + update: AssetProps, + locale: string +): Record { + // FIXME: copy nested asset.fields.file values to root to match the + // Content Type definition for GraphQL + return updateGQLEntry(AssetContentType, data, update as unknown as EntryProps, locale); +} diff --git a/src/transform/entries.ts b/src/transform/entries.ts new file mode 100644 index 00000000..7ad05743 --- /dev/null +++ b/src/transform/entries.ts @@ -0,0 +1,62 @@ +import { ContentTypeProps, ContentFields, EntryProps } from 'contentful-management/types'; + +function logUnrecognizedFields(contentTypeFields: string[], data: Record) { + const recognized = new Set(['sys', '__typename', 'contentfulMetadata', ...contentTypeFields]); + + for (const field of Object.keys(data)) { + if (!recognized.has(field)) { + console.warn(`Unrecognized field '${field}'. Note that GraphQL aliases are not supported`); + } + } +} + +function isPrimitiveField(field: ContentFields) { + const types = new Set(['Symbol', 'Text', 'Integer', 'Boolean', 'Date', 'Location', 'Object']); + + if (types.has(field.type)) { + return true; + } + + // Array of Symbols + if (field.type === 'Array' && field.items?.type === 'Symbol') { + return true; + } + + return false; +} + +/** + * Updates GraphQL response data based on CMA entry object + * + * @param contentType entity + * @param data the GraphQL response to be updated + * @param update CMA entry object containing the update + * @param locale locale code + */ +export function updateGQLEntry( + contentType: ContentTypeProps, + data: Record, + update: EntryProps, + locale: string +): Record { + const modified = { ...data }; + const { fields } = contentType; + + // Warn about unrecognized fields + logUnrecognizedFields( + fields.map((f) => f.apiName ?? f.name), + data + ); + + for (const field of fields) { + const name = field.apiName ?? field.name; + + if (isPrimitiveField(field) && name in data) { + // Falling back to 'null' as it's what GraphQL users would expect + // FIXME: handle locale fallbacks + modified[name] = update.fields?.[name]?.[locale] ?? null; + } + } + + return modified; +} From 23c9e0799f1e5c172e8b6e28ade3063e9bc9a2e4 Mon Sep 17 00:00:00 2001 From: "Ahmed T. Ali" <12673605+z0al@users.noreply.github.com> Date: Tue, 21 Mar 2023 10:58:21 +0100 Subject: [PATCH 4/6] chore: wip --- .../__tests__/entries.test.ts | 4 +-- .../__tests__/fixtures/contentType.json | 0 .../__tests__/fixtures/entry.json | 0 src/{transform => graphql}/assets.ts | 9 +++--- src/{transform => graphql}/entries.ts | 29 ++----------------- src/graphql/index.ts | 2 ++ src/graphql/utils.ts | 29 +++++++++++++++++++ src/live-updates.ts | 9 ++++-- 8 files changed, 47 insertions(+), 35 deletions(-) rename src/{transform => graphql}/__tests__/entries.test.ts (95%) rename src/{transform => graphql}/__tests__/fixtures/contentType.json (100%) rename src/{transform => graphql}/__tests__/fixtures/entry.json (100%) rename src/{transform => graphql}/assets.ts (85%) rename src/{transform => graphql}/entries.ts (51%) create mode 100644 src/graphql/index.ts create mode 100644 src/graphql/utils.ts diff --git a/src/transform/__tests__/entries.test.ts b/src/graphql/__tests__/entries.test.ts similarity index 95% rename from src/transform/__tests__/entries.test.ts rename to src/graphql/__tests__/entries.test.ts index d61a6f6d..4c07ff5f 100644 --- a/src/transform/__tests__/entries.test.ts +++ b/src/graphql/__tests__/entries.test.ts @@ -1,7 +1,7 @@ import { EntryProps } from 'contentful-management/types'; import { describe, it, expect, vi, afterEach } from 'vitest'; -import { updateGQLEntry } from '../gql'; +import { updateEntry } from '../entries'; import contentType from './fixtures/contentType.json'; import entry from './fixtures/entry.json'; @@ -22,7 +22,7 @@ describe('Update GraphQL Entry', () => { update?: EntryProps; locale?: string; }) => { - return updateGQLEntry(contentType, data, update, locale); + return updateEntry(contentType, data, update, locale); }; it('keeps __typename unchanged', () => { diff --git a/src/transform/__tests__/fixtures/contentType.json b/src/graphql/__tests__/fixtures/contentType.json similarity index 100% rename from src/transform/__tests__/fixtures/contentType.json rename to src/graphql/__tests__/fixtures/contentType.json diff --git a/src/transform/__tests__/fixtures/entry.json b/src/graphql/__tests__/fixtures/entry.json similarity index 100% rename from src/transform/__tests__/fixtures/entry.json rename to src/graphql/__tests__/fixtures/entry.json diff --git a/src/transform/assets.ts b/src/graphql/assets.ts similarity index 85% rename from src/transform/assets.ts rename to src/graphql/assets.ts index c83e6037..a14d2242 100644 --- a/src/transform/assets.ts +++ b/src/graphql/assets.ts @@ -4,12 +4,13 @@ import { AssetProps, ContentFields, } from 'contentful-management/types'; -import { updateGQLEntry } from './entries'; + +import { updateEntry } from './entries'; const field = (name: string, type = 'Symbol'): ContentFields => ({ id: name, name, - type: 'Symbol', + type, localized: true, required: false, }); @@ -42,12 +43,12 @@ const AssetContentType = { * @param asset CMA Asset object containing the update * @param locale locale code */ -export function updateGQLAsset( +export function updateAsset( data: Record, update: AssetProps, locale: string ): Record { // FIXME: copy nested asset.fields.file values to root to match the // Content Type definition for GraphQL - return updateGQLEntry(AssetContentType, data, update as unknown as EntryProps, locale); + return updateEntry(AssetContentType, data, update as unknown as EntryProps, locale); } diff --git a/src/transform/entries.ts b/src/graphql/entries.ts similarity index 51% rename from src/transform/entries.ts rename to src/graphql/entries.ts index 7ad05743..9ceccd4e 100644 --- a/src/transform/entries.ts +++ b/src/graphql/entries.ts @@ -1,29 +1,6 @@ -import { ContentTypeProps, ContentFields, EntryProps } from 'contentful-management/types'; +import { ContentTypeProps, EntryProps } from 'contentful-management/types'; -function logUnrecognizedFields(contentTypeFields: string[], data: Record) { - const recognized = new Set(['sys', '__typename', 'contentfulMetadata', ...contentTypeFields]); - - for (const field of Object.keys(data)) { - if (!recognized.has(field)) { - console.warn(`Unrecognized field '${field}'. Note that GraphQL aliases are not supported`); - } - } -} - -function isPrimitiveField(field: ContentFields) { - const types = new Set(['Symbol', 'Text', 'Integer', 'Boolean', 'Date', 'Location', 'Object']); - - if (types.has(field.type)) { - return true; - } - - // Array of Symbols - if (field.type === 'Array' && field.items?.type === 'Symbol') { - return true; - } - - return false; -} +import { isPrimitiveField, logUnrecognizedFields } from './utils'; /** * Updates GraphQL response data based on CMA entry object @@ -33,7 +10,7 @@ function isPrimitiveField(field: ContentFields) { * @param update CMA entry object containing the update * @param locale locale code */ -export function updateGQLEntry( +export function updateEntry( contentType: ContentTypeProps, data: Record, update: EntryProps, diff --git a/src/graphql/index.ts b/src/graphql/index.ts new file mode 100644 index 00000000..4a3eda32 --- /dev/null +++ b/src/graphql/index.ts @@ -0,0 +1,2 @@ +export { updateEntry } from './entries'; +export { updateAsset } from './assets'; diff --git a/src/graphql/utils.ts b/src/graphql/utils.ts new file mode 100644 index 00000000..4dd8667a --- /dev/null +++ b/src/graphql/utils.ts @@ -0,0 +1,29 @@ +import { ContentFields } from 'contentful-management/types'; + +export function logUnrecognizedFields( + contentTypeFields: string[], + data: Record +): void { + const recognized = new Set(['sys', '__typename', 'contentfulMetadata', ...contentTypeFields]); + + for (const field of Object.keys(data)) { + if (!recognized.has(field)) { + console.warn(`Unrecognized field '${field}'. Note that GraphQL aliases are not supported`); + } + } +} + +export function isPrimitiveField(field: ContentFields): boolean { + const types = new Set(['Symbol', 'Text', 'Integer', 'Boolean', 'Date', 'Location', 'Object']); + + if (types.has(field.type)) { + return true; + } + + // Array of Symbols + if (field.type === 'Array' && field.items?.type === 'Symbol') { + return true; + } + + return false; +} diff --git a/src/live-updates.ts b/src/live-updates.ts index 5fafda33..2a4240a7 100644 --- a/src/live-updates.ts +++ b/src/live-updates.ts @@ -1,3 +1,4 @@ +import * as gql from './graphql'; import { Argument, Entity, SubscribeCallback } from './types'; import { generateUID } from './utils'; @@ -16,9 +17,11 @@ export class LiveUpdates { // eslint-disable-next-line @typescript-eslint/no-unused-vars private mergeGraphQL(initial: Argument, locale: string, updated: Entity): Argument { - // TODO: https://contentful.atlassian.net/browse/TOL-1000 - // TODO: https://contentful.atlassian.net/browse/TOL-1022 - return initial; + if ((initial as any).__typename === 'Asset') { + return gql.updateAsset(initial as any, updated as any, locale); + } + + return gql.updateEntry(null, initial, updated, locale); } // eslint-disable-next-line @typescript-eslint/no-unused-vars From 26dbf490c039b2e7486257cffa1f171c31644a49 Mon Sep 17 00:00:00 2001 From: Yves Rijckaert Date: Wed, 22 Mar 2023 14:47:39 +0100 Subject: [PATCH 5/6] feat: use content types from user interface --- src/graphql/utils.ts | 18 ++++++++++++++++++ src/live-updates.ts | 20 ++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/graphql/utils.ts b/src/graphql/utils.ts index 4dd8667a..d18a55b5 100644 --- a/src/graphql/utils.ts +++ b/src/graphql/utils.ts @@ -27,3 +27,21 @@ export function isPrimitiveField(field: ContentFields): boolean { return false; } + +export function isComplexField(field: ContentFields): boolean { + const types = new Set(['Link', 'ResourceLink']); + + if (types.has(field.type)) { + return true; + } + + // Array of Links or ResourceLinks + if ( + (field.type === 'Array' && field.items?.type === 'Link') || + (field.type === 'Array' && field.items?.type === 'ResourceLink') + ) { + return true; + } + + return false; +} diff --git a/src/live-updates.ts b/src/live-updates.ts index 2a4240a7..c7537d12 100644 --- a/src/live-updates.ts +++ b/src/live-updates.ts @@ -16,12 +16,18 @@ export class LiveUpdates { private subscriptions = new Map(); // eslint-disable-next-line @typescript-eslint/no-unused-vars - private mergeGraphQL(initial: Argument, locale: string, updated: Entity): Argument { + private mergeGraphQL( + initial: Argument, + locale: string, + updated: Entity, + contentType: any + ): Argument { if ((initial as any).__typename === 'Asset') { return gql.updateAsset(initial as any, updated as any, locale); } - return gql.updateEntry(null, initial, updated, locale); + //@ts-expect-error -- .. + return gql.updateEntry(contentType, initial, updated, locale); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -31,17 +37,19 @@ export class LiveUpdates { return initial; } - private merge(initial: Argument, locale: string, updated: Entity): Argument { + private merge(initial: Argument, locale: string, updated: Entity, contentType: any): Argument { if ('__typename' in initial) { - return this.mergeGraphQL(initial, locale, updated); + return this.mergeGraphQL(initial, locale, updated, contentType); } return this.mergeRest(initial, locale, updated); } /** Receives the data from the message event handler and calls the subscriptions */ - public receiveMessage({ entity }: Record): void { + public receiveMessage({ entity, contentType }: Record): void { if (entity && typeof entity === 'object') { - this.subscriptions.forEach((s) => s.cb(this.merge(s.data, s.locale, entity as Entity))); + this.subscriptions.forEach((s) => + s.cb(this.merge(s.data, s.locale, entity as Entity, contentType)) + ); } } From 5d7669595ac9ca8ce6a953a605fd4309fd316849 Mon Sep 17 00:00:00 2001 From: Kudakwashe Mupeni Date: Wed, 22 Mar 2023 14:55:28 +0100 Subject: [PATCH 6/6] Update src/graphql/entries.ts Co-authored-by: Yves Rijckaert --- src/graphql/entries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphql/entries.ts b/src/graphql/entries.ts index 9ceccd4e..5eb15df4 100644 --- a/src/graphql/entries.ts +++ b/src/graphql/entries.ts @@ -8,7 +8,7 @@ import { isPrimitiveField, logUnrecognizedFields } from './utils'; * @param contentType entity * @param data the GraphQL response to be updated * @param update CMA entry object containing the update - * @param locale locale code + * @param locale code */ export function updateEntry( contentType: ContentTypeProps,