From 2a17d983dcd1360229073c7572513b6ccd2ebc99 Mon Sep 17 00:00:00 2001 From: Devin Abbott Date: Sun, 17 Dec 2023 12:49:51 -0800 Subject: [PATCH] Refactor applyDiff and add tests --- .../src/__tests__/editing.test.ts | 190 +++++++++++------- packages/noya-component/src/applyDiff.ts | 81 ++++++++ packages/noya-component/src/index.ts | 2 + .../src}/partitionDiff.tsx | 7 +- .../noya-component/src/resolvedHierarchy.ts | 33 ++- .../src/dseditor/DSComponentInspector.tsx | 78 ++----- 6 files changed, 253 insertions(+), 138 deletions(-) create mode 100644 packages/noya-component/src/applyDiff.ts rename packages/{site/src/dseditor/utils => noya-component/src}/partitionDiff.tsx (90%) diff --git a/packages/noya-component/src/__tests__/editing.test.ts b/packages/noya-component/src/__tests__/editing.test.ts index b82df7636..f4b8559ed 100644 --- a/packages/noya-component/src/__tests__/editing.test.ts +++ b/packages/noya-component/src/__tests__/editing.test.ts @@ -1,10 +1,14 @@ /* eslint-disable jest/no-commented-out-tests */ import { + ElementHierarchy, Model, + NoyaNode, NoyaPrimitiveElement, NoyaResolvedCompositeElement, NoyaResolvedPrimitiveElement, + ResolvedHierarchy, added, + applyDiff, createResolvedNode, removed, } from 'noya-component'; @@ -155,75 +159,6 @@ describe('diffing', () => { expect(resolvedChild.classNames).toEqual([]); }); - - // it('embeds class names in primitive', () => { - // const primitive = Model.primitiveElement({ - // componentID: PRIMITIVE_ID, - // classNames: ['bg-red-500'], - // }); - - // const updated = embedRootLevelDiff( - // primitive, - // Model.diff([ - // Model.diffItem({ - // path: [primitive.id], - // classNames: { - // add: ['bg-green-500'], - // }, - // }), - // ]), - // ) as NoyaPrimitiveElement; - - // expect(updated.classNames).toEqual(['bg-red-500', 'bg-green-500']); - // }); - - // it('embeds nested diff in composite element', () => { - // const element = Model.compositeElement({ - // componentID: wrapperComponent.componentID, - // diff: Model.diff([ - // Model.diffItem({ - // path: [wrapperComponent.rootElement.id, redComponent.rootElement.id], - // classNames: { - // add: ['bg-blue-500'], - // }, - // }), - // ]), - // }); - - // const updated = embedRootLevelDiff( - // element, - // Model.diff([ - // Model.diffItem({ - // path: [ - // element.id, - // wrapperComponent.rootElement.id, - // redComponent.rootElement.id, - // ], - // classNames: { - // add: ['bg-green-500'], - // }, - // }), - // ]), - // ) as NoyaCompositeElement; - - // expect(updated.diff?.items).toEqual([ - // { - // path: [wrapperComponent.rootElement.id, redComponent.rootElement.id], - // classNames: { - // add: ['bg-blue-500', 'bg-green-500'], - // }, - // }, - // ]); - - // // const updatedChild = updated.rootElement as NoyaResolvedPrimitiveElement; - - // // expect(updatedChild.classNames).toEqual([ - // // { value: 'bg-red-500' }, - // // { value: 'bg-blue-500', status: 'added' }, - // // { value: 'bg-green-500', status: 'added' }, - // // ]); - // }); - // it('embeds string value', () => { // const string = Model.string('hello'); @@ -448,3 +383,120 @@ describe('diffing', () => { // expect(found?.id).toEqual('d'); // }); }); + +describe('apply diff', () => { + const enforceSchema = (node: NoyaNode) => node; + + it('embeds class names in primitive', () => { + const primitive = Model.primitiveElement({ + componentID: PRIMITIVE_ID, + classNames: [Model.className('bg-red-500')], + }); + + const component = Model.component({ + componentID: 'a', + rootElement: primitive, + }); + + const components = { + [component.componentID]: component, + }; + + const diff = Model.diff([ + Model.diffItem({ + path: [primitive.id], + classNames: [added(Model.className('bg-green-500'), 1)], + }), + ]); + + const findComponent = (componentID: string) => components[componentID]; + + const updated = applyDiff({ + selection: { componentID: component.componentID, diff }, + component, + findComponent, + enforceSchema, + }); + + const element = ElementHierarchy.find( + updated.component.rootElement, + (node) => node.id === primitive.id, + ) as NoyaPrimitiveElement; + + expect(element.classNames.map((c) => c.value)).toEqual([ + 'bg-red-500', + 'bg-green-500', + ]); + }); + + it('embeds nested diff in composite element', () => { + const redComponent = Model.component({ + componentID: 'a', + rootElement: Model.primitiveElement({ + componentID: PRIMITIVE_ID, + classNames: [Model.className('bg-red-500')], + }), + }); + + const wrapperComponent = Model.component({ + componentID: 'b', + rootElement: Model.compositeElement({ + componentID: redComponent.componentID, + diff: Model.diff([ + Model.diffItem({ + path: [redComponent.rootElement.id], + classNames: [added(Model.className('bg-pink-500'), 1)], + }), + ]), + }), + }); + + const element = Model.compositeElement({ + componentID: wrapperComponent.componentID, + diff: Model.diff([ + Model.diffItem({ + path: [wrapperComponent.rootElement.id, redComponent.rootElement.id], + classNames: [added(Model.className('bg-blue-500'), 2)], + }), + ]), + }); + + const component = Model.component({ + componentID: 'c', + rootElement: element, + }); + + const components = { + [redComponent.componentID]: redComponent, + [wrapperComponent.componentID]: wrapperComponent, + [component.componentID]: component, + }; + + const findComponent = (componentID: string) => components[componentID]; + + const updated = applyDiff({ + selection: { componentID: component.componentID, diff: element.diff }, + component, + findComponent, + enforceSchema, + }); + + const resolved = createResolvedNode( + findComponent, + updated.component.rootElement, + ); + + const resolvedPrimitive = ResolvedHierarchy.find( + resolved, + (node) => + node.type === 'noyaPrimitiveElement' && + node.componentID === PRIMITIVE_ID, + ) as NoyaResolvedPrimitiveElement; + + expect(resolvedPrimitive.classNames.map((c) => c.value)).toEqual([ + 'bg-red-500', + 'bg-pink-500', + 'bg-blue-500', + ]); + }); +}); diff --git a/packages/noya-component/src/applyDiff.ts b/packages/noya-component/src/applyDiff.ts new file mode 100644 index 000000000..c158caa7c --- /dev/null +++ b/packages/noya-component/src/applyDiff.ts @@ -0,0 +1,81 @@ +import { partitionDiff } from './partitionDiff'; +import { + FindComponent, + instantiateResolvedComponent, + unresolve, +} from './traversal'; +import { NoyaComponent, NoyaNode, SelectedComponent } from './types'; + +export function applyDiff({ + selection, + component, + findComponent, + enforceSchema, +}: { + selection: SelectedComponent; + component: NoyaComponent; + findComponent: FindComponent; + enforceSchema: (node: NoyaNode) => NoyaNode; +}): { + selection: SelectedComponent; + component: NoyaComponent; +} { + const resolvedNode = instantiateResolvedComponent(findComponent, selection); + + if (!selection.diff) return { selection, component }; + + if (selection.variantID) { + // Merge the diff into the variant's diff + const variant = component.variants?.find( + (variant) => variant.id === selection.variantID, + ); + + if (!variant) return { selection, component }; + + const newVariant = { + ...variant, + diff: { + items: [...(variant.diff?.items ?? []), ...selection.diff.items], + }, + }; + + return { + component: { + ...component, + variants: component.variants?.map((variant) => + variant.id === selection.variantID ? newVariant : variant, + ), + }, + selection: { + ...selection, + diff: undefined, + }, + }; + } + + const [primitivesDiff, compositesDiff] = partitionDiff( + resolvedNode, + selection.diff.items || [], + ); + + const instance = instantiateResolvedComponent(findComponent, { + componentID: selection.componentID, + variantID: selection.variantID, + diff: { items: primitivesDiff }, + }); + + const newRootElement = enforceSchema( + unresolve(instance, { items: compositesDiff }), + ); + + return { + component: { + ...component, + rootElement: newRootElement, + }, + selection: { + ...selection, + diff: undefined, + }, + }; +} diff --git a/packages/noya-component/src/index.ts b/packages/noya-component/src/index.ts index bb9e05497..4c7ed7632 100644 --- a/packages/noya-component/src/index.ts +++ b/packages/noya-component/src/index.ts @@ -1,5 +1,7 @@ +export * from './applyDiff'; export * from './arrayDiff'; export * from './builders'; +export * from './partitionDiff'; export * from './patterns'; export * from './renderResolvedNode'; export * from './renderSVGElement'; diff --git a/packages/site/src/dseditor/utils/partitionDiff.tsx b/packages/noya-component/src/partitionDiff.tsx similarity index 90% rename from packages/site/src/dseditor/utils/partitionDiff.tsx rename to packages/noya-component/src/partitionDiff.tsx index daa27775c..3ad7f6ad8 100644 --- a/packages/site/src/dseditor/utils/partitionDiff.tsx +++ b/packages/noya-component/src/partitionDiff.tsx @@ -1,9 +1,6 @@ -import { - NoyaDiffItem, - NoyaResolvedNode, - ResolvedHierarchy, -} from 'noya-component'; import { isDeepEqual, partition } from 'noya-utils'; +import { ResolvedHierarchy } from './resolvedHierarchy'; +import { NoyaDiffItem, NoyaResolvedNode } from './types'; /** * Partition diff into nodes that apply to a primitive element and nodes that apply diff --git a/packages/noya-component/src/resolvedHierarchy.ts b/packages/noya-component/src/resolvedHierarchy.ts index 2ad079b40..f3b7c8e36 100644 --- a/packages/noya-component/src/resolvedHierarchy.ts +++ b/packages/noya-component/src/resolvedHierarchy.ts @@ -1,6 +1,6 @@ -import { NoyaResolvedNode } from 'noya-component'; import { uuid } from 'noya-utils'; import { defineTree } from 'tree-visit'; +import { NoyaResolvedNode } from './types'; const Hierarchy = defineTree({ getChildren: (node) => { @@ -24,8 +24,39 @@ const Hierarchy = defineTree({ return { ...node, children }; } }, + getLabel: (node) => { + const pathString = `[${node.path + .map((p) => ellipsisMiddle(p, 8)) + .join('/')}]`; + + switch (node.type) { + case 'noyaString': + return [JSON.stringify(node.value), pathString].join(' '); + case 'noyaCompositeElement': + return [node.name || node.componentID, '', pathString].join(' '); + case 'noyaPrimitiveElement': + const classNames = node.classNames.map((c) => c.value).join(' '); + + return [ + [node.name || node.componentID, '', pathString].join(' '), + classNames, + ] + .filter(Boolean) + .map((v, i) => (i > 0 ? ' ' + v : v)) + .join('\n'); + } + }, }); +function ellipsisMiddle(str: string, maxLength: number) { + if (str.length <= maxLength) return str; + + const start = str.slice(0, maxLength / 2); + const end = str.slice(-maxLength / 2); + + return `${start}...${end}`; +} + function clone(node: T): T { return Hierarchy.map(node, (node, transformedChildren) => { switch (node.type) { diff --git a/packages/site/src/dseditor/DSComponentInspector.tsx b/packages/site/src/dseditor/DSComponentInspector.tsx index bb33a3ddc..dc9a7fb15 100644 --- a/packages/site/src/dseditor/DSComponentInspector.tsx +++ b/packages/site/src/dseditor/DSComponentInspector.tsx @@ -18,10 +18,10 @@ import { NoyaVariant, ResolvedHierarchy, SelectedComponent, + applyDiff, describeDiffItem, diffResolvedTrees, instantiateResolvedComponent, - unresolve, } from 'noya-component'; import { Button, @@ -59,7 +59,6 @@ import { DSLayoutTree } from './DSLayoutTree'; import { exportLayout } from './componentLayout'; import { enforceSchema } from './layoutSchema'; import { getNodeName } from './utils/nodeUtils'; -import { partitionDiff } from './utils/partitionDiff'; type Props = Pick< ComponentProps, @@ -557,70 +556,23 @@ export function DSComponentInspector({