From 570d544eb59871f799111f7765c841f9646e06f5 Mon Sep 17 00:00:00 2001 From: joaquin Date: Fri, 12 Dec 2025 14:03:07 -0300 Subject: [PATCH] Rich Text Versioning optimizations --- .../src/components/HtmlDiffViewer.tsx | 30 +++-- .../src/components/createOptions.tsx | 10 +- .../src/locations/Dialog.tsx | 112 ++++++++++-------- .../src/locations/Field.tsx | 15 ++- apps/rich-text-versioning/src/utils.ts | 7 +- .../test/locations/Dialog.spec.tsx | 56 +++++---- 6 files changed, 138 insertions(+), 92 deletions(-) diff --git a/apps/rich-text-versioning/src/components/HtmlDiffViewer.tsx b/apps/rich-text-versioning/src/components/HtmlDiffViewer.tsx index 8035be5690..959d498ee4 100644 --- a/apps/rich-text-versioning/src/components/HtmlDiffViewer.tsx +++ b/apps/rich-text-versioning/src/components/HtmlDiffViewer.tsx @@ -13,20 +13,24 @@ interface HtmlDiffViewerProps { currentField: Document; publishedField: Document; onChangeCount: (count: number) => void; - entries: EntryProps[]; - entryContentTypes: Record; + entriesFromPublished: EntryProps[]; + entriesFromCurrent: EntryProps[]; + entryContentTypes: ContentTypeProps[]; locale: string; - assets: AssetProps[]; + assetsFromPublished: AssetProps[]; + assetsFromCurrent: AssetProps[]; } const HtmlDiffViewer = ({ currentField, publishedField, onChangeCount, - entries, + entriesFromPublished, + entriesFromCurrent, entryContentTypes, locale, - assets, + assetsFromPublished, + assetsFromCurrent, }: HtmlDiffViewerProps) => { const [diffHtml, setDiffHtml] = useState(''); @@ -35,11 +39,11 @@ const HtmlDiffViewer = ({ // Convert current field to React components with embedded entry renderers const currentComponents = documentToReactComponents( currentField, - createOptions(entries, entryContentTypes, assets, locale) + createOptions(entriesFromCurrent, entryContentTypes, assetsFromCurrent, locale) ); const publishedComponents = documentToReactComponents( publishedField, - createOptions(entries, entryContentTypes, assets, locale) + createOptions(entriesFromPublished, entryContentTypes, assetsFromPublished, locale) ); // Convert React components to HTML strings @@ -66,7 +70,17 @@ const HtmlDiffViewer = ({ }; processDiff(); - }, [currentField, publishedField, onChangeCount, entries, entryContentTypes, locale, assets]); + }, [ + currentField, + publishedField, + onChangeCount, + entriesFromCurrent, + entriesFromPublished, + entryContentTypes, + locale, + assetsFromCurrent, + assetsFromPublished, + ]); if (!diffHtml) { return ( diff --git a/apps/rich-text-versioning/src/components/createOptions.tsx b/apps/rich-text-versioning/src/components/createOptions.tsx index f69285dce7..4d0f638293 100644 --- a/apps/rich-text-versioning/src/components/createOptions.tsx +++ b/apps/rich-text-versioning/src/components/createOptions.tsx @@ -10,7 +10,7 @@ const ASSET_NOT_FOUND = 'Asset missing or inaccessible'; export const createOptions = ( entries: EntryProps[], - entryContentTypes: Record, + entryContentTypes: ContentTypeProps[], assets: AssetProps[], locale: string ): Options => ({ @@ -24,7 +24,9 @@ export const createOptions = ( ); } - const contentType = entryContentTypes[entry.sys.id]; + const contentType = entryContentTypes.find( + (ct) => ct.sys.id === entry.sys.contentType.sys.id + ); const contentTypeName = contentType?.name || UNKNOWN; const title = getEntryTitle(entry, contentType, locale); @@ -41,7 +43,9 @@ export const createOptions = ( return {ENTRY_NOT_FOUND}; } - const contentType = entryContentTypes[entry.sys.id]; + const contentType = entryContentTypes.find( + (ct) => ct.sys.id === entry.sys.contentType.sys.id + ); const contentTypeName = contentType?.name || UNKNOWN; const title = getEntryTitle(entry, contentType, locale); diff --git a/apps/rich-text-versioning/src/locations/Dialog.tsx b/apps/rich-text-versioning/src/locations/Dialog.tsx index 7e9a5853e1..5ce19de490 100644 --- a/apps/rich-text-versioning/src/locations/Dialog.tsx +++ b/apps/rich-text-versioning/src/locations/Dialog.tsx @@ -32,9 +32,11 @@ const Dialog = () => { const sdk = useSDK(); const invocationParams = sdk.parameters.invocation as unknown as InvocationParameters; const [changeCount, setChangeCount] = useState(0); - const [entries, setEntries] = useState([]); - const [entryContentTypes, setEntryContentTypes] = useState>({}); - const [assets, setAssets] = useState([]); + const [entriesFromPublished, setEntriesFromPublished] = useState([]); + const [entriesFromCurrent, setEntriesFromCurrent] = useState([]); + const [entryContentTypes, setEntryContentTypes] = useState([]); + const [assetsFromPublished, setAssetsFromPublished] = useState([]); + const [assetsFromCurrent, setAssetsFromCurrent] = useState([]); const [loading, setLoading] = useState(true); useAutoResizer(); @@ -43,11 +45,11 @@ const Dialog = () => { const publishedField = invocationParams?.publishedField || { content: [] }; const locale = invocationParams?.locale; - const getReferenceIdsFromDocument = (doc: Document, types: string[]): string[] => { - const ids: string[] = []; + const getReferenceIdsFromDocument = (doc: Document, types: string[]): Set => { + const ids: Set = new Set(); const getEntryIdsFromNode = (node: any) => { if (types.includes(node.nodeType)) { - ids.push(node.data.target.sys.id); + ids.add(node.data.target.sys.id); } if ('content' in node) { node.content.forEach(getEntryIdsFromNode); @@ -57,10 +59,42 @@ const Dialog = () => { return ids; }; + const getAssets = async (assetIds: string[]): Promise => { + if (assetIds.length === 0) return []; + + try { + const fetchedAssets = await sdk.cma.asset.getMany({ + query: { + select: 'sys.id,fields.title', + 'sys.id[in]': assetIds.join(','), + }, + }); + return fetchedAssets.items; + } catch (error) { + console.error('Error fetching assets:', error); + return []; + } + }; + + const getEntries = async (entryIds: string[]): Promise => { + if (entryIds.length === 0) return []; + + try { + const fetchedEntries = await sdk.cma.entry.getMany({ + query: { + 'sys.id[in]': entryIds.join(','), + }, + }); + return fetchedEntries.items; + } catch (error) { + console.error('Error fetching entries:', error); + return []; + } + }; + useEffect(() => { const fetchReferences = async () => { setLoading(true); - const contentTypes: Record = {}; const currentEntryIds = getReferenceIdsFromDocument(currentField, [ BLOCKS.EMBEDDED_ENTRY, @@ -74,53 +108,31 @@ const Dialog = () => { const publishedAssetIds = getReferenceIdsFromDocument(publishedField, [ BLOCKS.EMBEDDED_ASSET, ]); - const allEntryIds = [...new Set([...currentEntryIds, ...publishedEntryIds])]; - const allAssetIds = [...new Set([...currentAssetIds, ...publishedAssetIds])]; - if (allEntryIds.length > 0) { - let entries: EntryProps[] = []; + if (currentEntryIds.size > 0 || publishedEntryIds.size > 0) { + const fetchedEntriesFromCurrent = await getEntries(Array.from(currentEntryIds)); + const fetchedEntriesFromPublished = await getEntries(Array.from(publishedEntryIds)); + setEntriesFromCurrent(fetchedEntriesFromCurrent); + setEntriesFromPublished(fetchedEntriesFromPublished); + try { - const fetchedEntries = await sdk.cma.entry.getMany({ + const allEntries = [...entriesFromCurrent, ...entriesFromPublished]; + const fetchedContentTypes = await sdk.cma.contentType.getMany({ query: { - 'sys.id[in]': allEntryIds.join(','), + 'sys.id[in]': allEntries.map((entry) => entry.sys.contentType.sys.id), }, }); - entries = fetchedEntries.items; - setEntries(entries); + setEntryContentTypes(fetchedContentTypes.items); } catch (error) { - entries = []; - console.error('Error fetching entries:', error); + console.error('Error fetching content types:', error); } - if (entries.length > 0) { - await Promise.all( - entries.map(async (entry) => { - const entryId = entry.sys.id; - try { - const contentType = await sdk.cma.contentType.get({ - contentTypeId: entry.sys.contentType.sys.id, - }); - - contentTypes[entryId] = contentType; - } catch (error) { - console.error(`Error fetching content type for entry ${entryId}:`, error); - contentTypes[entryId] = { name: 'Reference is missing' } as ContentTypeProps; - } - }) - ); - } - setEntryContentTypes(contentTypes); } - if (allAssetIds.length > 0) { - try { - const fetchedAssets = await sdk.cma.asset.getMany({ - query: { - 'sys.id[in]': allAssetIds.join(','), - }, - }); - setAssets(fetchedAssets.items); - } catch (error) { - console.error('Error fetching assets:', error); - } + if (publishedAssetIds.size > 0 || currentAssetIds.size > 0) { + const fetchedAssetsFromPublished = await getAssets(Array.from(publishedAssetIds)); + const fetchedAssetsFromCurrent = await getAssets(Array.from(currentAssetIds)); + + setAssetsFromPublished(fetchedAssetsFromPublished); + setAssetsFromCurrent(fetchedAssetsFromCurrent); } setLoading(false); }; @@ -188,10 +200,12 @@ const Dialog = () => { currentField={currentField} publishedField={publishedField} onChangeCount={setChangeCount} - entries={entries} + entriesFromPublished={entriesFromPublished} + entriesFromCurrent={entriesFromCurrent} entryContentTypes={entryContentTypes} locale={sdk.locales.default} - assets={assets} + assetsFromPublished={assetsFromPublished} + assetsFromCurrent={assetsFromCurrent} /> )} @@ -200,7 +214,7 @@ const Dialog = () => { {documentToReactComponents( publishedField, - createOptions(entries, entryContentTypes, assets, locale) + createOptions(entriesFromPublished, entryContentTypes, assetsFromPublished, locale) )} diff --git a/apps/rich-text-versioning/src/locations/Field.tsx b/apps/rich-text-versioning/src/locations/Field.tsx index bebbf01c75..a6ef9ac571 100644 --- a/apps/rich-text-versioning/src/locations/Field.tsx +++ b/apps/rich-text-versioning/src/locations/Field.tsx @@ -58,15 +58,16 @@ const Field = () => { try { const publishedEntries = await sdk.cma.entry.getPublished({ - query: { 'sys.id': sdk.ids.entry }, + query: { + 'sys.id': sdk.ids.entry, + include: 0, + }, }); const publishedEntry = publishedEntries.items[0]; - if (publishedEntry?.fields?.[sdk.field.id]) { - const fieldData = publishedEntry.fields[sdk.field.id]; - publishedField = fieldData as Document; - } + publishedField = publishedEntry?.fields?.[sdk.field.id]?.[sdk.field.locale]; } catch (error) { + console.error('Error loading content:', error); currentErrorInfo = { hasError: true, errorCode: '500', @@ -81,9 +82,7 @@ const Field = () => { shouldCloseOnEscapePress: true, parameters: { currentField: convertToSerializableJson(value), - publishedField: publishedField - ? convertToSerializableJson(publishedField)[sdk.field.locale] - : undefined, + publishedField: publishedField ? convertToSerializableJson(publishedField) : undefined, errorInfo: convertToSerializableJson(currentErrorInfo), locale: sdk.field.locale, }, diff --git a/apps/rich-text-versioning/src/utils.ts b/apps/rich-text-versioning/src/utils.ts index 753ce11499..58a50836bc 100644 --- a/apps/rich-text-versioning/src/utils.ts +++ b/apps/rich-text-versioning/src/utils.ts @@ -1,6 +1,5 @@ import { AppState, ContentTypeField } from '@contentful/app-sdk'; -import { EntityStatus } from '@contentful/f36-components'; -import { ContentTypeProps, Entry, EntryProps } from 'contentful-management'; +import { ContentTypeProps, EntryProps } from 'contentful-management'; import { Document } from '@contentful/rich-text-types'; @@ -60,10 +59,10 @@ export const restoreSelectedFields = ( export const getEntryTitle = ( entry: EntryProps, - contentType: ContentTypeProps, + contentType: ContentTypeProps | undefined, locale: string ): string => { - let displayFieldId = contentType.displayField; + const displayFieldId = contentType?.displayField; if (!displayFieldId) return 'Untitled'; const value = entry.fields[displayFieldId]?.[locale]; diff --git a/apps/rich-text-versioning/test/locations/Dialog.spec.tsx b/apps/rich-text-versioning/test/locations/Dialog.spec.tsx index fe09c9ed44..a6780e0c2b 100644 --- a/apps/rich-text-versioning/test/locations/Dialog.spec.tsx +++ b/apps/rich-text-versioning/test/locations/Dialog.spec.tsx @@ -241,11 +241,15 @@ describe('Dialog component', () => { ], }); - mockSdk.cma.contentType.get = vi.fn().mockResolvedValue({ - displayField: 'title', - name: 'Fruits', - sys: { id: 'fruits' }, - fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + mockSdk.cma.contentType.getMany = vi.fn().mockResolvedValue({ + items: [ + { + displayField: 'title', + name: 'Fruits', + sys: { id: 'fruits' }, + fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + }, + ], }); render(); @@ -264,11 +268,15 @@ describe('Dialog component', () => { items: [], // No entries found }); - mockSdk.cma.contentType.get = vi.fn().mockResolvedValue({ - displayField: 'title', - name: 'Fruits', - sys: { id: 'fruits' }, - fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + mockSdk.cma.contentType.getMany = vi.fn().mockResolvedValue({ + items: [ + { + displayField: 'title', + name: 'Fruits', + sys: { id: 'fruits' }, + fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + }, + ], }); render(); @@ -344,11 +352,15 @@ describe('Dialog component', () => { ], }); - mockSdk.cma.contentType.get = vi.fn().mockResolvedValue({ - displayField: 'title', - name: 'Fruits', - sys: { id: 'fruits' }, - fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + mockSdk.cma.contentType.getMany = vi.fn().mockResolvedValue({ + items: [ + { + displayField: 'title', + name: 'Fruits', + sys: { id: 'fruits' }, + fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + }, + ], }); render(); @@ -366,11 +378,15 @@ describe('Dialog component', () => { items: [], // No entries found }); - mockSdk.cma.contentType.get = vi.fn().mockResolvedValue({ - displayField: 'title', - name: 'Fruits', - sys: { id: 'fruits' }, - fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + mockSdk.cma.contentType.getMany = vi.fn().mockResolvedValue({ + items: [ + { + displayField: 'title', + name: 'Fruits', + sys: { id: 'fruits' }, + fields: [{ id: 'title', name: 'Title', type: 'Symbol' }], + }, + ], }); render();