diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.ts deleted file mode 100644 index 4ea614db..00000000 --- a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ReadOnlyTreeNode } from '../../Node/TreeNode'; -import { RenderQueueOrderer } from './RenderQueueOrderer'; - -export class CollapseDepthOrderer implements RenderQueueOrderer { - public orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { - return orderNodes(nodes); - } -} - -function orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { - return [...nodes] - .sort((a, b) => { - const [aCollapseStatus, bCollapseStatus] = [isNodeCollapsed(a), isNodeCollapsed(b)]; - if (aCollapseStatus !== bCollapseStatus) { - return (aCollapseStatus ? 1 : 0) - (bCollapseStatus ? 1 : 0); - } - return a.hierarchy.depthInTree - b.hierarchy.depthInTree; - }); -} - -function isNodeCollapsed(node: ReadOnlyTreeNode): boolean { - if (!node.state.current.isExpanded) { - return true; - } - if (node.hierarchy.parent) { - return isNodeCollapsed(node.hierarchy.parent); - } - return false; -} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts new file mode 100644 index 00000000..0f1fc854 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts @@ -0,0 +1,35 @@ +import { ReadOnlyTreeNode } from '../../Node/TreeNode'; +import { RenderQueueOrderer } from './RenderQueueOrderer'; + +export class CollapsedParentOrderer implements RenderQueueOrderer { + public orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { + return orderNodes(nodes); + } +} + +function orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { + return [...nodes] + .map((node, index) => ({ node, index })) + .sort((a, b) => { + const [ + isANodeOfCollapsedParent, + isBNodeOfCollapsedParent, + ] = [isParentCollapsed(a.node), isParentCollapsed(b.node)]; + if (isANodeOfCollapsedParent !== isBNodeOfCollapsedParent) { + return (isANodeOfCollapsedParent ? 1 : 0) - (isBNodeOfCollapsedParent ? 1 : 0); + } + return a.index - b.index; + }) + .map(({ node }) => node); +} + +function isParentCollapsed(node: ReadOnlyTreeNode): boolean { + const parentNode = node.hierarchy.parent; + if (parentNode) { + if (!parentNode.state.current.isExpanded) { + return true; + } + return isParentCollapsed(parentNode); + } + return false; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts index 34661719..5429c6b5 100644 --- a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts @@ -9,7 +9,7 @@ import { NodeRenderingStrategy } from './Scheduling/NodeRenderingStrategy'; import { DelayScheduler } from './DelayScheduler'; import { TimeoutDelayScheduler } from './Scheduling/TimeoutDelayScheduler'; import { RenderQueueOrderer } from './Ordering/RenderQueueOrderer'; -import { CollapseDepthOrderer } from './Ordering/CollapseDepthOrderer'; +import { CollapsedParentOrderer } from './Ordering/CollapsedParentOrderer'; /** * Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes. @@ -21,7 +21,7 @@ export function useGradualNodeRendering( scheduler: DelayScheduler = new TimeoutDelayScheduler(), initialBatchSize = 30, subsequentBatchSize = 5, - orderer: RenderQueueOrderer = new CollapseDepthOrderer(), + orderer: RenderQueueOrderer = new CollapsedParentOrderer(), ): NodeRenderingStrategy { const nodesToRender = new Set(); const nodesBeingRendered = shallowRef(new Set()); diff --git a/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.spec.ts b/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.spec.ts deleted file mode 100644 index df6436e8..00000000 --- a/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode'; -import { CollapseDepthOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer'; -import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub'; -import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub'; -import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub'; -import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub'; - -describe('CollapseDepthOrderer', () => { - describe('orderNodes', () => { - it('should order by collapsed state and then by depth in', () => { - // arrange - const node1 = createNodeForOrder({ - isExpanded: false, - depthInTree: 1, - }); - const node2 = createNodeForOrder({ - isExpanded: true, - depthInTree: 2, - }); - const node3 = createNodeForOrder({ - isExpanded: false, - depthInTree: 3, - }); - const node4 = createNodeForOrder({ - isExpanded: false, - depthInTree: 4, - }); - const nodes = [node1, node2, node3, node4]; - const expectedOrder = [node2, node1, node3, node4]; - // act - const orderer = new CollapseDepthOrderer(); - const orderedNodes = orderer.orderNodes(nodes); - // assert - expect(orderedNodes.map((node) => node.id)).to.deep - .equal(expectedOrder.map((node) => node.id)); - }); - it('should handle parent collapsed state', () => { - // arrange - const collapsedParent = createNodeForOrder({ - isExpanded: false, - depthInTree: 0, - }); - const childWithCollapsedParent = createNodeForOrder({ - isExpanded: true, - depthInTree: 1, - parent: collapsedParent, - }); - const deepExpandedNode = createNodeForOrder({ - isExpanded: true, - depthInTree: 3, - }); - const nodes = [childWithCollapsedParent, collapsedParent, deepExpandedNode]; - const expectedOrder = [ - deepExpandedNode, // comes first due to collapse parent of child - collapsedParent, - childWithCollapsedParent, - ]; - // act - const orderer = new CollapseDepthOrderer(); - const orderedNodes = orderer.orderNodes(nodes); - // assert - expect(orderedNodes.map((node) => node.id)).to.deep - .equal(expectedOrder.map((node) => node.id)); - }); - }); -}); - -function createNodeForOrder(options: { - readonly isExpanded: boolean; - readonly depthInTree: number; - readonly parent?: TreeNode; -}): TreeNode { - return new TreeNodeStub() - .withId([ - `isExpanded: ${options.isExpanded}`, - `depthInTree: ${options.depthInTree}`, - ...(options.parent ? [`parent: ${options.parent.id}`] : []), - ].join(', ')) - .withState( - new TreeNodeStateAccessStub() - .withCurrent( - new TreeNodeStateDescriptorStub() - .withVisibility(true) - .withExpansion(options.isExpanded), - ), - ) - .withHierarchy( - new HierarchyAccessStub() - .withDepthInTree(options.depthInTree) - .withParent(options.parent), - ); -} diff --git a/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts b/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts new file mode 100644 index 00000000..ab28742e --- /dev/null +++ b/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts @@ -0,0 +1,165 @@ +import { describe, it, expect } from 'vitest'; +import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode'; +import { CollapsedParentOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer'; +import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub'; +import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub'; +import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub'; +import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub'; + +describe('CollapsedParentOrderer', () => { + describe('orderNodes', () => { + const scenarios: ReadonlyArray<{ + readonly description: string; + readonly nodes: TreeNode[]; + readonly expectedOrderedNodes: TreeNode[]; + }> = [ + { + description: 'handles empty nodes list', + nodes: [], + expectedOrderedNodes: [], + }, + (() => { + const expectedNode = createNodeForOrder({ + isExpanded: false, + }); + return { + description: 'handles single node list', + nodes: [expectedNode], + expectedOrderedNodes: [expectedNode], + }; + })(), + (() => { + const node1 = createNodeForOrder({ + isExpanded: false, + }); + const node2 = createNodeForOrder({ + isExpanded: true, + }); + const node3 = createNodeForOrder({ + isExpanded: true, + }); + const node4 = createNodeForOrder({ + isExpanded: false, + }); + return { + description: 'orders by index ignoring self collapsed state', + nodes: [node1, node2, node3, node4], + expectedOrderedNodes: [node1, node2, node3, node4], + }; + })(), + (() => { + const node1 = createNodeForOrder({ + isExpanded: false, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node2 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node3 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node4 = createNodeForOrder({ + isExpanded: false, + parent: createNodeForOrder({ isExpanded: true }), + }); + return { + description: 'orders by index if all parents are expanded', + nodes: [node1, node2, node3, node4], + expectedOrderedNodes: [node1, node2, node3, node4], + }; + })(), + (() => { + const node1 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: false }), + }); + const node2 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node3 = createNodeForOrder({ + isExpanded: true, + }); + const node4 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: false }), + }); + return { + description: 'order by parent collapsed state then by index', + nodes: [node1, node2, node3, node4], + expectedOrderedNodes: [node2, node3, node1, node4], + }; + })(), + (() => { + const collapsedNode = createNodeForOrder({ + isExpanded: false, + }); + const collapsedNodeChild = createNodeForOrder({ + isExpanded: true, + parent: collapsedNode, + }); + const collapsedNodeNestedChild = createNodeForOrder({ + isExpanded: true, + parent: collapsedNodeChild, + }); + const expandedNode = createNodeForOrder({ + isExpanded: true, + }); + const expandedNodeChild = createNodeForOrder({ + isExpanded: true, + parent: expandedNode, + }); + const expandedNodeNestedChild = createNodeForOrder({ + isExpanded: true, + parent: expandedNodeChild, + }); + return { + description: 'should handle deep parent collapsed state', + nodes: [ + collapsedNode, collapsedNodeChild, collapsedNodeNestedChild, + expandedNode, expandedNodeChild, expandedNodeNestedChild, + ], + expectedOrderedNodes: [ + collapsedNode, expandedNode, expandedNodeChild, + expandedNodeNestedChild, collapsedNodeChild, collapsedNodeNestedChild, + ], + }; + })(), + ]; + scenarios.forEach(({ description, nodes, expectedOrderedNodes }) => { + it(description, () => { + // act + const orderer = new CollapsedParentOrderer(); + const orderedNodes = orderer.orderNodes(nodes); + // assert + expect(orderedNodes.map((node) => node.id)).to.deep + .equal(expectedOrderedNodes.map((node) => node.id)); + }); + }); + }); +}); + +function createNodeForOrder(options: { + readonly isExpanded: boolean; + readonly parent?: TreeNode; +}): TreeNode { + return new TreeNodeStub() + .withId([ + `isExpanded: ${options.isExpanded}`, + ...(options.parent ? [`parent: ${options.parent.id}`] : []), + ].join(', ')) + .withState( + new TreeNodeStateAccessStub() + .withCurrent( + new TreeNodeStateDescriptorStub() + .withVisibility(true) + .withExpansion(options.isExpanded), + ), + ) + .withHierarchy( + new HierarchyAccessStub() + .withParent(options.parent), + ); +}