Skip to content

Commit 7501cfa

Browse files
authored
[OGUI-1853] Move actionables of objectTree in the table header (#3247)
As a user, it is difficult to see that object tree can be filtered by name. Thus, the search input text (from top right in header) should be moved in the table header in a nice UI and UX manner. Moreover, the (collapse all tree button) should be moved also in the table header(right most). Moreover, the sort by name buton should also become part of the header so that when the user clicks on the header Name column, to display a sort icon up, or down or nothing depending on what is currently being sorted. See for example how Bookkeeping sorts by Title on page "Log Entries" (log-overview)
1 parent 709e06e commit 7501cfa

File tree

8 files changed

+231
-89
lines changed

8 files changed

+231
-89
lines changed

QualityControl/public/app.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,24 @@
193193
white-space: nowrap;
194194
}
195195

196+
.sort-button {
197+
.hover-icon {
198+
display: none;
199+
opacity: 0.6;
200+
}
201+
202+
&:hover {
203+
.current-icon {
204+
display: none;
205+
}
206+
207+
.hover-icon {
208+
display: inline-block;
209+
color: var(--color-gray-dark)
210+
}
211+
}
212+
}
213+
196214
.drop-zone {
197215
position: absolute;
198216
height: 100%;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
/**
16+
* Enumeration for sort directions
17+
* @enum {number}
18+
* @readonly
19+
*/
20+
export const SortDirectionsEnum = Object.freeze({
21+
ASC: 1,
22+
DESC: -1,
23+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js';
16+
import { h, iconCircleX, iconCaretBottom, iconCaretTop } from '/js/src/index.js';
17+
18+
/**
19+
* Get the icon for the sort direction.
20+
* @param {SortDirectionsEnum} direction - direction of the sort.
21+
* @returns {vnode} the correct icon related to the direction.
22+
*/
23+
const getSortIcon = (direction) => {
24+
if (direction === SortDirectionsEnum.ASC) {
25+
return iconCaretTop();
26+
}
27+
if (direction === SortDirectionsEnum.DESC) {
28+
return iconCaretBottom();
29+
}
30+
return iconCircleX();
31+
};
32+
33+
/**
34+
* @callback SortClickCallback
35+
* @param {string} label - The label of the column being sorted.
36+
* @param {number} order - The next sort direction in the cycle.
37+
* @param {vnode} icon - The VNode for the icon representing the next sort state.
38+
* @returns {void}
39+
*/
40+
41+
/**
42+
* Renders a sortable table header button that cycles through sort states.
43+
* Displays the current sort icon and a preview icon of the next state on hover.
44+
* @param {object} props - The component properties.
45+
* @param {number} props.order - The current sort direction value from SortDirectionsEnum.
46+
* @param {object|undefined} props.icon - The VNode/element for the current active sort icon.
47+
* @param {string} props.label - The display text for the column header.
48+
* @param {SortClickCallback} props.onclick - Callback triggered on click.
49+
* @param {Array<number>} [props.sortOptions] - Array of SortDirectionsEnum values defining the
50+
* order of the sort cycle. Defaults to all enum values.
51+
* @returns {object} A HyperScript VNode representing the sortable button.
52+
*/
53+
export const sortableTableHead = ({
54+
order,
55+
icon,
56+
label,
57+
onclick,
58+
sortOptions = [...Object.values(SortDirectionsEnum)],
59+
}) => {
60+
const currentIndex = sortOptions.indexOf(order);
61+
const nextIndex = (currentIndex + 1) % sortOptions.length;
62+
const nextSortOrder = sortOptions[nextIndex];
63+
const hoverIcon = getSortIcon(nextSortOrder);
64+
65+
const directionLabel = Object.keys(SortDirectionsEnum).find((key) => SortDirectionsEnum[key] === nextSortOrder);
66+
67+
return h(
68+
'.sort-button.cursor-pointer',
69+
{
70+
onclick: () => onclick(label, nextSortOrder, hoverIcon),
71+
title: `Sort ${directionLabel} by ${label}`,
72+
},
73+
[
74+
label,
75+
h('span.icon-container.mh1', [
76+
h('span.current-icon', [order != SortDirectionsEnum.NONE ? icon : undefined]),
77+
h('span.hover-icon', [getSortIcon(nextSortOrder)]),
78+
]),
79+
],
80+
);
81+
};

QualityControl/public/object/QCObject.js

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* or submit itself to any jurisdiction.
1313
*/
1414

15-
import { RemoteData, iconArrowTop, BrowserStorage } from '/js/src/index.js';
15+
import { RemoteData, iconCaretTop, BrowserStorage } from '/js/src/index.js';
1616
import ObjectTree from './ObjectTree.class.js';
1717
import { prettyFormatDate, setBrowserTabTitle } from './../common/utils.js';
1818
import { isObjectOfTypeChecker } from './../library/qcObject/utils.js';
@@ -46,8 +46,7 @@ export default class QCObject extends BaseViewModel {
4646
field: 'name',
4747
title: 'Name',
4848
order: 1,
49-
icon: iconArrowTop(),
50-
open: false,
49+
icon: iconCaretTop(),
5150
};
5251

5352
this.tree = new ObjectTree('database');
@@ -115,15 +114,6 @@ export default class QCObject extends BaseViewModel {
115114
this.notify();
116115
}
117116

118-
/**
119-
* Toggle the display of the sort by dropdown
120-
* @returns {undefined}
121-
*/
122-
toggleSortDropdown() {
123-
this.sortBy.open = !this.sortBy.open;
124-
this.notify();
125-
}
126-
127117
/**
128118
* Computes the final list of objects to be seen by user depending on search input from user
129119
* If any of those changes, this method should be called to update the outputs.
@@ -189,7 +179,7 @@ export default class QCObject extends BaseViewModel {
189179

190180
this._computeFilters();
191181

192-
this.sortBy = { field, title, order, icon, open: false };
182+
this.sortBy = { field, title, order, icon };
193183
this.notify();
194184
}
195185

@@ -252,8 +242,7 @@ export default class QCObject extends BaseViewModel {
252242
field: 'name',
253243
title: 'Name',
254244
order: 1,
255-
icon: iconArrowTop(),
256-
open: false,
245+
icon: iconCaretTop(),
257246
};
258247
this._computeFilters();
259248

QualityControl/public/object/objectTreeHeader.js

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
*/
1414

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

1918
/**
@@ -39,53 +38,9 @@ export default function objectTreeHeader(qcObject, filterModel) {
3938
qcObject.objectsRemote.isSuccess() && h('span', `(${howMany})`),
4039
]),
4140

42-
rightCol: h('.w-25.flex-row.items-center.g2.justify-end', [
43-
filterModel.isRunModeActivated ? null : filterPanelToggleButton(filterModel),
44-
' ',
45-
h('.dropdown', {
46-
id: 'sortTreeButton', title: 'Sort by', class: qcObject.sortBy.open ? 'dropdown-open' : '',
47-
}, [
48-
h('button.btn', {
49-
title: 'Sort by',
50-
onclick: () => qcObject.toggleSortDropdown(),
51-
}, [qcObject.sortBy.title, ' ', qcObject.sortBy.icon]),
52-
h('.dropdown-menu.text-left', [
53-
sortMenuItem(qcObject, 'Name', 'Sort by name ASC', iconArrowTop(), 'name', 1),
54-
sortMenuItem(qcObject, 'Name', 'Sort by name DESC', iconArrowBottom(), 'name', -1),
55-
56-
]),
57-
]),
58-
' ',
59-
h('button.btn', {
60-
title: 'Close whole tree',
61-
id: 'collapse-tree-button',
62-
onclick: () => qcObject.tree.closeAll(),
63-
disabled: Boolean(qcObject.searchInput),
64-
}, iconCollapseUp()),
65-
' ',
66-
h('input.form-control.form-inline.mh1.w-33', {
67-
id: 'searchObjectTree',
68-
placeholder: 'Search',
69-
type: 'text',
70-
value: qcObject.searchInput,
71-
disabled: qcObject.queryingObjects ? true : false,
72-
oninput: (e) => qcObject.search(e.target.value),
73-
}),
74-
' ',
75-
]),
41+
rightCol: h(
42+
'.w-25.flex-row.items-center.g2.justify-end',
43+
[filterModel.isRunModeActivated ? null : filterPanelToggleButton(filterModel)],
44+
),
7645
};
7746
}
78-
79-
/**
80-
* Create a menu-item for sort-by dropdown
81-
* @param {QcObject} qcObject - Model that manages the QCObject state.
82-
* @param {string} shortTitle - title that gets displayed to the user
83-
* @param {string} title - title that gets displayed to the user on hover
84-
* @param {Icon} icon - svg icon to be used
85-
* @param {string} field - field by which sorting should happen
86-
* @param {number} order - {-1/1}/{DESC/ASC}
87-
* @returns {vnode} - virtual node element
88-
*/
89-
const sortMenuItem = (qcObject, shortTitle, title, icon, field, order) => h('a.menu-item', {
90-
title: title, style: 'white-space: nowrap;', onclick: () => qcObject.sortTree(shortTitle, field, order, icon),
91-
}, [shortTitle, ' ', icon]);

QualityControl/public/object/objectTreePage.js

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@
1212
* or submit itself to any jurisdiction.
1313
*/
1414

15-
import { h, iconBarChart, iconCaretRight, iconResizeBoth, iconCaretBottom, iconCircleX } from '/js/src/index.js';
15+
import {
16+
h,
17+
iconCollapseUp,
18+
iconBarChart,
19+
iconCaretRight,
20+
iconResizeBoth,
21+
iconCaretBottom,
22+
iconCircleX,
23+
} from '/js/src/index.js';
1624
import { spinner } from '../common/spinner.js';
1725
import { draw } from '../common/object/draw.js';
1826
import timestampSelectForm from './../common/timestampSelectForm.js';
1927
import virtualTable from './virtualTable.js';
2028
import { defaultRowAttributes, qcObjectInfoPanel } from '../common/object/objectInfoCard.js';
2129
import { downloadButton } from '../common/downloadButton.js';
2230
import { resizableDivider } from '../common/resizableDivider.js';
31+
import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js';
32+
import { sortableTableHead } from '../common/sortButton.js';
2333
import { downloadRootImageButton } from '../common/downloadRootImageButton.js';
2434

2535
/**
@@ -50,9 +60,15 @@ export default (model) => {
5060
const objectsLoaded = object.list;
5161
const objectsToDisplay = objectsLoaded.filter((qcObject) =>
5262
qcObject.name.toLowerCase().includes(searchInput.toLowerCase()));
53-
return virtualTable(model, 'main', objectsToDisplay);
63+
return h('.flex-column.flex-grow', [
64+
actionablesHeaderGroup(model.object),
65+
virtualTable(model, 'side', objectsToDisplay),
66+
]);
5467
}
55-
return tableShow(model);
68+
return h('', [
69+
actionablesHeaderGroup(model.object),
70+
tableShow(model),
71+
]);
5672
},
5773
Failure: () => null, // Notification is displayed
5874
})),
@@ -170,11 +186,75 @@ const statusBarRight = (model) => model.object.selected
170186
* @returns {vnode} - virtual node element
171187
*/
172188
const tableShow = (model) =>
173-
h('table.table.table-sm.text-no-select', [
174-
h('thead', [h('tr', [h('th', 'Name')])]),
175-
h('tbody', [treeRows(model)]),
189+
h('table.table.table-sm.text-no-select', h('tbody', [treeRows(model)]));
190+
191+
/**
192+
* A composite header component for the actionables section.
193+
* It groups the column sorting header and the functional toolbar (search/collapse).
194+
* @param {QCObject} qcObject - The state object for Quality Control actionables.
195+
* @returns {vnode} A virtual DOM node containing the grouped header elements.
196+
*/
197+
const actionablesHeaderGroup = (qcObject) => {
198+
const {
199+
order = SortDirectionsEnum.ASC,
200+
icon = 'sort',
201+
} = qcObject.sortBy || {};
202+
203+
return h('.bg-gray-light.pv2', [
204+
sortableTableHead({
205+
order,
206+
icon,
207+
label: 'Name',
208+
sortOptions: [SortDirectionsEnum.ASC, SortDirectionsEnum.DESC],
209+
onclick: (label, order, icon) => {
210+
qcObject.sortTree(label, 'name', order, icon);
211+
},
212+
}),
213+
actionablesContainer(qcObject),
214+
]);
215+
};
216+
217+
/**
218+
* A toolbar containing interactive controls for the object tree table,
219+
* specifically the search input and the 'Collapse All' button.
220+
* @param {QCObject} qcObject - The state object for managing tree interactions.
221+
* @returns {vnode} A flex-row container with search and collapse actions.
222+
*/
223+
const actionablesContainer = (qcObject) =>
224+
h('.flex-row.w-100', [
225+
actionableSearchInput(qcObject),
226+
actionableCollapseAll(qcObject),
176227
]);
177228

229+
/**
230+
* A button to collapse all expanded nodes in the object tree table.
231+
* Disabled when a search filter is active to prevent UI inconsistency.
232+
* @param {QCObject} qcObject - The state object containing the tree controller.
233+
* @returns {vnode} A button element with a collapse icon.
234+
*/
235+
const actionableCollapseAll = (qcObject) =>
236+
h('button.btn.m2', {
237+
title: 'Close whole tree',
238+
onclick: () => qcObject.tree.closeAll(),
239+
disabled: Boolean(qcObject.searchInput),
240+
id: 'collapse-tree-button',
241+
}, iconCollapseUp());
242+
243+
/**
244+
* A text input for filtering the object tree table based on user queries.
245+
* @param {QCObject} qcObject - The state object managing search input and loading state.
246+
* @returns {vnode} An input element for searching.
247+
*/
248+
const actionableSearchInput = (qcObject) =>
249+
h('input.form-control.form-inline.mv2.mh3.flex-grow', {
250+
id: 'searchObjectTree',
251+
placeholder: 'Search',
252+
type: 'text',
253+
value: qcObject.searchInput,
254+
disabled: qcObject.queryingObjects ? true : false,
255+
oninput: (e) => qcObject.search(e.target.value),
256+
});
257+
178258
/**
179259
* Shows a list of lines <tr> of objects
180260
* @param {Model} model - root model of the application

QualityControl/test/public/features/filterTest.test.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,16 +241,15 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => {
241241
);
242242

243243
let rowCount = await page.evaluate(() => document.querySelectorAll('tr').length);
244-
strictEqual(rowCount, 7);
244+
strictEqual(rowCount, 6);
245245

246246
const runNumber = '0';
247247
await page.locator('#runNumberFilter').fill(runNumber);
248248
await page.locator('#filterElement #triggerFilterButton').click();
249-
250-
await extendTree(3, 5);
249+
await delay(100);
251250

252251
rowCount = await page.evaluate(() => document.querySelectorAll('tr').length);
253-
strictEqual(rowCount, 5); // Due to the filter there are two objects fewer.
252+
strictEqual(rowCount, 4); // Due to the filter there are two objects fewer.
254253
});
255254

256255
await testParent.test('ObjectTree infoPanel should show filtered object versions', { timeout }, async () => {

0 commit comments

Comments
 (0)