From d2ce4fdb0903eac79c3296988190c81d57cf8ee5 Mon Sep 17 00:00:00 2001 From: Damien Retzinger Date: Mon, 29 Apr 2024 09:02:58 -0400 Subject: [PATCH] feat(design): add immutable tree transform (#2776) --- libs/design/tree/src/public_api.ts | 1 + libs/design/tree/src/utils/transform.spec.ts | 223 +++++++++++++++++++ libs/design/tree/src/utils/transform.ts | 40 ++++ 3 files changed, 264 insertions(+) create mode 100644 libs/design/tree/src/utils/transform.spec.ts create mode 100644 libs/design/tree/src/utils/transform.ts diff --git a/libs/design/tree/src/public_api.ts b/libs/design/tree/src/public_api.ts index de42b2cfcc..d44ceb7734 100644 --- a/libs/design/tree/src/public_api.ts +++ b/libs/design/tree/src/public_api.ts @@ -4,4 +4,5 @@ export { DaffTreeItemDirective } from './tree-item/tree-item.directive'; export { DaffTreeData } from './interfaces/tree-data'; export { DaffTreeUi } from './interfaces/tree-ui'; export { daffTransformTreeInPlace } from './utils/transform-in-place'; +export { daffTransformTree } from './utils/transform'; export { DaffTreeRenderMode } from './interfaces/tree-render-mode'; diff --git a/libs/design/tree/src/utils/transform.spec.ts b/libs/design/tree/src/utils/transform.spec.ts new file mode 100644 index 0000000000..0b0bac16e9 --- /dev/null +++ b/libs/design/tree/src/utils/transform.spec.ts @@ -0,0 +1,223 @@ +import { daffTransformTree } from './transform'; +import { DaffTreeData } from '../interfaces/tree-data'; + +describe('daffTransformTree', () => { + it('should transform a tree like structure into a DaffTreeData', () => { + const myTree = { + name: 'Test', + link: '/test.html', + carbonScore: 1, + children: [{ + name: 'Test 2', + link: '/test2.html', + carbonScore: 2, + children: [], + }], + }; + const transformer = (node: typeof myTree): DaffTreeData<{ carbonScore: number}> => ({ + title: node.name, + url: node.link, + id: node.name, + items: [], + data: { + carbonScore: node.carbonScore, + }, + }); + + expect(daffTransformTree(myTree, transformer, 'children')).toEqual({ + title: 'Test', + url: '/test.html', + data: { + carbonScore: 1, + }, + items: [ + { + title: 'Test 2', + url: '/test2.html', + data: { + carbonScore: 2, + }, + items: [], + id: 'Test 2', + }, + ], + id: 'Test', + }); + }); + + it('should transform a more complex tree structure into a DaffTreeData', () => { + const myTree = { + name: 'Test', + link: '/test.html', + carbonScore: 1, + children: [ + { + name: 'Test 2', + link: '/test2.html', + carbonScore: 2, + children: [], + }, + { + name: 'Test 3', + link: '/test3.html', + carbonScore: 3, + children: [ + { + name: 'Test 4', + link: '/test4.html', + carbonScore: 4, + children: [], + }, + { + name: 'Test 5', + link: '/test5.html', + carbonScore: 5, + children: [], + }, + ], + }, + ], + }; + const transformer = (node: typeof myTree): DaffTreeData<{ carbonScore: number}> => ({ + title: node.name, + url: node.link, + id: node.name, + items: [], + data: { + carbonScore: node.carbonScore, + }, + }); + + expect(daffTransformTree(myTree, transformer, 'children')).toEqual({ + title: 'Test', + url: '/test.html', + data: { + carbonScore: 1, + }, + items: [ + { + title: 'Test 2', + url: '/test2.html', + data: { + carbonScore: 2, + }, + items: [], + id: 'Test 2', + }, + { + title: 'Test 3', + url: '/test3.html', + data: { + carbonScore: 3, + }, + items: [ + { + title: 'Test 4', + url: '/test4.html', + data: { + carbonScore: 4, + }, + items: [], + id: 'Test 4', + }, + { + title: 'Test 5', + url: '/test5.html', + data: { + carbonScore: 5, + }, + items: [], + id: 'Test 5', + }, + ], + id: 'Test 3', + }, + ], + id: 'Test', + }); + }); + + it('should not modify the original tree structure', () => { + const myTree = { + name: 'test', + tacos: 1, + items: [], + }; + Object.freeze(myTree); + const transform = (t: typeof myTree): DaffTreeData<{ tacos: number}> => ({ + title: t.name, + url: '', + id: '', + items: [], + data: { + tacos: t.tacos, + }, + }); + expect(() => daffTransformTree(myTree, transform, 'items')).not.toThrowError(); + }); + + it('should allow you to add more items during transform', () => { + const myTree = { + name: 'test', + tacos: 1, + items: [ + { + name: 'test', + tacos: 2, + items: [], + }, + ], + }; + const transform = (t: typeof myTree): DaffTreeData<{ tacos: number}> => { + const items = t.tacos === 1 ? [ + { + title: 'test', + url: '', + id: '', + items: [], + data: { + tacos: 3, + }, + }, + ] : []; + return { + title: t.name, + url: '', + id: '', + items, + data: { + tacos: t.tacos, + }, + }; + }; + + expect(daffTransformTree(myTree, transform, 'items')).toEqual({ + title: 'test', + url: '', + id: '', + items: [ + { + title: 'test', + url: '', + id: '', + items: [], + data: { + tacos: 3, + }, + }, + { + title: 'test', + url: '', + id: '', + items: [], + data: { + tacos: 2, + }, + }, + ], + data: { + tacos: 1, + }, + }); + }); +}); diff --git a/libs/design/tree/src/utils/transform.ts b/libs/design/tree/src/utils/transform.ts new file mode 100644 index 0000000000..186369c629 --- /dev/null +++ b/libs/design/tree/src/utils/transform.ts @@ -0,0 +1,40 @@ +import { RecursiveTreeKeyOfType } from '../interfaces/recursive-key'; +import { DaffTreeData } from '../interfaces/tree-data'; + +/** + * Transform a tree-like structure into a {@link DaffTreeData}. + * + * @param tree - The data structure representing tree-like data. + * @param transformFn - A user-supplied function that will transform the user + * type into a {@link DaffTreeData} + * @param key - The property of the your tree that indicates which + * key contains the "children" of your tree structure. + * + */ +export const daffTransformTree = < + // eslint-disable-next-line @typescript-eslint/ban-types + T extends Record, + V +>( + tree: T, + transformFn: (type: T) => DaffTreeData, + key: RecursiveTreeKeyOfType, +): DaffTreeData => { + + const transformedTree: DaffTreeData = transformFn(tree); + + const queue: { node: T; parent: DaffTreeData }[] = [{ node: tree, parent: transformedTree }]; + + while (queue.length > 0) { + const { node, parent } = queue.shift(); + + const childItems = node[key]; + for (const child of childItems) { + const transformedChild: DaffTreeData = transformFn(child); + parent.items.push(transformedChild); + queue.push({ node: child, parent: transformedChild }); + } + } + + return transformedTree; +};