Fix OS switching not working on tree view UI

This commit resolves a rendering bug in the tree view component.
Previously, updating the tree collection prior to node updates led to
rendering errors due to the presence of non-existent nodes in the new
collection.

Changes:

- Implement manual control over the rendering process in tree view. This
  includes clearing the rendering queue and currently rendered nodes
  before updates, aligning the rendering process with the updated
  collection.
- Add Cypress E2E tests to test switching between all operating systems
  and script views, ensuring no uncaught errors and preventing
  regression.
- Replace hardcoded operating system lists in the download URL list view
  with a unified `getSupportedOsList()` method from the application,
  reducing duplication and simplifying future updates.
- Rename `initial-nodes` to `nodes` in `TreeView.vue` to reflect their
  mutable nature.
- Centralize the function for getting operating system names into
  `OperatingSystemNames.ts`, improving reusability in E2E tests.
This commit is contained in:
undergroundwires
2023-12-14 09:51:42 +01:00
parent fe3de498c8
commit 3457fe18cf
13 changed files with 285 additions and 76 deletions

View File

@@ -14,10 +14,11 @@
import { defineComponent, computed } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames';
import MenuOptionList from './MenuOptionList.vue';
import MenuOptionListItem from './MenuOptionListItem.vue';
interface IOsViewModel {
interface OperatingSystemOption {
readonly name: string;
readonly os: OperatingSystem;
}
@@ -31,12 +32,12 @@ export default defineComponent({
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
const { application } = injectKey((keys) => keys.useApplication);
const allOses = computed<ReadonlyArray<IOsViewModel>>(
const allOses = computed<ReadonlyArray<OperatingSystemOption>>(
() => application
.getSupportedOsList()
.map((os) : IOsViewModel => ({
.map((os) : OperatingSystemOption => ({
os,
name: renderOsName(os),
name: getOperatingSystemDisplayName(os),
})),
);
@@ -57,13 +58,4 @@ export default defineComponent({
};
},
});
function renderOsName(os: OperatingSystem): string {
switch (os) {
case OperatingSystem.Windows: return 'Windows';
case OperatingSystem.macOS: return 'macOS';
case OperatingSystem.Linux: return 'Linux (preview)';
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
}
}
</script>

View File

@@ -5,9 +5,9 @@
'top-padding': hasTopPadding,
}"
>
<template v-if="initialNodes.length">
<template v-if="nodes.length">
<TreeView
:initial-nodes="initialNodes"
:nodes="nodes"
:selected-leaf-node-ids="selectedScriptNodeIds"
:latest-filter-event="latestFilterEvent"
@node-state-changed="handleNodeChangedEvent($event)"
@@ -61,7 +61,7 @@ export default defineComponent({
}
return {
initialNodes: treeViewInputNodes,
nodes: treeViewInputNodes,
selectedScriptNodeIds,
latestFilterEvent,
handleNodeChangedEvent,

View File

@@ -5,12 +5,19 @@ import { ReadOnlyTreeNode } from '../Node/TreeNode';
import { useNodeStateChangeAggregator } from '../UseNodeStateChangeAggregator';
import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { QueryableNodes } from '../TreeRoot/NodeCollection/Query/QueryableNodes';
import { NodeRenderingStrategy } from './Scheduling/NodeRenderingStrategy';
import { DelayScheduler } from './DelayScheduler';
import { TimeoutDelayScheduler } from './Scheduling/TimeoutDelayScheduler';
import { RenderQueueOrderer } from './Ordering/RenderQueueOrderer';
import { CollapsedParentOrderer } from './Ordering/CollapsedParentOrderer';
export interface NodeRenderingControl {
readonly renderingStrategy: NodeRenderingStrategy;
clearRenderingStates(): void;
notifyRenderingUpdates(): void;
}
/**
* Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes.
*/
@@ -22,7 +29,7 @@ export function useGradualNodeRendering(
initialBatchSize = 30,
subsequentBatchSize = 5,
orderer: RenderQueueOrderer = new CollapsedParentOrderer(),
): NodeRenderingStrategy {
): NodeRenderingControl {
const nodesToRender = new Set<ReadOnlyTreeNode>();
const nodesBeingRendered = shallowRef(new Set<ReadOnlyTreeNode>());
let isRenderingInProgress = false;
@@ -31,6 +38,10 @@ export function useGradualNodeRendering(
const { onNodeStateChange } = useChangeAggregator(treeRootRef);
const { nodes } = useTreeNodes(treeRootRef);
function notifyRenderingUpdates() {
triggerRef(nodesBeingRendered);
}
function updateNodeRenderQueue(node: ReadOnlyTreeNode, isVisible: boolean) {
if (isVisible
&& !nodesToRender.has(node)
@@ -43,16 +54,20 @@ export function useGradualNodeRendering(
}
if (nodesBeingRendered.value.has(node)) {
nodesBeingRendered.value.delete(node);
triggerRef(nodesBeingRendered);
notifyRenderingUpdates();
}
}
}
watch(nodes, (newNodes) => {
function clearRenderingStates() {
nodesToRender.clear();
nodesBeingRendered.value.clear();
}
function initializeAndRenderNodes(newNodes: QueryableNodes) {
clearRenderingStates();
if (!newNodes || newNodes.flattenedNodes.length === 0) {
triggerRef(nodesBeingRendered);
notifyRenderingUpdates();
return;
}
newNodes
@@ -60,6 +75,10 @@ export function useGradualNodeRendering(
.filter((node) => node.state.current.isVisible)
.forEach((node) => nodesToRender.add(node));
beginRendering();
}
watch(nodes, (newNodes) => {
initializeAndRenderNodes(newNodes);
}, { immediate: true });
onNodeStateChange((change) => {
@@ -91,7 +110,7 @@ export function useGradualNodeRendering(
nodesToRender.delete(node);
nodesBeingRendered.value.add(node);
});
triggerRef(nodesBeingRendered);
notifyRenderingUpdates();
scheduler.scheduleNext(
() => renderNextBatch(subsequentBatchSize),
renderingDelayInMs,
@@ -103,6 +122,10 @@ export function useGradualNodeRendering(
}
return {
shouldRender: shouldNodeBeRendered,
renderingStrategy: {
shouldRender: shouldNodeBeRendered,
},
clearRenderingStates,
notifyRenderingUpdates,
};
}

View File

@@ -3,7 +3,7 @@
ref="treeContainerElement"
class="tree"
>
<TreeRoot :tree-root="tree" :rendering-strategy="nodeRenderingScheduler">
<TreeRoot :tree-root="tree" :rendering-strategy="renderingStrategy">
<template #default="slotProps">
<slot name="node-content" v-bind="slotProps" />
</template>
@@ -15,6 +15,7 @@
import {
defineComponent, onMounted, watch,
shallowRef, toRef, shallowReadonly,
nextTick,
} from 'vue';
import { TreeRootManager } from './TreeRoot/TreeRootManager';
import TreeRoot from './TreeRoot/TreeRoot.vue';
@@ -27,7 +28,7 @@ import { useLeafNodeCheckedStateUpdater } from './UseLeafNodeCheckedStateUpdater
import { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent';
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
import { useGradualNodeRendering } from './Rendering/UseGradualNodeRendering';
import { useGradualNodeRendering, NodeRenderingControl } from './Rendering/UseGradualNodeRendering';
import type { PropType } from 'vue';
export default defineComponent({
@@ -35,7 +36,7 @@ export default defineComponent({
TreeRoot,
},
props: {
initialNodes: {
nodes: {
type: Array as PropType<readonly TreeInputNodeData[]>,
default: () => [],
},
@@ -65,7 +66,7 @@ export default defineComponent({
useLeafNodeCheckedStateUpdater(treeRef, toRef(props, 'selectedLeafNodeIds'));
useAutoUpdateParentCheckState(treeRef);
useAutoUpdateChildrenCheckState(treeRef);
const nodeRenderingScheduler = useGradualNodeRendering(treeRef);
const nodeRenderer = useGradualNodeRendering(treeRef);
const { onNodeStateChange } = useNodeStateChangeAggregator(treeRef);
@@ -78,18 +79,44 @@ export default defineComponent({
});
onMounted(() => {
watch(() => props.initialNodes, (nodes) => {
tree.collection.updateRootNodes(nodes);
watch(() => props.nodes, async (nodes) => {
await forceRerenderNodes(
nodeRenderer,
() => tree.collection.updateRootNodes(nodes),
);
}, { immediate: true });
});
return {
treeContainerElement,
nodeRenderingScheduler,
renderingStrategy: nodeRenderer.renderingStrategy,
tree,
};
},
});
/**
* This function is used to manually trigger a re-render of the tree nodes.
* In Vue, manually controlling the rendering process is typically an anti-pattern,
* as Vue's reactivity system is designed to handle updates efficiently. However,
* in this specific case, it's necessary to ensure the correct order of rendering operations.
* This function first clears the rendering queue and the currently rendered nodes,
* ensuring that UI elements relying on outdated node states are removed. This is needed
* in scenarios where the collection is updated before the nodes, which can lead to errors
* if nodes that no longer exist in the collection are still being rendered.
* Using this function, we ensure a clean state before updating the nodes, aligning with
* the updated collection.
*/
async function forceRerenderNodes(
renderer: NodeRenderingControl,
nodeUpdater: () => void,
) {
renderer.clearRenderingStates();
renderer.notifyRenderingUpdates();
await nextTick();
nodeUpdater();
}
</script>
<style scoped lang="scss">