From 73e2d60823f21fb91b39a9995972f24a88f1fdc7 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 15:54:15 +0100 Subject: [PATCH 01/21] feat: add search bar to the table header of the object tree page --- .../public/object/objectTreePage.js | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index f53b09d38..b0351584e 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -46,9 +46,15 @@ export default (model) => { const objectsLoaded = object.list; const objectsToDisplay = objectsLoaded.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); - return virtualTable(model, 'main', objectsToDisplay); + return [ + tableSearchInput(model.object), + virtualTable(model, 'main', objectsToDisplay), + ]; } - return tableShow(model); + return [ + tableSearchInput(model.object), + tableShow(model), + ]; }, Failure: () => null, // Notification is displayed })), @@ -167,7 +173,17 @@ const tableShow = (model) => h('table.table.table-sm.text-no-select', [ h('thead', [h('tr', [h('th', 'Name')])]), h('tbody', [treeRows(model)]), - ]); + ]) + +const tableSearchInput = (qcObject) => + h('input.form-control.form-inline.m2', { + id: 'searchObjectTree', + placeholder: 'Search', + type: 'text', + value: qcObject.searchInput, + disabled: qcObject.queryingObjects ? true : false, + oninput: (e) => qcObject.search(e.target.value), + }) /** * Shows a list of lines of objects From c4b9b58866fecabb533c5b0b39cde5a09592c8d0 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 17:53:22 +0100 Subject: [PATCH 02/21] feat: add the collapse all button to the table header --- .../public/object/objectTreeHeader.js | 17 +---------- .../public/object/objectTreePage.js | 29 ++++++++++++++++--- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/QualityControl/public/object/objectTreeHeader.js b/QualityControl/public/object/objectTreeHeader.js index fe53637d8..e9787ef1b 100644 --- a/QualityControl/public/object/objectTreeHeader.js +++ b/QualityControl/public/object/objectTreeHeader.js @@ -13,7 +13,7 @@ */ import { h } from '/js/src/index.js'; -import { iconCollapseUp, iconArrowBottom, iconArrowTop } from '/js/src/icons.js'; +import { iconArrowBottom, iconArrowTop } from '/js/src/icons.js'; import { filterPanelToggleButton } from '../common/filters/filterViews.js'; /** @@ -56,21 +56,6 @@ export default function objectTreeHeader(qcObject, filterModel) { ]), ]), ' ', - h('button.btn', { - title: 'Close whole tree', - onclick: () => qcObject.tree.closeAll(), - disabled: Boolean(qcObject.searchInput), - }, iconCollapseUp()), - ' ', - h('input.form-control.form-inline.mh1.w-33', { - id: 'searchObjectTree', - placeholder: 'Search', - type: 'text', - value: qcObject.searchInput, - disabled: qcObject.queryingObjects ? true : false, - oninput: (e) => qcObject.search(e.target.value), - }), - ' ', ]), }; } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index b0351584e..d053de268 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -12,7 +12,15 @@ * or submit itself to any jurisdiction. */ -import { h, iconBarChart, iconCaretRight, iconResizeBoth, iconCaretBottom, iconCircleX } from '/js/src/index.js'; +import { + h, + iconCollapseUp, + iconBarChart, + iconCaretRight, + iconResizeBoth, + iconCaretBottom, + iconCircleX +} from '/js/src/index.js'; import { spinner } from '../common/spinner.js'; import { draw } from '../common/object/draw.js'; import timestampSelectForm from './../common/timestampSelectForm.js'; @@ -47,12 +55,12 @@ export default (model) => { const objectsToDisplay = objectsLoaded.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); return [ - tableSearchInput(model.object), + tableHeader(model.object), virtualTable(model, 'main', objectsToDisplay), ]; } return [ - tableSearchInput(model.object), + tableHeader(model.object), tableShow(model), ]; }, @@ -175,8 +183,21 @@ const tableShow = (model) => h('tbody', [treeRows(model)]), ]) +const tableHeader = (qcObject) => + h('.flex-row.w-100', [ + tableSearchInput(qcObject), + tableCollapseAll(qcObject), + ]) + +const tableCollapseAll = (qcObject) => + h('button.btn.m2', { + title: 'Close whole tree', + onclick: () => qcObject.tree.closeAll(), + disabled: Boolean(qcObject.searchInput), + }, iconCollapseUp()) + const tableSearchInput = (qcObject) => - h('input.form-control.form-inline.m2', { + h('input.form-control.form-inline.m2.flex-grow', { id: 'searchObjectTree', placeholder: 'Search', type: 'text', From 1b8eeabd88d2b15bacd971cfd119ae1a18675d15 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 19:38:05 +0100 Subject: [PATCH 03/21] feat: add reusable component for adding table sort buttons --- QualityControl/public/app.css | 17 +++++ .../public/common/enums/columnSort.enum.js | 24 +++++++ QualityControl/public/common/sortButton.js | 72 +++++++++++++++++++ QualityControl/public/object/QCObject.js | 13 +--- .../public/object/objectTreeHeader.js | 16 ----- .../public/object/objectTreePage.js | 18 ++++- 6 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 QualityControl/public/common/enums/columnSort.enum.js create mode 100644 QualityControl/public/common/sortButton.js diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index 956128933..fe8b491dd 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -187,3 +187,20 @@ .whitespace-nowrap { white-space: nowrap; } + +.sort-button { + .hover-icon { + display: none; + opacity: 0.6; + } + + &:hover { + .current-icon { + display: none; + } + + .hover-icon { + display: inline-block; + } + } +} diff --git a/QualityControl/public/common/enums/columnSort.enum.js b/QualityControl/public/common/enums/columnSort.enum.js new file mode 100644 index 000000000..ba58e6619 --- /dev/null +++ b/QualityControl/public/common/enums/columnSort.enum.js @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Enumeration for sort directions + * @enum {number} + * @readonly + */ +export const SortDirectionsEnum = Object.freeze({ + NONE: 0, + ASC: 1, + DESC: -1, +}); diff --git a/QualityControl/public/common/sortButton.js b/QualityControl/public/common/sortButton.js new file mode 100644 index 000000000..936d411fd --- /dev/null +++ b/QualityControl/public/common/sortButton.js @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js'; +import { h, iconCircleX, iconArrowBottom, iconArrowTop } from '/js/src/index.js'; + +/** + * Get the icon for the sort direction. + * @param {SortDirectionsEnum} direction - direction of the sort. + * @returns {vnode} the correct icon related to the direction. + */ +const getSortIcon = (direction) => { + if (direction === SortDirectionsEnum.ASC) { + return iconArrowTop(); + } + if (direction === SortDirectionsEnum.DESC) { + return iconArrowBottom(); + } + return iconCircleX(); +}; + +/** + * @callback SortClickCallback + * @param {string} label - The label of the column being sorted. + * @param {number} order - The next sort direction in the cycle. + * @param {vnode} icon - The VNode for the icon representing the next sort state. + * @returns {void} + */ + +/** + * Renders a sortable table header button that cycles through sort states. + * Displays the current sort icon and a preview icon of the next state on hover. + * @param {object} props - The component properties. + * @param {number} props.order - The current sort direction value from SortDirectionsEnum. + * @param {object|undefined} props.icon - The VNode/element for the current active sort icon. + * @param {string} props.label - The display text for the column header. + * @param {SortClickCallback} props.onclick - Callback triggered on click. + * @param {Array} [props.sortOptions] - Array of SortDirectionsEnum values defining the + * order of the sort cycle. Defaults to all enum values. + * @returns {object} A HyperScript VNode representing the sortable button. + */ +export const sortableTableHead = ({ + order, + icon, + label, + onclick, + sortOptions = [...Object.values(SortDirectionsEnum)] +}) => { + const currentIndex = sortOptions.indexOf(order); + const nextIndex = (currentIndex + 1) % sortOptions.length; + const nextSortOrder = sortOptions[nextIndex]; + const hoverIcon = getSortIcon(nextSortOrder); + + return h('button.btn.sort-button', { onclick: () => onclick(label, nextSortOrder, hoverIcon) }, [ + label, + h('span.icon-container', [ + h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]), + h('span.hover-icon', [getSortIcon(nextSortOrder)]) + ]) + ]); +} diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index 840215994..83f69e2a9 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -47,7 +47,6 @@ export default class QCObject extends BaseViewModel { title: 'Name', order: 1, icon: iconArrowTop(), - open: false, }; this.tree = new ObjectTree('database'); @@ -115,15 +114,6 @@ export default class QCObject extends BaseViewModel { this.notify(); } - /** - * Toggle the display of the sort by dropdown - * @returns {undefined} - */ - toggleSortDropdown() { - this.sortBy.open = !this.sortBy.open; - this.notify(); - } - /** * Computes the final list of objects to be seen by user depending on search input from user * If any of those changes, this method should be called to update the outputs. @@ -189,7 +179,7 @@ export default class QCObject extends BaseViewModel { this._computeFilters(); - this.sortBy = { field, title, order, icon, open: false }; + this.sortBy = { field, title, order, icon }; this.notify(); } @@ -253,7 +243,6 @@ export default class QCObject extends BaseViewModel { title: 'Name', order: 1, icon: iconArrowTop(), - open: false, }; this._computeFilters(); diff --git a/QualityControl/public/object/objectTreeHeader.js b/QualityControl/public/object/objectTreeHeader.js index e9787ef1b..742040c56 100644 --- a/QualityControl/public/object/objectTreeHeader.js +++ b/QualityControl/public/object/objectTreeHeader.js @@ -13,7 +13,6 @@ */ import { h } from '/js/src/index.js'; -import { iconArrowBottom, iconArrowTop } from '/js/src/icons.js'; import { filterPanelToggleButton } from '../common/filters/filterViews.js'; /** @@ -41,21 +40,6 @@ export default function objectTreeHeader(qcObject, filterModel) { rightCol: h('.w-25.flex-row.items-center.g2.justify-end', [ filterModel.isRunModeActivated ? null : filterPanelToggleButton(filterModel), - ' ', - h('.dropdown', { - id: 'sortTreeButton', title: 'Sort by', class: qcObject.sortBy.open ? 'dropdown-open' : '', - }, [ - h('button.btn', { - title: 'Sort by', - onclick: () => qcObject.toggleSortDropdown(), - }, [qcObject.sortBy.title, ' ', qcObject.sortBy.icon]), - h('.dropdown-menu.text-left', [ - sortMenuItem(qcObject, 'Name', 'Sort by name ASC', iconArrowTop(), 'name', 1), - sortMenuItem(qcObject, 'Name', 'Sort by name DESC', iconArrowBottom(), 'name', -1), - - ]), - ]), - ' ', ]), }; } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index d053de268..0bd9311c6 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -19,7 +19,7 @@ import { iconCaretRight, iconResizeBoth, iconCaretBottom, - iconCircleX + iconCircleX, } from '/js/src/index.js'; import { spinner } from '../common/spinner.js'; import { draw } from '../common/object/draw.js'; @@ -28,6 +28,8 @@ import virtualTable from './virtualTable.js'; import { defaultRowAttributes, qcObjectInfoPanel } from '../common/object/objectInfoCard.js'; import { downloadButton } from '../common/downloadButton.js'; import { resizableDivider } from '../common/resizableDivider.js'; +import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js'; +import { sortableTableHead } from '../common/sortButton.js'; /** * Shows a page to explore though a tree of objects with a preview on the right if clicked @@ -179,7 +181,19 @@ const statusBarRight = (model) => model.object.selected */ const tableShow = (model) => h('table.table.table-sm.text-no-select', [ - h('thead', [h('tr', [h('th', 'Name')])]), + h('thead', [ + h('tr', [ + h('th', sortableTableHead({ + order: model.object.sortBy.order, + icon: model.object.sortBy.icon, + label: 'Name', + sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], + onclick: (label, order, icon) => { + model.object.sortTree(label, 'name', order, icon) + } + })) + ]) + ]), h('tbody', [treeRows(model)]), ]) From 4273674da21d6296bdd0d0df23087ee59824d811 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 19:41:32 +0100 Subject: [PATCH 04/21] feat: add more margin on the sort direction icon --- QualityControl/public/common/sortButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/common/sortButton.js b/QualityControl/public/common/sortButton.js index 936d411fd..f7363c6e4 100644 --- a/QualityControl/public/common/sortButton.js +++ b/QualityControl/public/common/sortButton.js @@ -64,7 +64,7 @@ export const sortableTableHead = ({ return h('button.btn.sort-button', { onclick: () => onclick(label, nextSortOrder, hoverIcon) }, [ label, - h('span.icon-container', [ + h('span.icon-container.mh1', [ h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]), h('span.hover-icon', [getSortIcon(nextSortOrder)]) ]) From fc4452f2c365aeeee451963258ee95df019a7e9b Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 19:46:39 +0100 Subject: [PATCH 05/21] feat: add sortable column to virtual table --- QualityControl/public/object/virtualTable.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/QualityControl/public/object/virtualTable.js b/QualityControl/public/object/virtualTable.js index 29ebda4d4..ddb5326f1 100644 --- a/QualityControl/public/object/virtualTable.js +++ b/QualityControl/public/object/virtualTable.js @@ -12,6 +12,8 @@ * or submit itself to any jurisdiction. */ +import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js'; +import { sortableTableHead } from '../common/sortButton.js'; import { h, iconBarChart } from '/js/src/index.js'; let ROW_HEIGHT = 33.6; @@ -102,7 +104,15 @@ const objectFullRow = (model, item, location) => const tableHeader = () => h('table.table.table-sm.text-no-select', { style: 'margin-bottom:0', - }, h('thead', [h('tr', [h('th', 'Name')])])); + }, h('thead', [h('tr', [h('th', sortableTableHead({ + order: model.object.sortBy.order, + icon: model.object.sortBy.icon, + label: 'Name', + sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], + onclick: (label, order, icon) => { + model.object.sortTree(label, 'name', order, icon) + } + }))])])); /** * Set styles of the floating table and its position inside the big div .tableLogsContentPlaceholder From 4b06d78b300726c8c71f4e3428c1ab77fcddfebb Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 19:49:56 +0100 Subject: [PATCH 06/21] style: fix linting errors --- QualityControl/public/common/sortButton.js | 8 +++---- .../public/object/objectTreeHeader.js | 21 ++++-------------- .../public/object/objectTreePage.js | 16 +++++++------- QualityControl/public/object/virtualTable.js | 22 +++++++++++-------- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/QualityControl/public/common/sortButton.js b/QualityControl/public/common/sortButton.js index f7363c6e4..76c24b633 100644 --- a/QualityControl/public/common/sortButton.js +++ b/QualityControl/public/common/sortButton.js @@ -55,7 +55,7 @@ export const sortableTableHead = ({ icon, label, onclick, - sortOptions = [...Object.values(SortDirectionsEnum)] + sortOptions = [...Object.values(SortDirectionsEnum)], }) => { const currentIndex = sortOptions.indexOf(order); const nextIndex = (currentIndex + 1) % sortOptions.length; @@ -66,7 +66,7 @@ export const sortableTableHead = ({ label, h('span.icon-container.mh1', [ h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]), - h('span.hover-icon', [getSortIcon(nextSortOrder)]) - ]) + h('span.hover-icon', [getSortIcon(nextSortOrder)]), + ]), ]); -} +}; diff --git a/QualityControl/public/object/objectTreeHeader.js b/QualityControl/public/object/objectTreeHeader.js index 742040c56..00b8b0447 100644 --- a/QualityControl/public/object/objectTreeHeader.js +++ b/QualityControl/public/object/objectTreeHeader.js @@ -38,22 +38,9 @@ export default function objectTreeHeader(qcObject, filterModel) { qcObject.objectsRemote.isSuccess() && h('span', `(${howMany})`), ]), - rightCol: h('.w-25.flex-row.items-center.g2.justify-end', [ - filterModel.isRunModeActivated ? null : filterPanelToggleButton(filterModel), - ]), + rightCol: h( + '.w-25.flex-row.items-center.g2.justify-end', + [filterModel.isRunModeActivated ? null : filterPanelToggleButton(filterModel)], + ), }; } - -/** - * Create a menu-item for sort-by dropdown - * @param {QcObject} qcObject - Model that manages the QCObject state. - * @param {string} shortTitle - title that gets displayed to the user - * @param {string} title - title that gets displayed to the user on hover - * @param {Icon} icon - svg icon to be used - * @param {string} field - field by which sorting should happen - * @param {number} order - {-1/1}/{DESC/ASC} - * @returns {vnode} - virtual node element - */ -const sortMenuItem = (qcObject, shortTitle, title, icon, field, order) => h('a.menu-item', { - title: title, style: 'white-space: nowrap;', onclick: () => qcObject.sortTree(shortTitle, field, order, icon), -}, [shortTitle, ' ', icon]); diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 0bd9311c6..4ebc4425f 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -189,26 +189,26 @@ const tableShow = (model) => label: 'Name', sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], onclick: (label, order, icon) => { - model.object.sortTree(label, 'name', order, icon) - } - })) - ]) + model.object.sortTree(label, 'name', order, icon); + }, + })), + ]), ]), h('tbody', [treeRows(model)]), - ]) + ]); const tableHeader = (qcObject) => h('.flex-row.w-100', [ tableSearchInput(qcObject), tableCollapseAll(qcObject), - ]) + ]); const tableCollapseAll = (qcObject) => h('button.btn.m2', { title: 'Close whole tree', onclick: () => qcObject.tree.closeAll(), disabled: Boolean(qcObject.searchInput), - }, iconCollapseUp()) + }, iconCollapseUp()); const tableSearchInput = (qcObject) => h('input.form-control.form-inline.m2.flex-grow', { @@ -218,7 +218,7 @@ const tableSearchInput = (qcObject) => value: qcObject.searchInput, disabled: qcObject.queryingObjects ? true : false, oninput: (e) => qcObject.search(e.target.value), - }) + }); /** * Shows a list of lines of objects diff --git a/QualityControl/public/object/virtualTable.js b/QualityControl/public/object/virtualTable.js index ddb5326f1..c5e472f95 100644 --- a/QualityControl/public/object/virtualTable.js +++ b/QualityControl/public/object/virtualTable.js @@ -104,15 +104,19 @@ const objectFullRow = (model, item, location) => const tableHeader = () => h('table.table.table-sm.text-no-select', { style: 'margin-bottom:0', - }, h('thead', [h('tr', [h('th', sortableTableHead({ - order: model.object.sortBy.order, - icon: model.object.sortBy.icon, - label: 'Name', - sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], - onclick: (label, order, icon) => { - model.object.sortTree(label, 'name', order, icon) - } - }))])])); + }, h('thead', [ + h('tr', [ + h('th', sortableTableHead({ + order: model.object.sortBy.order, + icon: model.object.sortBy.icon, + label: 'Name', + sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], + onclick: (label, order, icon) => { + model.object.sortTree(label, 'name', order, icon); + }, + })), + ]), + ])); /** * Set styles of the floating table and its position inside the big div .tableLogsContentPlaceholder From 5078fd919fa67ecfef06c6ad6f81c90ad3a63565 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 19:58:02 +0100 Subject: [PATCH 07/21] feat: add hover title text to sort button --- QualityControl/public/common/sortButton.js | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/QualityControl/public/common/sortButton.js b/QualityControl/public/common/sortButton.js index 76c24b633..2690bf3bf 100644 --- a/QualityControl/public/common/sortButton.js +++ b/QualityControl/public/common/sortButton.js @@ -62,11 +62,20 @@ export const sortableTableHead = ({ const nextSortOrder = sortOptions[nextIndex]; const hoverIcon = getSortIcon(nextSortOrder); - return h('button.btn.sort-button', { onclick: () => onclick(label, nextSortOrder, hoverIcon) }, [ - label, - h('span.icon-container.mh1', [ - h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]), - h('span.hover-icon', [getSortIcon(nextSortOrder)]), - ]), - ]); + const directionLabel = Object.keys(SortDirectionsEnum).find((key) => SortDirectionsEnum[key] === nextSortOrder); + + return h( + 'button.btn.sort-button', + { + onclick: () => onclick(label, nextSortOrder, hoverIcon), + title: `Sort by ${directionLabel}` + }, + [ + label, + h('span.icon-container.mh1', [ + h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]), + h('span.hover-icon', [getSortIcon(nextSortOrder)]), + ]), + ] + ); }; From 8c9450f4a00196389935cf5a55364cb6d1de456c Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 20:15:21 +0100 Subject: [PATCH 08/21] test: fix object tree tests --- .../test/public/pages/object-tree.test.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 45788051d..4c4931b60 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -55,8 +55,8 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should have a button to sort by (default "Name" ASC)', async () => { - const sortByButtonTitle = await page.evaluate((path) => document.querySelector(path).title, '#sortTreeButton'); - strictEqual(sortByButtonTitle, 'Sort by'); + const sortByButtonTitle = await page.evaluate((path) => document.querySelector(path).title, '.btn.sort-button'); + strictEqual(sortByButtonTitle, 'Sort by DESC'); }); await testParent.test('should have first element in tree as "qc/test/object/1"', async () => { @@ -229,9 +229,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) ); 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)'; - await page.locator(sortingByNameOptionPath).click(); + await page.locator('.btn.sort-button').click(); const sorted = await page.evaluate(() => ({ list: window.model.object.currentList, @@ -244,9 +242,8 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should sort list of histograms by name in ascending order', async () => { - await page.locator('#sortTreeButton').click(); - const sortingByNameOptionPath = '#sortTreeButton > div > a:nth-child(1)'; - await page.locator(sortingByNameOptionPath).click(); + await page.locator('.btn.sort-button').click(); + const sorted = await page.evaluate(() => ({ list: window.model.object.currentList, sort: window.model.object.sortBy, From 9c4bf1ac4be2de728f5cf304773c4ff2efb04505 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 21:03:02 +0100 Subject: [PATCH 09/21] fix: dom node errors when rerendering --- QualityControl/public/object/objectTreePage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 4ebc4425f..132895603 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -56,15 +56,15 @@ export default (model) => { const objectsLoaded = object.list; const objectsToDisplay = objectsLoaded.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); - return [ + return h('', [ tableHeader(model.object), virtualTable(model, 'main', objectsToDisplay), - ]; + ]); } - return [ + return h('', [ tableHeader(model.object), tableShow(model), - ]; + ]); }, Failure: () => null, // Notification is displayed })), From b43ad11550c45c00be9269753134d6b8f5946bca Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 21:03:16 +0100 Subject: [PATCH 10/21] test: fix failing tests due to changes --- QualityControl/test/public/pages/object-tree.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 4c4931b60..62b75bdee 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -34,7 +34,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should have a tree as a table', { timeout }, async () => { - const tableRowPath = 'section > div > div > div > table > tbody > tr'; + const tableRowPath = 'section > div > div > div > div > table > tbody > tr'; await page.waitForSelector(tableRowPath, { timeout: 1000 }); const rowsCount = await page.evaluate( (tableRowPath) => document.querySelectorAll(tableRowPath).length, @@ -44,12 +44,12 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should not preserve state if refreshed not in run mode', { timeout }, async () => { - const tbodyPath = 'section > div > div > div > table > tbody'; + const tbodyPath = 'section > div > div > div > div > table > tbody'; await page.locator(`${tbodyPath} > tr:nth-child(2)`).click(); await page.reload({ waitUntil: 'networkidle0' }); const rowCount = await page.evaluate(() => - document.querySelectorAll('section > div > div > div > table > tbody > tr').length); + document.querySelectorAll('section > div > div > div > div > table > tbody > tr').length); strictEqual(rowCount, 2); }); From ee02932777965ff9351f2ea57f9700085b592367 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 18 Dec 2025 21:07:47 +0100 Subject: [PATCH 11/21] style: fix linting errors --- QualityControl/public/common/sortButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/QualityControl/public/common/sortButton.js b/QualityControl/public/common/sortButton.js index 2690bf3bf..97e3197d4 100644 --- a/QualityControl/public/common/sortButton.js +++ b/QualityControl/public/common/sortButton.js @@ -68,7 +68,7 @@ export const sortableTableHead = ({ 'button.btn.sort-button', { onclick: () => onclick(label, nextSortOrder, hoverIcon), - title: `Sort by ${directionLabel}` + title: `Sort by ${directionLabel}`, }, [ label, @@ -76,6 +76,6 @@ export const sortableTableHead = ({ h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]), h('span.hover-icon', [getSortIcon(nextSortOrder)]), ]), - ] + ], ); }; From dc87639f3f76bdbbfbbb20da1285ca7e1a764931 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Wed, 7 Jan 2026 11:58:52 +0100 Subject: [PATCH 12/21] wip: trying to get the table actions inside the table header --- .../public/object/objectTreePage.js | 37 +++++++++---------- QualityControl/public/object/virtualTable.js | 28 +++++++++----- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 132895603..c4d375a98 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -56,15 +56,9 @@ export default (model) => { const objectsLoaded = object.list; const objectsToDisplay = objectsLoaded.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); - return h('', [ - tableHeader(model.object), - virtualTable(model, 'main', objectsToDisplay), - ]); + return virtualTable(model, 'main', objectsToDisplay); } - return h('', [ - tableHeader(model.object), - tableShow(model), - ]); + return tableShow(model); }, Failure: () => null, // Notification is displayed })), @@ -183,15 +177,18 @@ const tableShow = (model) => h('table.table.table-sm.text-no-select', [ h('thead', [ h('tr', [ - h('th', sortableTableHead({ - order: model.object.sortBy.order, - icon: model.object.sortBy.icon, - label: 'Name', - sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], - onclick: (label, order, icon) => { - model.object.sortTree(label, 'name', order, icon); - }, - })), + h('th', [ + sortableTableHead({ + order: model.object.sortBy.order, + icon: model.object.sortBy.icon, + label: 'Name', + sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], + onclick: (label, order, icon) => { + model.object.sortTree(label, 'name', order, icon); + }, + }), + tableHeader(model.object), + ]), ]), ]), h('tbody', [treeRows(model)]), @@ -203,15 +200,15 @@ const tableHeader = (qcObject) => tableCollapseAll(qcObject), ]); -const tableCollapseAll = (qcObject) => +export const tableCollapseAll = (qcObject) => h('button.btn.m2', { title: 'Close whole tree', onclick: () => qcObject.tree.closeAll(), disabled: Boolean(qcObject.searchInput), }, iconCollapseUp()); -const tableSearchInput = (qcObject) => - h('input.form-control.form-inline.m2.flex-grow', { +export const tableSearchInput = (qcObject) => + h('input.form-control.form-inline.mv2.mh3.flex-grow', { id: 'searchObjectTree', placeholder: 'Search', type: 'text', diff --git a/QualityControl/public/object/virtualTable.js b/QualityControl/public/object/virtualTable.js index c5e472f95..3494922fa 100644 --- a/QualityControl/public/object/virtualTable.js +++ b/QualityControl/public/object/virtualTable.js @@ -14,6 +14,7 @@ import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js'; import { sortableTableHead } from '../common/sortButton.js'; +import { tableCollapseAll, tableSearchInput } from './objectTreePage.js'; import { h, iconBarChart } from '/js/src/index.js'; let ROW_HEIGHT = 33.6; @@ -106,18 +107,27 @@ const tableHeader = () => style: 'margin-bottom:0', }, h('thead', [ h('tr', [ - h('th', sortableTableHead({ - order: model.object.sortBy.order, - icon: model.object.sortBy.icon, - label: 'Name', - sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], - onclick: (label, order, icon) => { - model.object.sortTree(label, 'name', order, icon); - }, - })), + h('th', [ + sortableTableHead({ + order: model.object.sortBy.order, + icon: model.object.sortBy.icon, + label: 'Name', + sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], + onclick: (label, order, icon) => { + model.object.sortTree(label, 'name', order, icon); + }, + }), + tableActions(model.object), + ]), ]), ])); +const tableActions = (qcObject) => + h('.flex-row.w-100', [ + tableSearchInput(qcObject), + tableCollapseAll(qcObject), + ]); + /** * Set styles of the floating table and its position inside the big div .tableLogsContentPlaceholder * @param {Model} model - root model of the application From 4677c41afb96f4de1181f1916d1ba6c7954b094e Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Wed, 7 Jan 2026 12:12:31 +0100 Subject: [PATCH 13/21] feat: add table actions inside a fake table header --- .../public/object/objectTreePage.js | 47 ++++++++++--------- QualityControl/public/object/virtualTable.js | 26 +--------- 2 files changed, 25 insertions(+), 48 deletions(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index c4d375a98..3277eaef5 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -56,9 +56,15 @@ export default (model) => { const objectsLoaded = object.list; const objectsToDisplay = objectsLoaded.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); - return virtualTable(model, 'main', objectsToDisplay); + return h('', [ + tableHeaderRow(model), + virtualTable(model, 'side', objectsToDisplay), + ]); } - return tableShow(model); + return h('', [ + tableHeaderRow(model), + tableShow(model), + ]); }, Failure: () => null, // Notification is displayed })), @@ -174,25 +180,20 @@ const statusBarRight = (model) => model.object.selected * @returns {vnode} - virtual node element */ const tableShow = (model) => - h('table.table.table-sm.text-no-select', [ - h('thead', [ - h('tr', [ - h('th', [ - sortableTableHead({ - order: model.object.sortBy.order, - icon: model.object.sortBy.icon, - label: 'Name', - sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], - onclick: (label, order, icon) => { - model.object.sortTree(label, 'name', order, icon); - }, - }), - tableHeader(model.object), - ]), - ]), - ]), - h('tbody', [treeRows(model)]), - ]); + h('table.table.table-sm.text-no-select', h('tbody', [treeRows(model)])); + +const tableHeaderRow = (model) => h('.bg-gray-light.pv2', [ + sortableTableHead({ + order: model.object.sortBy.order, + icon: model.object.sortBy.icon, + label: 'Name', + sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], + onclick: (label, order, icon) => { + model.object.sortTree(label, 'name', order, icon); + }, + }), + tableHeader(model.object), +]); const tableHeader = (qcObject) => h('.flex-row.w-100', [ @@ -200,14 +201,14 @@ const tableHeader = (qcObject) => tableCollapseAll(qcObject), ]); -export const tableCollapseAll = (qcObject) => +const tableCollapseAll = (qcObject) => h('button.btn.m2', { title: 'Close whole tree', onclick: () => qcObject.tree.closeAll(), disabled: Boolean(qcObject.searchInput), }, iconCollapseUp()); -export const tableSearchInput = (qcObject) => +const tableSearchInput = (qcObject) => h('input.form-control.form-inline.mv2.mh3.flex-grow', { id: 'searchObjectTree', placeholder: 'Search', diff --git a/QualityControl/public/object/virtualTable.js b/QualityControl/public/object/virtualTable.js index 3494922fa..29ebda4d4 100644 --- a/QualityControl/public/object/virtualTable.js +++ b/QualityControl/public/object/virtualTable.js @@ -12,9 +12,6 @@ * or submit itself to any jurisdiction. */ -import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js'; -import { sortableTableHead } from '../common/sortButton.js'; -import { tableCollapseAll, tableSearchInput } from './objectTreePage.js'; import { h, iconBarChart } from '/js/src/index.js'; let ROW_HEIGHT = 33.6; @@ -105,28 +102,7 @@ const objectFullRow = (model, item, location) => const tableHeader = () => h('table.table.table-sm.text-no-select', { style: 'margin-bottom:0', - }, h('thead', [ - h('tr', [ - h('th', [ - sortableTableHead({ - order: model.object.sortBy.order, - icon: model.object.sortBy.icon, - label: 'Name', - sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], - onclick: (label, order, icon) => { - model.object.sortTree(label, 'name', order, icon); - }, - }), - tableActions(model.object), - ]), - ]), - ])); - -const tableActions = (qcObject) => - h('.flex-row.w-100', [ - tableSearchInput(qcObject), - tableCollapseAll(qcObject), - ]); + }, h('thead', [h('tr', [h('th', 'Name')])])); /** * Set styles of the floating table and its position inside the big div .tableLogsContentPlaceholder From 2dc0c404c6e2864915f7a0670daac6b2c113c805 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Wed, 7 Jan 2026 13:29:21 +0100 Subject: [PATCH 14/21] test: fix failing tests due to markup change --- QualityControl/public/object/objectTreePage.js | 1 + .../test/public/features/filterTest.test.js | 7 +++---- QualityControl/test/public/pages/object-tree.test.js | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index f0e6cedde..c886ca863 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -206,6 +206,7 @@ const tableCollapseAll = (qcObject) => title: 'Close whole tree', onclick: () => qcObject.tree.closeAll(), disabled: Boolean(qcObject.searchInput), + id: 'collapse-tree-button' }, iconCollapseUp()); const tableSearchInput = (qcObject) => diff --git a/QualityControl/test/public/features/filterTest.test.js b/QualityControl/test/public/features/filterTest.test.js index 23c34d1df..f783406bf 100644 --- a/QualityControl/test/public/features/filterTest.test.js +++ b/QualityControl/test/public/features/filterTest.test.js @@ -241,16 +241,15 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { ); let rowCount = await page.evaluate(() => document.querySelectorAll('tr').length); - strictEqual(rowCount, 7); + strictEqual(rowCount, 6); const runNumber = '0'; await page.locator('#runNumberFilter').fill(runNumber); await page.locator('#filterElement #triggerFilterButton').click(); - - await extendTree(3, 5); + await delay(100); rowCount = await page.evaluate(() => document.querySelectorAll('tr').length); - strictEqual(rowCount, 5); // Due to the filter there are two objects fewer. + strictEqual(rowCount, 4); // Due to the filter there are two objects fewer. }); await testParent.test('ObjectTree infoPanel should show filtered object versions', { timeout }, async () => { diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 7f57784a6..11596b328 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -44,18 +44,18 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should preserve state if refreshed', { timeout }, async () => { - const selector = 'section > div > div > div > table > tbody > tr:nth-child(2)'; + const selector = 'section > div > div > div > div > table > tbody > tr:nth-child(2)'; await page.locator(selector).click(); await page.reload({ waitUntil: 'networkidle0' }); const rowCountExpanded = await page.evaluate(() => - document.querySelectorAll('section > div > div > div > table > tbody > tr').length); + document.querySelectorAll('section > div > div > div > div > table > tbody > tr').length); 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); + document.querySelectorAll('section > div > div > div > div > table > tbody > tr').length); strictEqual(rowCountExpanded, 3); strictEqual(rowCountCollapsed, 2); @@ -232,7 +232,7 @@ 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 selector = 'section > div > 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}`; @@ -287,7 +287,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) await delay(100); const storedNodes = await getLocalStorageAsJson(page, storageKey); const tableRowCount = await page.evaluate(() => - document.querySelectorAll('section > div > div > div > table > tbody > tr').length); + document.querySelectorAll('section > div > div > div > div > table > tbody > tr').length); deepStrictEqual(storedNodes, {}, 'Stores nodes should be empty'); strictEqual(tableRowCount, 1, 'Tree should be fully collapsed'); @@ -298,7 +298,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) await page.type('#searchObjectTree', 'qc/test/object/1'); const rowsDisplayed = await page.evaluate(() => { const rows = []; - document.querySelectorAll('section > div > div > div > table > tbody > tr') + document.querySelectorAll('section > div > div > div > div > table > tbody > tr') .forEach((item) => rows.push(item.innerText)); return rows; }, { timeout: 5000 }); From 073514e48ddf39d6ecc382273d620cbfde5c24c9 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Wed, 7 Jan 2026 13:32:02 +0100 Subject: [PATCH 15/21] style: fix linting errors --- QualityControl/public/object/objectTreePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index c886ca863..b00127020 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -206,7 +206,7 @@ const tableCollapseAll = (qcObject) => title: 'Close whole tree', onclick: () => qcObject.tree.closeAll(), disabled: Boolean(qcObject.searchInput), - id: 'collapse-tree-button' + id: 'collapse-tree-button', }, iconCollapseUp()); const tableSearchInput = (qcObject) => From b4542d015960d6dab06fa57d5d9be4b1a0efbcd9 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Fri, 9 Jan 2026 14:21:52 +0100 Subject: [PATCH 16/21] fix: virtual table not working anymore --- QualityControl/public/object/objectTreePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index b00127020..ce980d2c3 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -56,7 +56,7 @@ export default (model) => { const objectsLoaded = object.list; const objectsToDisplay = objectsLoaded.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); - return h('', [ + return h('.scroll-y.flex-column.flex-grow', [ tableHeaderRow(model), virtualTable(model, 'side', objectsToDisplay), ]); From baf33e005f4aeca4aea8a1b7bec63c2fd5d8085d Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Tue, 13 Jan 2026 11:44:45 +0100 Subject: [PATCH 17/21] chore: remove unused none sorting option --- QualityControl/public/common/enums/columnSort.enum.js | 1 - 1 file changed, 1 deletion(-) diff --git a/QualityControl/public/common/enums/columnSort.enum.js b/QualityControl/public/common/enums/columnSort.enum.js index ba58e6619..46afb3925 100644 --- a/QualityControl/public/common/enums/columnSort.enum.js +++ b/QualityControl/public/common/enums/columnSort.enum.js @@ -18,7 +18,6 @@ * @readonly */ export const SortDirectionsEnum = Object.freeze({ - NONE: 0, ASC: 1, DESC: -1, }); From 076e2f9783f61a46962b025c630bc7d62ddc2bba Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Tue, 13 Jan 2026 11:46:20 +0100 Subject: [PATCH 18/21] feat: add label name to title on sortable buttons --- QualityControl/public/common/sortButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/common/sortButton.js b/QualityControl/public/common/sortButton.js index 97e3197d4..905af1ecd 100644 --- a/QualityControl/public/common/sortButton.js +++ b/QualityControl/public/common/sortButton.js @@ -68,7 +68,7 @@ export const sortableTableHead = ({ 'button.btn.sort-button', { onclick: () => onclick(label, nextSortOrder, hoverIcon), - title: `Sort by ${directionLabel}`, + title: `Sort ${directionLabel} by ${label}`, }, [ label, From aaef6fcd3ea7f198a69ccb936274022f5ca7143f Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Tue, 13 Jan 2026 11:51:04 +0100 Subject: [PATCH 19/21] feat: add better function names, remove redudant class, simplify function body --- QualityControl/public/app.css | 1 + QualityControl/public/common/sortButton.js | 8 +-- QualityControl/public/object/QCObject.js | 6 +- .../public/object/objectTreePage.js | 70 +++++++++++++------ 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index 755aa8595..05dcac3bd 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -206,6 +206,7 @@ .hover-icon { display: inline-block; + color: var(--color-gray-dark) } } } diff --git a/QualityControl/public/common/sortButton.js b/QualityControl/public/common/sortButton.js index 905af1ecd..f25a73c89 100644 --- a/QualityControl/public/common/sortButton.js +++ b/QualityControl/public/common/sortButton.js @@ -13,7 +13,7 @@ */ import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js'; -import { h, iconCircleX, iconArrowBottom, iconArrowTop } from '/js/src/index.js'; +import { h, iconCircleX, iconCaretBottom, iconCaretTop } from '/js/src/index.js'; /** * Get the icon for the sort direction. @@ -22,10 +22,10 @@ import { h, iconCircleX, iconArrowBottom, iconArrowTop } from '/js/src/index.js' */ const getSortIcon = (direction) => { if (direction === SortDirectionsEnum.ASC) { - return iconArrowTop(); + return iconCaretTop(); } if (direction === SortDirectionsEnum.DESC) { - return iconArrowBottom(); + return iconCaretBottom(); } return iconCircleX(); }; @@ -65,7 +65,7 @@ export const sortableTableHead = ({ const directionLabel = Object.keys(SortDirectionsEnum).find((key) => SortDirectionsEnum[key] === nextSortOrder); return h( - 'button.btn.sort-button', + '.sort-button.cursor-pointer', { onclick: () => onclick(label, nextSortOrder, hoverIcon), title: `Sort ${directionLabel} by ${label}`, diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index 83f69e2a9..72780449e 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { RemoteData, iconArrowTop, BrowserStorage } from '/js/src/index.js'; +import { RemoteData, iconCaretTop, BrowserStorage } from '/js/src/index.js'; import ObjectTree from './ObjectTree.class.js'; import { prettyFormatDate, setBrowserTabTitle } from './../common/utils.js'; import { isObjectOfTypeChecker } from './../library/qcObject/utils.js'; @@ -46,7 +46,7 @@ export default class QCObject extends BaseViewModel { field: 'name', title: 'Name', order: 1, - icon: iconArrowTop(), + icon: iconCaretTop(), }; this.tree = new ObjectTree('database'); @@ -242,7 +242,7 @@ export default class QCObject extends BaseViewModel { field: 'name', title: 'Name', order: 1, - icon: iconArrowTop(), + icon: iconCaretTop(), }; this._computeFilters(); diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 117a69a6e..250a10269 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -60,13 +60,13 @@ export default (model) => { const objectsLoaded = object.list; const objectsToDisplay = objectsLoaded.filter((qcObject) => qcObject.name.toLowerCase().includes(searchInput.toLowerCase())); - return h('.scroll-y.flex-column.flex-grow', [ - tableHeaderRow(model), + return h('.flex-column.flex-grow', [ + actionablesHeaderGroup(model.object), virtualTable(model, 'side', objectsToDisplay), ]); } return h('', [ - tableHeaderRow(model), + actionablesHeaderGroup(model.object), tableShow(model), ]); }, @@ -188,26 +188,51 @@ const statusBarRight = (model) => model.object.selected const tableShow = (model) => h('table.table.table-sm.text-no-select', h('tbody', [treeRows(model)])); -const tableHeaderRow = (model) => h('.bg-gray-light.pv2', [ - sortableTableHead({ - order: model.object.sortBy.order, - icon: model.object.sortBy.icon, - label: 'Name', - sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], - onclick: (label, order, icon) => { - model.object.sortTree(label, 'name', order, icon); - }, - }), - tableHeader(model.object), -]); +/** + * A composite header component for the actionables section. + * It groups the column sorting header and the functional toolbar (search/collapse). + * @param {QCObject} qcObject - The state object for Quality Control actionables. + * @returns {vnode} A virtual DOM node containing the grouped header elements. + */ +const actionablesHeaderGroup = (qcObject) => { + const { + order = SortDirectionsEnum.ASC, + icon = 'sort' + } = qcObject.sortBy || {}; + + return h('.bg-gray-light.pv2', [ + sortableTableHead({ + order, + icon, + label: 'Name', + sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC], + onclick: (label, order, icon) => { + qcObject.sortTree(label, 'name', order, icon); + }, + }), + actionablesContainer(qcObject), + ]); +}; -const tableHeader = (qcObject) => +/** + * A toolbar containing interactive controls for the object tree table, + * specifically the search input and the 'Collapse All' button. + * @param {QCObject} qcObject - The state object for managing tree interactions. + * @returns {vnode} A flex-row container with search and collapse actions. + */ +const actionablesContainer = (qcObject) => h('.flex-row.w-100', [ - tableSearchInput(qcObject), - tableCollapseAll(qcObject), + actionableSearchInput(qcObject), + actionableCollapseAll(qcObject), ]); -const tableCollapseAll = (qcObject) => +/** + * A button to collapse all expanded nodes in the object tree table. + * Disabled when a search filter is active to prevent UI inconsistency. + * @param {QCObject} qcObject - The state object containing the tree controller. + * @returns {vnode} A button element with a collapse icon. + */ +const actionableCollapseAll = (qcObject) => h('button.btn.m2', { title: 'Close whole tree', onclick: () => qcObject.tree.closeAll(), @@ -215,7 +240,12 @@ const tableCollapseAll = (qcObject) => id: 'collapse-tree-button', }, iconCollapseUp()); -const tableSearchInput = (qcObject) => +/** + * A text input for filtering the object tree table based on user queries. + * @param {QCObject} qcObject - The state object managing search input and loading state. + * @returns {vnode} An input element for searching. + */ +const actionableSearchInput = (qcObject) => h('input.form-control.form-inline.mv2.mh3.flex-grow', { id: 'searchObjectTree', placeholder: 'Search', From e1b751505680d81406d62277806f8d866af10511 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Tue, 13 Jan 2026 11:52:44 +0100 Subject: [PATCH 20/21] style: fix linting errors --- QualityControl/public/object/objectTreePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 250a10269..7a9b19716 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -197,7 +197,7 @@ const tableShow = (model) => const actionablesHeaderGroup = (qcObject) => { const { order = SortDirectionsEnum.ASC, - icon = 'sort' + icon = 'sort', } = qcObject.sortBy || {}; return h('.bg-gray-light.pv2', [ From 34858486bba3e866aad4ec235718121d5b528f71 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Tue, 13 Jan 2026 12:17:17 +0100 Subject: [PATCH 21/21] test: update the selector to select the right element --- QualityControl/test/public/pages/object-tree.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index b93a9affa..e4382d645 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -62,8 +62,8 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should have a button to sort by (default "Name" ASC)', async () => { - const sortByButtonTitle = await page.evaluate((path) => document.querySelector(path).title, '.btn.sort-button'); - strictEqual(sortByButtonTitle, 'Sort by DESC'); + const sortByButtonTitle = await page.evaluate((path) => document.querySelector(path).title, '.sort-button'); + strictEqual(sortByButtonTitle, 'Sort DESC by Name'); }); await testParent.test('should have first element in tree as "qc/test/object/1"', async () => { @@ -260,7 +260,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should sort list of histograms by name in descending order', async () => { - await page.locator('.btn.sort-button').click(); + await page.locator('.sort-button').click(); const sorted = await page.evaluate(() => ({ list: window.model.object.currentList, @@ -273,7 +273,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should sort list of histograms by name in ascending order', async () => { - await page.locator('.btn.sort-button').click(); + await page.locator('.sort-button').click(); const sorted = await page.evaluate(() => ({ list: window.model.object.currentList,