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
91e6937
feat: add dropzones and event listeners for the tabs in edit
AlexJanson Dec 16, 2025
f86ae85
feat: add ability to reorder the tabs using drag and drop
AlexJanson Dec 16, 2025
ec3396f
feat: add class styling for drop target
AlexJanson Dec 16, 2025
9b93d7a
feat: add the primary color for the drop borders
AlexJanson Dec 16, 2025
cbeeb8e
style: cleanup inline style
AlexJanson Dec 16, 2025
4eb8259
docs: add documentation to the functions
AlexJanson Dec 16, 2025
8688ac1
style: fix linting errors
AlexJanson Dec 16, 2025
b9be99c
fix: pointer events blocking clicking on the tab buttons
AlexJanson Dec 16, 2025
268fc28
feat: add grab cursor when hovering tabs in edit mode
AlexJanson Dec 17, 2025
6650f8d
test: add test for drag and drop reordering of tabs in edit mode
AlexJanson Dec 17, 2025
0e93f2a
feat: add title to incite the user to re-arrange the tabs
AlexJanson Dec 17, 2025
bb9bece
fix: compare original tab names for consistancy of the test
AlexJanson Dec 17, 2025
4f11556
test: add delays for drag and drop to combat slow cicd
AlexJanson Dec 17, 2025
22aafae
test: add console log to see where the cicd gets hang up on
AlexJanson Dec 17, 2025
62ee82f
test: update the target selector to select the tab button
AlexJanson Dec 17, 2025
95fc7db
feat: add pointer-events none while dragging on edit and delete buttons
AlexJanson Dec 17, 2025
f3588a9
test: update the target zone selector to select the dropdown
AlexJanson Dec 17, 2025
e6b354c
fix: revert the changes to the delete and edit button
AlexJanson Dec 17, 2025
c6753b1
style: remove unused variable
AlexJanson Dec 17, 2025
d423a16
test: remove source selector to btn-tab
AlexJanson Dec 17, 2025
187e4f0
test: add screenshot and points of where the target and source are lo…
AlexJanson Dec 17, 2025
6216434
test: add pointer events none when dragging
AlexJanson Dec 17, 2025
a501ad2
test: remove the redundant screenshot command
AlexJanson Dec 18, 2025
bda861d
test: add delay instead of the screenshot command
AlexJanson Dec 18, 2025
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
36 changes: 33 additions & 3 deletions QualityControl/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@
}
}

.cursor-pointer {
cursor: pointer;
}
.cursor-pointer { cursor: pointer; }
.cursor-grab { cursor: grab; }
.cursor-inherit { cursor: inherit; }

.header-layout {
&.edit {
Expand Down Expand Up @@ -187,3 +187,33 @@
.whitespace-nowrap {
white-space: nowrap;
}

.drop-zone {
position: absolute;
height: 100%;
width: 50%;
pointer-events: none;

&.before {
left: 0;

&.active {
border-left: 2px solid var(--color-primary);
}
}

&.after {
right: 0;

&.active {
border-right: 2px solid var(--color-primary);
}
}
}

.pointer-events-auto {
pointer-events: auto;
}
.pointer-events-none {
pointer-events: none;
}
82 changes: 82 additions & 0 deletions QualityControl/public/layout/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export default class Layout extends BaseViewModel {
});
this.cellHeight = 100 / this.gridListSize * 0.95; // %, put some margin at bottom to see below
this.cellWidth = 100 / this.gridListSize; // %

this.isDragging = false;
this.dropTargetId = undefined;
this.position = undefined;
}

/**
Expand Down Expand Up @@ -777,4 +781,82 @@ export default class Layout extends BaseViewModel {
ownsLayout(layoutOwnerId) {
return this.model.session.personid == layoutOwnerId;
}

/**
* Sets the current drop target for a drag-and-drop operation.
* This is typically used to render a visual indicator (like a blue line)
* in the UI showing where the dragged tab will be placed.
* @param {string|number} tabId - The ID of the tab currently being hovered over.
* @param {'before'|'after'} position - The side of the target tab where the drop indicator should appear.
*/
setDropTarget(tabId, position) {
this.dropTargetId = tabId;
this.position = position;

this.notify();
}

/**
* Clears the current drop target state, usually when the drag operation
* is finished or the dragged item is no longer over a valid drop zone.
* This action typically causes the visual drop indicator to be hidden.
*/
clearDropTarget() {
this.dropTargetId = undefined;
this.position = undefined;

this.notify();
}

/**
* Reorders the tabs in the internal array based on the drag source and drop target.
* This function calculates the correct index for insertion, accounting for the
* tab being removed from its original position.
* @param {string|number} sourceId - The ID of the tab that was dragged.
* @param {string|number} targetId - The ID of the tab that the source was dropped onto.
* @param {'before'|'after'} position - The placement relative to the target tab.
*/
reorderTabs(sourceId, targetId, position) {
const sourceIndex = this.item.tabs.findIndex((t) => t.id === sourceId);
let targetIndex = this.item.tabs.findIndex((t) => t.id === targetId);

if (sourceIndex === -1 || targetIndex === -1) {
return;
}

if (position === 'after') {
targetIndex += 1;
}

const [movedTab] = this.item.tabs.splice(sourceIndex, 1);

if (sourceIndex < targetIndex) {
targetIndex--;
}

this.item.tabs.splice(targetIndex, 0, movedTab);

this.notify();
}

/**
* Sets the layout state to indicate that a tab drag-and-drop operation has begun.
* It typically triggers a redraw and enables pointer events on all drop zones via CSS.
*/
startDragging() {
this.isDragging = true;

this.notify();
}

/**
* Resets the layout state to indicate that a tab drag-and-drop operation has ended,
* regardless of whether the drop was successful or cancelled.
* It typically triggers a redraw and disables pointer events on the drop zones via CSS.
*/
stopDragging() {
this.isDragging = false;

this.notify();
}
}
69 changes: 62 additions & 7 deletions QualityControl/public/layout/view/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,70 @@ const toolbarEditModeTab = (layout, tab, i) => {
*/
const selectTab = () => layout.selectTab(i);

const dragActiveClass = layout.isDragging ? 'pointer-events-auto' : '';
const disableButtonsOnDragClass = layout.isDragging ? 'pointer-events-none' : '';
const dropZoneClass = (position) => layout.dropTargetId === tab.id && layout.position === position ? 'active' : '';

return [
h('.btn-group.flex-fixed', [
h('button.br-pill.ph2.btn.btn-tab.whitespace-nowrap', { class: linkClass, onclick: selectTab }, tab.name),
selected && [
editTabButton(layout, linkClass, tab, i),
resizeGridTabDropDown(layout, tab),
deleteTabButton(layout, linkClass, i),
h(
'.btn-group.flex-fixed.relative.cursor-grab',
{
title: 'Drag the tab to re-arrange them',
draggable: true,
ondragstart: (e) => {
e.dataTransfer.setData('text/plain', tab.id);
layout.startDragging();
},
ondrop: (e) => {
layout.reorderTabs(e.dataTransfer.getData('text/plain'), layout.dropTargetId, layout.position);
layout.clearDropTarget();
layout.stopDragging();
},
ondragend: () => layout.stopDragging(),
},
[
h(
'button.br-pill.ph2.btn.btn-tab.whitespace-nowrap',
{ id: 'btn-tab', class: `${linkClass} cursor-inherit`, onclick: selectTab },
tab.name,
),
[
h(
'.drop-zone.before',
{
class: `${dragActiveClass} ${dropZoneClass('before')}`,
ondragenter: () => layout.setDropTarget(tab.id, 'before'),
ondragover: (e) => e.preventDefault(), // prevent default to allow drop
ondragleave: () => {
if (layout.dropTargetId === tab.id && layout.position === 'before') {
layout.clearDropTarget();
}
},
},
'',
),
h(
'.drop-zone.after',
{
class: `${dragActiveClass} ${dropZoneClass('after')}`,
ondragenter: () => layout.setDropTarget(tab.id, 'after'),
ondragover: (e) => e.preventDefault(), // prevent default to allow drop
ondragleave: () => {
if (layout.dropTargetId === tab.id && layout.position === 'after') {
layout.clearDropTarget();
}
},
},
'',
),
selected && [
editTabButton(layout, `${disableButtonsOnDragClass} ${linkClass}`, tab, i),
resizeGridTabDropDown(layout, tab),
deleteTabButton(layout, `${disableButtonsOnDragClass} ${linkClass}`, i),
],
].flat().filter(Boolean),
],
]),
),
' ',
];
};
Expand Down
34 changes: 34 additions & 0 deletions QualityControl/test/public/pages/layout-show.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { strictEqual, ok, deepStrictEqual } from 'node:assert';
import { delay } from '../../testUtils/delay.js';
import { editedMockedLayout } from '../../setup/seeders/layout-show/json-file-mock.js';
import { getElementCenter } from '../../testUtils/dragAndDrop.js';

/**
* Performs a series of automated tests on the layoutShow page using Puppeteer.
Expand Down Expand Up @@ -297,6 +298,39 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) =>
},
);

await testParent.test(
'should reorder tabs via drag and drop in edit mode',
{ timeout },
async () => {
const originalTabNames = await page.$$eval('#btn-tab', (elements) =>
elements.map((element) => element.textContent.trim()));

const sourceTabSelector = '.btn-group.flex-fixed.relative:nth-child(1)';
const targetZoneSelector = '.btn-group.flex-fixed.relative:nth-child(2) .drop-zone.after';

const sourceCenter = await getElementCenter(page, sourceTabSelector);
const targetCenter = await getElementCenter(page, targetZoneSelector);

await page.mouse.move(sourceCenter.x, sourceCenter.y);
await page.mouse.down();

// We add 'steps' to make the move smoother, which helps trigger event
await page.mouse.move(targetCenter.x, targetCenter.y, { steps: 10 });

await delay(1000);

// Wait a moment for the 'active' class to appear in the UI
await page.waitForSelector('.drop-zone.after.active');

await page.mouse.up();

const tabNames = await page.$$eval('#btn-tab', (elements) =>
elements.map((element) => element.textContent.trim()));

strictEqual(tabNames[1], originalTabNames[0]);
}
);

await testParent.test(
'should show normal sidebar after Cancel click',
{ timeout },
Expand Down
28 changes: 28 additions & 0 deletions QualityControl/test/testUtils/dragAndDrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @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.
*/

/**
* Helper to get the center of an element.
* @param {object} page - Puppeteer page object.
* @param {string} selector - Element selector to look for.
* @returns {Promise<{x: number, y: number}>} A promise that resolves to the center x & y coordinates.
*/
export const getElementCenter = async (page, selector) => {
const element = await page.waitForSelector(selector);
const box = await element.boundingBox();
return {
x: box.x + box.width / 2,
y: box.y + box.height / 2
};
};
Loading