Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
73e2d60
feat: add search bar to the table header of the object tree page
AlexJanson Dec 18, 2025
c4b9b58
feat: add the collapse all button to the table header
AlexJanson Dec 18, 2025
1b8eeab
feat: add reusable component for adding table sort buttons
AlexJanson Dec 18, 2025
4273674
feat: add more margin on the sort direction icon
AlexJanson Dec 18, 2025
fc4452f
feat: add sortable column to virtual table
AlexJanson Dec 18, 2025
4b06d78
style: fix linting errors
AlexJanson Dec 18, 2025
5078fd9
feat: add hover title text to sort button
AlexJanson Dec 18, 2025
8c9450f
test: fix object tree tests
AlexJanson Dec 18, 2025
9c4bf1a
fix: dom node errors when rerendering
AlexJanson Dec 18, 2025
b43ad11
test: fix failing tests due to changes
AlexJanson Dec 18, 2025
ee02932
style: fix linting errors
AlexJanson Dec 18, 2025
dc87639
wip: trying to get the table actions inside the table header
AlexJanson Jan 7, 2026
4677c41
feat: add table actions inside a fake table header
AlexJanson Jan 7, 2026
cbcb41e
Merge remote-tracking branch 'origin/dev' into improv/QCG/OGUI-1853/m…
AlexJanson Jan 7, 2026
2dc0c40
test: fix failing tests due to markup change
AlexJanson Jan 7, 2026
073514e
style: fix linting errors
AlexJanson Jan 7, 2026
b4542d0
fix: virtual table not working anymore
AlexJanson Jan 9, 2026
1c64d62
Merge remote-tracking branch 'origin/dev' into improv/QCG/OGUI-1853/m…
AlexJanson Jan 9, 2026
baf33e0
chore: remove unused none sorting option
AlexJanson Jan 13, 2026
076e2f9
feat: add label name to title on sortable buttons
AlexJanson Jan 13, 2026
aaef6fc
feat: add better function names, remove redudant class, simplify func…
AlexJanson Jan 13, 2026
e1b7515
style: fix linting errors
AlexJanson Jan 13, 2026
196a51e
Merge remote-tracking branch 'origin/dev' into improv/QCG/OGUI-1853/m…
AlexJanson Jan 13, 2026
3485848
test: update the selector to select the right element
AlexJanson Jan 13, 2026
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
18 changes: 18 additions & 0 deletions QualityControl/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,24 @@
white-space: nowrap;
}

.sort-button {
.hover-icon {
display: none;
opacity: 0.6;
}

&:hover {
.current-icon {
display: none;
}

.hover-icon {
display: inline-block;
color: var(--color-gray-dark)
}
}
}

.drop-zone {
position: absolute;
height: 100%;
Expand Down
23 changes: 23 additions & 0 deletions QualityControl/public/common/enums/columnSort.enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @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({
ASC: 1,
DESC: -1,
});
81 changes: 81 additions & 0 deletions QualityControl/public/common/sortButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @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, iconCaretBottom, iconCaretTop } 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 iconCaretTop();
}
if (direction === SortDirectionsEnum.DESC) {
return iconCaretBottom();
}
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<number>} [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);

const directionLabel = Object.keys(SortDirectionsEnum).find((key) => SortDirectionsEnum[key] === nextSortOrder);

return h(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to keep the same UX/UI for the users as for other applications that we develop.
Please have a look at Bookkeeping on what design they use and try to implement a similar one: attaching here a screenshot but perhaps running BKP locally would be more helpful

Image

'.sort-button.cursor-pointer',
{
onclick: () => onclick(label, nextSortOrder, hoverIcon),
title: `Sort ${directionLabel} by ${label}`,
},
[
label,
h('span.icon-container.mh1', [
h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]),
h('span.hover-icon', [getSortIcon(nextSortOrder)]),
]),
],
);
};
19 changes: 4 additions & 15 deletions QualityControl/public/object/QCObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -46,8 +46,7 @@ export default class QCObject extends BaseViewModel {
field: 'name',
title: 'Name',
order: 1,
icon: iconArrowTop(),
open: false,
icon: iconCaretTop(),
};

this.tree = new ObjectTree('database');
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -252,8 +242,7 @@ export default class QCObject extends BaseViewModel {
field: 'name',
title: 'Name',
order: 1,
icon: iconArrowTop(),
open: false,
icon: iconCaretTop(),
};
this._computeFilters();

Expand Down
53 changes: 4 additions & 49 deletions QualityControl/public/object/objectTreeHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
*/

import { h } from '/js/src/index.js';
import { iconCollapseUp, iconArrowBottom, iconArrowTop } from '/js/src/icons.js';
import { filterPanelToggleButton } from '../common/filters/filterViews.js';

/**
Expand All @@ -39,53 +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),
' ',
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),

]),
]),
' ',
h('button.btn', {
title: 'Close whole tree',
id: 'collapse-tree-button',
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),
}),
' ',
]),
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]);
92 changes: 86 additions & 6 deletions QualityControl/public/object/objectTreePage.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the newly added methods in this file are missing documentation. Please make sure to add

Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@
* 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';
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';
import { downloadRootImageButton } from '../common/downloadRootImageButton.js';

/**
Expand Down Expand Up @@ -50,9 +60,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('.flex-column.flex-grow', [
actionablesHeaderGroup(model.object),
virtualTable(model, 'side', objectsToDisplay),
]);
}
return tableShow(model);
return h('', [
actionablesHeaderGroup(model.object),
tableShow(model),
]);
},
Failure: () => null, // Notification is displayed
})),
Expand Down Expand Up @@ -170,11 +186,75 @@ 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', 'Name')])]),
h('tbody', [treeRows(model)]),
h('table.table.table-sm.text-no-select', h('tbody', [treeRows(model)]));

/**
* 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),
]);
};

/**
* 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', [
actionableSearchInput(qcObject),
actionableCollapseAll(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(),
disabled: Boolean(qcObject.searchInput),
id: 'collapse-tree-button',
}, iconCollapseUp());

/**
* 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',
type: 'text',
value: qcObject.searchInput,
disabled: qcObject.queryingObjects ? true : false,
oninput: (e) => qcObject.search(e.target.value),
});

/**
* Shows a list of lines <tr> of objects
* @param {Model} model - root model of the application
Expand Down
7 changes: 3 additions & 4 deletions QualityControl/test/public/features/filterTest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading
Loading