UNPKG

@kontent-ai/smart-link

Version:

Kontent.ai Smart Link SDK allowing to automatically inject [smart links](https://docs.kontent.ai/tutorials/develop-apps/build-strong-foundation/set-up-editing-from-preview#a-using-smart-links) to Kontent.ai according to manually specified [HTML data attri

250 lines 13.3 kB
import { ElementType } from '@kontent-ai/delivery-sdk'; import { applyOnOptionallyAsync, evaluateOptionallyAsync, createOptionallyAsync, mergeOptionalAsyncs, chainOptionallyAsync, } from './liveReload/optionallyAsync'; /** * Applies an update to a content item synchronously. * This function takes a content item and an update message, and returns a new content item with the updates applied. * The update is applied recursively to all linked items within the content item. */ export const applyUpdateOnItem = (item, update) => evaluateOptionallyAsync(applyUpdateOnItemOptionallyAsync(item, update), null); /** * Applies an update to a content item asynchronously and loads newly added linked items. * This function takes a content item, an update message, and a function to fetch linked items, * and returns a promise that resolves to a new content item with the updates applied. * The update is applied recursively to all linked items within the content item. */ export const applyUpdateOnItemAndLoadLinkedItems = (item, update, fetchItems) => Promise.resolve(evaluateOptionallyAsync(applyUpdateOnItemOptionallyAsync(item, update), fetchItems)); const applyUpdateOnItemOptionallyAsync = (item, update, updatedItem = null, processedItemsPath = []) => { const shouldApplyOnThisItem = item.system.codename === update.item.codename && item.system.language === update.variant.codename; const newUpdatedItem = !updatedItem && shouldApplyOnThisItem ? { ...item } : updatedItem; // We will mutate its elements to new values before returning. This is necesary to preserve cyclic dependencies between items without infinite recursion. const updatedElements = mergeOptionalAsyncs(Object.entries(item.elements).map(([elementCodename, element]) => { const matchingUpdate = update.elements.find((u) => u.element.codename === elementCodename); if (shouldApplyOnThisItem && matchingUpdate) { return applyOnOptionallyAsync(applyUpdateOnElement(element, matchingUpdate), (newElement) => [elementCodename, newElement]); } if (element.type === ElementType.ModularContent || element.type === ElementType.RichText) { const typedItemElement = element; return applyOnOptionallyAsync(mergeOptionalAsyncs(typedItemElement.linkedItems.map((i) => { if (updatedItem?.system.codename === i.system.codename) { // we closed the cycle and on the updated item and need to connect to the new item return createOptionallyAsync(() => updatedItem); } return closesCycleWithoutUpdate(processedItemsPath, i.system.codename, updatedItem?.system.codename ?? null) ? createOptionallyAsync(() => i) // we found a cycle that doesn't need any update so we just ignore it : applyUpdateOnItemOptionallyAsync(i, update, newUpdatedItem, [ ...processedItemsPath, i.system.codename, ]); })), (linkedItems) => { return linkedItems.some((newItem, index) => newItem !== typedItemElement.linkedItems[index]) ? [elementCodename, { ...typedItemElement, linkedItems }] : [elementCodename, typedItemElement]; }); } return createOptionallyAsync(() => [elementCodename, element]); })); return applyOnOptionallyAsync(updatedElements, (newElements) => { if (newUpdatedItem?.system.codename === item.system.codename) { newUpdatedItem.elements = Object.fromEntries(newElements); return newUpdatedItem; } return newElements.some(([codename, newEl]) => item.elements[codename] !== newEl) ? { ...item, elements: Object.fromEntries(newElements) } : item; }); }; const closesCycleWithoutUpdate = (path, nextItem, updatedItem) => { const cycleStartIndex = path.indexOf(nextItem); return cycleStartIndex !== -1 && (!updatedItem || cycleStartIndex > path.indexOf(updatedItem)); }; const applyUpdateOnElement = (element, update) => { switch (update.type) { case ElementType.Text: case ElementType.Number: case ElementType.UrlSlug: return createOptionallyAsync(() => applySimpleElement(element, update)); case ElementType.ModularContent: return applyLinkedItemsElement(element, update); case ElementType.RichText: return applyRichTextElement(element, update); case ElementType.MultipleChoice: return createOptionallyAsync(() => applyArrayElement(element, update, (o1, o2) => o1?.codename === o2?.codename)); case ElementType.DateTime: return createOptionallyAsync(() => applyDateTimeElement(element, update)); case ElementType.Asset: return createOptionallyAsync(() => applyArrayElement(element, update, (a1, a2) => a1?.url === a2?.url)); case ElementType.Taxonomy: return createOptionallyAsync(() => applyArrayElement(element, update, (t1, t2) => t1?.codename === t2?.codename)); case ElementType.Custom: return createOptionallyAsync(() => applyCustomElement(element, update)); default: return createOptionallyAsync(() => element); } }; const applyCustomElement = (element, update) => typeof element.value === 'string' && element.value !== update.data.value ? { ...element, value: update.data.value } : element; const applyDateTimeElement = (element, update) => element.value === update.data.value && element.displayTimeZone === update.data.displayTimeZone ? element : { ...element, value: update.data.value, displayTimeZone: update.data.displayTimeZone }; const applySimpleElement = (element, update) => (element.value === update.data.value ? element : { ...element, value: update.data.value }); const applyArrayElement = (element, update, areSame) => element.value.length === update.data.value.length && element.value.every((el, i) => areSame(el, update.data.value[i])) ? element : { ...element, value: update.data.value }; const applyLinkedItemsElement = (element, update) => { if (areLinkedItemsSame(element.value, update.data.value)) { return createOptionallyAsync(() => element); } return applyOnOptionallyAsync(updateLinkedItems(update.data.value, element.linkedItems), (linkedItems) => ({ ...element, value: update.data.value, linkedItems, })); }; const applyRichTextElement = (element, update) => { if (areRichTextElementsSame(element, update.data)) { return createOptionallyAsync(() => element); } const withItems = applyOnOptionallyAsync(updateLinkedItems(update.data.linkedItemCodenames, update.data.linkedItems .filter((i) => !element.linkedItems.find((u) => u.system.codename === i.system.codename)) .concat(element.linkedItems)), (linkedItems) => ({ ...element, value: update.data.value, linkedItemCodenames: update.data.linkedItemCodenames, links: update.data.links, images: update.data.images, linkedItems, })); return chainOptionallyAsync(withItems, (el) => applyOnOptionallyAsync(updateComponents(update.data.linkedItems, el.linkedItems), (linkedItems) => ({ ...el, linkedItems, }))); }; const areItemsSame = (item1, item2) => item1.system.codename === item2.system.codename && item1.system.language === item2.system.language && Object.entries(item1.elements).every(([codename, el1]) => areElementsSame(el1, item2.elements[codename])); const areElementsSame = (el1, el2) => { switch (el1.type) { case ElementType.Text: case ElementType.Number: case ElementType.UrlSlug: return el1.value === el2.value; case ElementType.MultipleChoice: { const typedElement1 = el1; const typedElement2 = el2; return (typedElement1.value.length === typedElement2.value.length && typedElement1.value.every((option, i) => option.codename === el2.value[i].codename)); } case ElementType.DateTime: { const typedElement1 = el1; const typedElement2 = el2; return (typedElement1.value === typedElement2.value && typedElement1.displayTimeZone === typedElement2.displayTimeZone); } case ElementType.RichText: { const typedElement1 = el1; const typedElement2 = el2; return areRichTextElementsSame(typedElement1, typedElement2); } case ElementType.Taxonomy: { const typedElement1 = el1; const typedElement2 = el2; return (typedElement1.value.length === typedElement2.value.length && typedElement1.value.every((term, i) => term.codename === typedElement2.value[i].codename)); } case ElementType.Asset: { const typedElement1 = el1; const typedElement2 = el2; return (typedElement1.value.length === typedElement2.value.length && typedElement1.value.every((asset, i) => asset.url === typedElement2.value[i].url)); } case ElementType.ModularContent: { const typedElement1 = el1; const typedElement2 = el2; return (typedElement1.value.length === typedElement2.value.length && typedElement1.value.every((item, i) => item === typedElement2.value[i])); } case ElementType.Custom: return el1.value === el2.value; default: throw new Error(); } }; const areRichTextElementsSame = (el1, el2) => el1.value === el2.value && el1.links.length === el2.links.length && el1.links.every((link, i) => link.codename === el2.links[i].codename) && el1.images.length === el2.images.length && el1.images.every((image, i) => image.url === el2.images[i].url) && el1.linkedItemCodenames.length === el2.linkedItemCodenames.length && el1.linkedItemCodenames.every((codename, i) => codename === el2.linkedItemCodenames[i]) && el1.linkedItems.length === el2.linkedItems.length && el1.linkedItems.every((item, i) => areItemsSame(item, el2.linkedItems[i])); const updateComponents = (newItems, oldItems) => mergeOptionalAsyncs(oldItems.map((item) => { const newItem = newItems.find((i) => i.system.codename === item.system.codename); return newItem ? applyUpdateOnItemOptionallyAsync(item, convertItemToUpdate(newItem)) : createOptionallyAsync(() => item); })); const updateLinkedItems = (newValue, loadedItems) => { const itemsByCodename = new Map(loadedItems.map((i) => [i.system.codename, i])); const newLinkedItems = newValue.map((codename) => itemsByCodename.get(codename) ?? codename); const itemsToFetch = newLinkedItems.filter(isString); return applyOnOptionallyAsync(createOptionallyAsync((fetchItems) => (fetchItems && itemsToFetch.length ? fetchItems(itemsToFetch) : [])), (fetchedItemsArray) => { const fetchedItems = new Map(fetchedItemsArray.map((i) => [i.system.codename, i])); return newLinkedItems .map((codename) => (isString(codename) ? (fetchedItems.get(codename) ?? null) : codename)) .filter(notNull); }); }; const areLinkedItemsSame = (items1, items2) => items1.length === items2.length && items1.every((codename, index) => codename === items2[index]); const notNull = (value) => value !== null; const isString = (value) => typeof value === 'string'; const convertItemToUpdate = (item) => ({ variant: { codename: item.system.language }, item: { codename: item.system.codename }, elements: Object.entries(item.elements).map(([elCodename, el]) => { switch (el.type) { case ElementType.Number: case ElementType.UrlSlug: case ElementType.MultipleChoice: case ElementType.Custom: case ElementType.Asset: case ElementType.Text: { return { element: { codename: elCodename }, type: el.type, data: el, }; } case ElementType.DateTime: { return { element: { codename: elCodename }, type: el.type, data: el, }; } case ElementType.RichText: { return { element: { codename: elCodename }, type: el.type, data: el, }; } case ElementType.Taxonomy: { return { element: { codename: elCodename }, type: el.type, data: el, }; } case ElementType.ModularContent: { return { element: { codename: elCodename }, type: el.type, data: el, }; } case ElementType.Unknown: throw new Error(`Cannot update element of type ${el.type}.`); } }), }); //# sourceMappingURL=liveReload.js.map