Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions QualityControl/public/common/enums/storageKeys.enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
export const StorageKeysEnum = Object.freeze({
OBJECT_VIEW_LEFT_PANEL_WIDTH: 'object-view-left-panel-width',
OBJECT_VIEW_INFO_VISIBILITY_SETTING: 'object-view-info-visibility-setting',
OBJECT_TREE_OPEN_NODES: 'object-tree-open-nodes',
});
128 changes: 120 additions & 8 deletions QualityControl/public/object/ObjectTree.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
* or submit itself to any jurisdiction.
*/

import { Observable } from '/js/src/index.js';
import { BrowserStorage, Observable, sessionService } from '/js/src/index.js';
import { StorageKeysEnum } from '../common/enums/storageKeys.enum.js';

/**
* This class allows to transforms objects names (A/B/C) into a tree that can have
Expand All @@ -27,6 +28,7 @@ export default class ObjectTree extends Observable {
*/
constructor(name, parent) {
super();
this.storage = new BrowserStorage(StorageKeysEnum.OBJECT_TREE_OPEN_NODES);
this.initTree(name, parent);
}

Expand All @@ -46,12 +48,110 @@ export default class ObjectTree extends Observable {
this.pathString = ''; // 'A/B'
}

/**
* Load the expanded/collapsed state for this node and its children from localStorage.
* Updates the `open` property for the current node and recursively for all children.
*/
loadExpandedNodes() {
if (!this.parent) {
// The main node may not be collapsable or expandable.
// Because of this we also have to load the expanded state of their direct children.
this.children.forEach((child) => child.loadExpandedNodes());
}

const session = sessionService.get();
const key = session.personid.toString();

// We traverse the path to reach the parent object of this node
let parentNode = this.storage.getLocalItem(key) ?? {};
for (let i = 0; i < this.path.length - 1; i++) {
parentNode = parentNode[this.path[i]];
if (!parentNode) {
// Cannot expand marked node because parent path does not exist
return;
}
}

this._applyExpandedNodesRecursive(parentNode, this);
}

/**
* Recursively traverse the stored data and update the tree nodes
* @param {object} data - The current level of the hierarchical expanded nodes object
* @param {ObjectTree} treeNode - The tree node to update
*/
_applyExpandedNodesRecursive(data, treeNode) {
if (data[treeNode.name]) {
treeNode.open = true;
Object.keys(data[treeNode.name]).forEach((childName) => {
const child = treeNode.children.find((child) => child.name === childName);
if (child) {
this._applyExpandedNodesRecursive(data[treeNode.name], child);
}
});
}
};

/**
* Persist the current node's expanded/collapsed state in localStorage.
*/
storeExpandedNodes() {
const session = sessionService.get();
const key = session.personid.toString();
const data = this.storage.getLocalItem(key) ?? {};

// We traverse the path to reach the parent object of this node
let parentNode = data;
for (let i = 0; i < this.path.length - 1; i++) {
const pathKey = this.path[i];
if (!parentNode[pathKey]) {
if (!this.open) {
// Cannot remove marked node because parent path does not exist
// Due to this the marked node also does not exist (so there is nothing to remove)
return;
}

// Parent path does not exist, we create it here so we can mark a deeper node
parentNode[pathKey] = {};
}

parentNode = parentNode[pathKey];
}

if (this.open) {
this._markExpandedNodesRecursive(parentNode, this);
this.storage.setLocalItem(key, data);
} else if (parentNode[this.name]) {
// Deleting from `parentNode` directly updates the `data` object
delete parentNode[this.name];
this.storage.setLocalItem(key, data);
}
}

/**
* Recursively mark a node and all open children in the hierarchical "expanded nodes" object.
* This method updates `data` to reflect the current node's expanded state:
* - If the node has any open children, it creates an object branch and recursively marks those children.
* - If the node has no open children (or is a leaf), it stores a marker value `{}`.
* @param {object} data - The current level in the hierarchical data object where nodes are stored.
* @param {ObjectTree} treeNode - The tree node whose expanded state should be stored.
*/
_markExpandedNodesRecursive(data, treeNode) {
if (!data[treeNode.name]) {
data[treeNode.name] = {};
}
treeNode.children
.filter((child) => child.open)
.forEach((child) => this._markExpandedNodesRecursive(data[treeNode.name], child));
};

/**
* Toggle this node (open/close)
* @returns {undefined}
*/
toggle() {
this.open = !this.open;
this.storeExpandedNodes();
this.notify();
}

Expand All @@ -70,6 +170,7 @@ export default class ObjectTree extends Observable {
openAll() {
this.open = true;
this.children.forEach((child) => child.openAll());
this.storeExpandedNodes();
this.notify();
}

Expand All @@ -80,6 +181,7 @@ export default class ObjectTree extends Observable {
closeAll() {
this.open = false;
this.children.forEach((child) => child.closeAll());
this.storeExpandedNodes();
this.notify();
}

Expand All @@ -97,15 +199,14 @@ export default class ObjectTree extends Observable {
* addChild(o, [], ['A', 'B']) // end inserting, affecting B
* @returns {undefined}
*/
addChild(object, path, pathParent) {
_addChild(object, path = undefined, pathParent = []) {
// Fill the path argument through recursive call
if (!path) {
if (!object.name) {
throw new Error('Object name must exist');
}
path = object.name.split('/');
this.addChild(object, path, []);
this.notify();
this._addChild(object, path);
return;
}

Expand Down Expand Up @@ -134,15 +235,26 @@ export default class ObjectTree extends Observable {
}

// Pass to child
subtree.addChild(object, path, fullPath);
subtree._addChild(object, path, fullPath);
}

/**
* Add a list of objects by calling `addChild`
* Add a single object as a child node
* @param {object} object - child to be added
*/
addOneChild(object) {
this._addChild(object);
this.loadExpandedNodes();
this.notify();
}

/**
* Add a list of objects as child nodes
* @param {Array<object>} objects - children to be added
* @returns {undefined}
*/
addChildren(objects) {
objects.forEach((object) => this.addChild(object));
objects.forEach((object) => this._addChild(object));
this.loadExpandedNodes();
this.notify();
}
}
1 change: 0 additions & 1 deletion QualityControl/test/public/features/filterTest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => {
{ waitUntil: 'networkidle0' },
);

await extendTree(3, 5);
let rowCount = await page.evaluate(() => document.querySelectorAll('tr').length);
strictEqual(rowCount, 7);

Expand Down
43 changes: 32 additions & 11 deletions QualityControl/test/public/pages/object-tree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
* or submit itself to any jurisdiction.
*/

import { strictEqual, ok, deepStrictEqual } from 'node:assert';
import { strictEqual, ok, deepStrictEqual, notDeepStrictEqual } from 'node:assert';
import { delay } from '../../testUtils/delay.js';
import { getLocalStorage } from '../../testUtils/localStorage.js';
import { getLocalStorage, getLocalStorageAsJson } from '../../testUtils/localStorage.js';
import { StorageKeysEnum } from '../../../public/common/enums/storageKeys.enum.js';

const OBJECT_TREE_PAGE_PARAM = '?page=objectTree';
Expand Down Expand Up @@ -43,15 +43,22 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
ok(rowsCount > 1); // more than 1 object in the tree
});

await testParent.test('should not preserve state if refreshed not in run mode', { timeout }, async () => {
const tbodyPath = 'section > div > div > div > table > tbody';
await page.locator(`${tbodyPath} > tr:nth-child(2)`).click();
await testParent.test('should preserve state if refreshed', { timeout }, async () => {
const selector = 'section > div > div > div > table > tbody > tr:nth-child(2)';
await page.locator(selector).click();
await page.reload({ waitUntil: 'networkidle0' });

const rowCount = await page.evaluate(() =>
const rowCountExpanded = await page.evaluate(() =>
document.querySelectorAll('section > div > div > div > table > tbody > tr').length);

strictEqual(rowCount, 2);
await page.locator(selector).click();
await page.reload({ waitUntil: 'networkidle0' });

const rowCountCollapsed = await page.evaluate(() =>
document.querySelectorAll('section > div > div > div > table > tbody > tr').length);

strictEqual(rowCountExpanded, 3);
strictEqual(rowCountCollapsed, 2);
});

await testParent.test('should have a button to sort by (default "Name" ASC)', async () => {
Expand Down Expand Up @@ -136,10 +143,6 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
async () => {
const dragAmount = 35;
await page.reload({ waitUntil: 'networkidle0' });
await page.evaluate(() => document.querySelector('tr.object-selectable:nth-child(2)').click());
await delay(500);
await page.evaluate(() => document.querySelector('tr.object-selectable:nth-child(3)').click());
await delay(500);
await page.evaluate(() => document.querySelector('tr.object-selectable:nth-child(4)').click());
await delay(1000);
const panelWidth = await page.evaluate(() =>
Expand Down Expand Up @@ -228,6 +231,24 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
}
);

await testParent.test('should update local storage when tree node is clicked', { timeout }, async () => {
const selector = 'section > div > div > div > table > tbody > tr:nth-child(2)';
const personid = await page.evaluate(() => window.model.session.personid);
const storageKey = `${StorageKeysEnum.OBJECT_TREE_OPEN_NODES}-${personid}`;

await page.locator(selector).click();
const localStorageBefore = await getLocalStorageAsJson(page, storageKey);

await page.locator(selector).click();
const localStorageAfter = await getLocalStorageAsJson(page, storageKey);

notDeepStrictEqual(
localStorageBefore,
localStorageAfter,
'local storage should have changed after clicking a tree node',
);
});

await testParent.test('should sort list of histograms by name in descending order', async () => {
await page.locator('#sortTreeButton').click();
const sortingByNameOptionPath = '#sortTreeButton > div > a:nth-child(2)';
Expand Down
Loading