From 8ec11aad750d2c48c311a7f66ad322da4082b2a0 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 29 Apr 2026 14:09:08 +0200 Subject: [PATCH 01/20] chore: add a url parameter to setFilterFromUrl --- .../Filters/common/FilteringModel.js | 32 +++++++++++++++++-- .../views/DataPasses/DataPassesModel.js | 4 +-- .../DataPasses/DataPassesOverviewModel.js | 2 +- .../views/Environments/EnvironmentModel.js | 2 +- .../Overview/EnvironmentOverviewModel.js | 2 +- lib/public/views/LhcFills/LhcFills.js | 2 +- .../Overview/LhcFillsOverviewModel.js | 2 +- lib/public/views/Logs/LogsModel.js | 2 +- .../views/Logs/Overview/LogsOverviewModel.js | 2 +- .../Overview/QcFlagTypesOverviewModel.js | 2 +- .../views/QcFlagTypes/QcFlagTypesModel.js | 2 +- .../views/Runs/Overview/RunsOverviewModel.js | 2 +- lib/public/views/Runs/RunsModel.js | 8 ++--- .../AnchoredSimulationPassesOverviewModel.js | 2 +- ...mulationPassesPerLhcPeriodOverviewModel.js | 2 +- .../SimulationPasses/SimulationPassesModel.js | 4 +-- .../views/lhcPeriods/LhcPeriodsModel.js | 2 +- .../Overview/LhcPeriodsOverviewModel.js | 2 +- 18 files changed, 52 insertions(+), 24 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 9cb4fd1d90..5b8b6b6aa3 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -14,7 +14,7 @@ import { expandQueryLikeNestedKey } from '../../../utilities/expandNestedKey.js'; import { SelectionModel } from '../../common/selection/SelectionModel.js'; import { FilterModel } from './FilterModel.js'; -import { buildUrl, Observable } from '/js/src/index.js'; +import { buildUrl, Observable, parseUrlParameters } from '/js/src/index.js'; /** * Model representing a filtering system, including filter inputs visibility, filters values and so on @@ -137,13 +137,37 @@ export class FilteringModel extends Observable { this.notify(); } + /** + * Compute seach parameters based a url or router + * + * @param {string|null} [url=null] the url that is to be parsed + * @returns {object} the serach parameters object + */ + _computeParameters(url = null) { + let params = {}; + + if (url) { + try { + params = parseUrlParameters(new URL(url).searchParams); + } catch (e) { + this._warnings.set('Unparseable URL', `URL could not be parsed. URL: ${url}`); + this.notify(); + } + } else { + params = this._router.params; + } + + return params; + } + /** * Look for parameters used for filtering in URL and apply them in the layout if it exists * * @param {boolean} notify if observers should be notified after setting the filters + * @param {string|null} [url=null] the url that is to be parsed into active filters * @returns {undefined} */ - setFilterFromURL(notify = false) { + setFilterFromURL(url = null, notify = false) { const { params: { page = '', filter } } = this._router; if (this._pageIdentifier === page) { @@ -186,6 +210,10 @@ export class FilteringModel extends Observable { } } + if (url) { + this._router.go(buildUrl('?', params), false, true); + } + if (notify) { this.notify(); } diff --git a/lib/public/views/DataPasses/DataPassesModel.js b/lib/public/views/DataPasses/DataPassesModel.js index 42fed10c3a..0ac1551358 100644 --- a/lib/public/views/DataPasses/DataPassesModel.js +++ b/lib/public/views/DataPasses/DataPassesModel.js @@ -40,7 +40,7 @@ export class DataPassesModel extends Observable { * @returns {void} */ loadPerLhcPeriodOverview({ lhcPeriodId }) { - this._perLhcPeriodOverviewModel.setFilterFromURL(false); + this._perLhcPeriodOverviewModel.setFilterFromUrl(null, false); this._perLhcPeriodOverviewModel.load({ lhcPeriodId }); } @@ -69,7 +69,7 @@ export class DataPassesModel extends Observable { */ loadPerSimulationPassOverview({ simulationPassId }) { this._perSimulationPassOverviewModel.simulationPassId = parseInt(simulationPassId, 10); - this._perSimulationPassOverviewModel.setFilterFromURL(false); + this._perSimulationPassOverviewModel.setFilterFromUrl(null, false); this._perSimulationPassOverviewModel.load(); } diff --git a/lib/public/views/DataPasses/DataPassesOverviewModel.js b/lib/public/views/DataPasses/DataPassesOverviewModel.js index 2834e1db1d..c7a690f6a0 100644 --- a/lib/public/views/DataPasses/DataPassesOverviewModel.js +++ b/lib/public/views/DataPasses/DataPassesOverviewModel.js @@ -61,7 +61,7 @@ export class DataPassesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/Environments/EnvironmentModel.js b/lib/public/views/Environments/EnvironmentModel.js index 1cc7fa484d..0e765df8fa 100644 --- a/lib/public/views/Environments/EnvironmentModel.js +++ b/lib/public/views/Environments/EnvironmentModel.js @@ -42,7 +42,7 @@ export class EnvironmentModel extends Observable { */ loadOverview() { if (!this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromURL(false); + this._overviewModel.setFilterFromUrl(null, false); this._overviewModel.load(); } } diff --git a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js index 5d6eb72cb3..1daa0025aa 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -76,7 +76,7 @@ export class EnvironmentOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/LhcFills/LhcFills.js b/lib/public/views/LhcFills/LhcFills.js index a4343be26a..ea92a4ae03 100644 --- a/lib/public/views/LhcFills/LhcFills.js +++ b/lib/public/views/LhcFills/LhcFills.js @@ -42,7 +42,7 @@ export default class LhcFills extends Observable { * @returns {void} */ loadOverview() { - this._overviewModel.setFilterFromURL(false); + this._overviewModel.setFilterFromUrl(null, false); this._overviewModel.load(); } diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 567182d3c5..8886126f36 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -80,7 +80,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/Logs/LogsModel.js b/lib/public/views/Logs/LogsModel.js index 7822fa4d79..67c3d9062f 100644 --- a/lib/public/views/Logs/LogsModel.js +++ b/lib/public/views/Logs/LogsModel.js @@ -55,7 +55,7 @@ export class LogsModel extends Observable { */ loadOverview() { if (!this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromURL(false); + this._overviewModel.setFilterFromUrl(null, false); this._overviewModel.fetchLogs(); } } diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index f6fcb01638..f1a89ebb0d 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -86,7 +86,7 @@ export class LogsOverviewModel extends Observable { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js index a87baedaa5..b5e37a298c 100644 --- a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js +++ b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js @@ -61,7 +61,7 @@ export class QcFlagTypesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js index 9fe8118a76..554cd61edf 100644 --- a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js +++ b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js @@ -38,7 +38,7 @@ export class QcFlagTypesModel extends Observable { * @return {void} */ loadOverview() { - this._overviewModel.setFilterFromURL(false); + this._overviewModel.setFilterFromUrl(null, false); this._overviewModel.load(); } diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 1b15fff0dc..7a4b7c36ac 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -168,7 +168,7 @@ export class RunsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/Runs/RunsModel.js b/lib/public/views/Runs/RunsModel.js index 007a456368..799c7b355d 100644 --- a/lib/public/views/Runs/RunsModel.js +++ b/lib/public/views/Runs/RunsModel.js @@ -48,7 +48,7 @@ export class RunsModel extends Observable { */ loadOverview() { if (! this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromURL(false); + this._overviewModel.setFilterFromUrl(null, false); this._overviewModel.load(); } } @@ -94,7 +94,7 @@ export class RunsModel extends Observable { this._perLhcPeriodOverviewModel.tabbedPanelModel.currentPanelKey = panel; if (!this._perLhcPeriodOverviewModel.pagination.isInfiniteScrollEnabled) { this._perLhcPeriodOverviewModel.lhcPeriodId = lhcPeriodId; - this._perLhcPeriodOverviewModel.setFilterFromURL(false); + this._perLhcPeriodOverviewModel.setFilterFromUrl(null, false); this._perLhcPeriodOverviewModel.load(); } } @@ -122,7 +122,7 @@ export class RunsModel extends Observable { * so the pagination trigger will not refresh the data. * Thus, we need to trigger the load here. */ - this._perDataPassOverviewModel.setFilterFromURL(false); + this._perDataPassOverviewModel.setFilterFromUrl(null, false); this._perDataPassOverviewModel.load(); } } @@ -151,7 +151,7 @@ export class RunsModel extends Observable { * so the pagination trigger will not refresh the data. * Thus, we need to trigger the load here. */ - this._perSimulationPassOverviewModel.setFilterFromURL(false); + this._perSimulationPassOverviewModel.setFilterFromUrl(null, false); this._perSimulationPassOverviewModel.load(); } } diff --git a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js index 810789c17f..6d6a80ff76 100644 --- a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js +++ b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js @@ -48,7 +48,7 @@ export class AnchoredSimulationPassesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js index ffc2eb5e02..cfff1e3d23 100644 --- a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js @@ -64,7 +64,7 @@ export class SimulationPassesPerLhcPeriodOverviewModel extends OverviewPageModel * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** diff --git a/lib/public/views/SimulationPasses/SimulationPassesModel.js b/lib/public/views/SimulationPasses/SimulationPassesModel.js index 8ba624efd8..5a0a9a7d17 100644 --- a/lib/public/views/SimulationPasses/SimulationPassesModel.js +++ b/lib/public/views/SimulationPasses/SimulationPassesModel.js @@ -42,7 +42,7 @@ export class SimulationPassesModel extends Observable { loadPerLhcPeriodOverview({ lhcPeriodId }) { if (!this._perLhcPeriodOverviewModel.pagination.isInfiniteScrollEnabled) { this._perLhcPeriodOverviewModel.lhcPeriodId = lhcPeriodId; - this._perLhcPeriodOverviewModel.setFilterFromURL(false); + this._perLhcPeriodOverviewModel.setFilterFromUrl(null, false); this._perLhcPeriodOverviewModel.load(); } } @@ -72,7 +72,7 @@ export class SimulationPassesModel extends Observable { */ loadAnchoredOverview({ dataPassId }) { this._anchoredOverviewModel.dataPassId = dataPassId; - this._anchoredOverviewModel.setFilterFromURL(false); + this._anchoredOverviewModel.setFilterFromUrl(null, false); this._anchoredOverviewModel.load(); } diff --git a/lib/public/views/lhcPeriods/LhcPeriodsModel.js b/lib/public/views/lhcPeriods/LhcPeriodsModel.js index 74df7b9dc7..a7e7da7385 100644 --- a/lib/public/views/lhcPeriods/LhcPeriodsModel.js +++ b/lib/public/views/lhcPeriods/LhcPeriodsModel.js @@ -35,7 +35,7 @@ export class LhcPeriodsModel extends Observable { * @returns {void} */ loadOverview() { - this._overviewModel.setFilterFromURL(false); + this._overviewModel.setFilterFromUrl(null, false); this._overviewModel.load(); } diff --git a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js index f517fe02e7..f29e39ad5a 100644 --- a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js +++ b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js @@ -61,7 +61,7 @@ export class LhcPeriodsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); + this._filteringModelsetFilterFromURL(null, notify); } /** From cd0654893d0a6f0526526a1381fe4130d6cdb11d Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 29 Apr 2026 16:55:21 +0200 Subject: [PATCH 02/20] feat: create dropdown options --- .../Filters/common/filtersPanelPopover.js | 98 ++++++++++++++++--- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 1d729eb93e..8aaa90df20 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -10,7 +10,8 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { h, info, popover, PopoverAnchors, PopoverTriggerPreConfiguration } from '/js/src/index.js'; +import { h, info, popover, PopoverAnchors, PopoverTriggerPreConfiguration, DropdownComponent, CopyToClipboardComponent } from '/js/src/index.js'; +import { iconCaretBottom } from '/js/src/icons.js'; import { profiles } from '../../common/table/profiles.js'; import { applyProfile } from '../../../utilities/applyProfile.js'; import { tooltip } from '../../common/popover/tooltip.js'; @@ -35,25 +36,34 @@ import { tooltip } from '../../common/popover/tooltip.js'; * * @return {Component} the button component */ -const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary', 'Filters'); +const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary.first-item', 'Filters'); /** - * Create main header of the filters panel - * @param {FilteringModel} filteringModel filtering model - * @returns {Component} main panel header + * Button component that resets all filters upon click + * + * @param {FilteringModel|OverviewPageModel} filteringModel the FilteringModel + * @returns {Component} the reset button component */ -const filtersToggleContentHeader = (filteringModel) => h('.flex-row.justify-between', [ - h('.f4', 'Filters'), - h( +const resetFiltersButton = (filteringModel) => h( 'button#reset-filters.btn.btn-danger', { + disabled: !filteringModel.isAnyFilterActive(), onclick: () => filteringModel.resetFiltering ? filteringModel.resetFiltering(true, true) : filteringModel.reset(true, true), - disabled: !filteringModel.isAnyFilterActive(), }, 'Reset all filters', - ), + ); + + +/** + * Create main header of the filters panel + * @param {FilteringModel} filteringModel filtering model + * @returns {Component} main panel header + */ +const filtersToggleContentHeader = (filteringModel) => h('.flex-row.justify-between', [ + h('.f4', 'Filters'), + resetFiltersButton(filteringModel), ]); /** @@ -114,9 +124,9 @@ const filtersToggleContent = ( * @param {FiltersConfiguration} filtersConfiguration filters configuration * @param {object} [configuration] optional configuration * @param {string} [configuration.profile] specify for which profile filtering should be enabled - * @return {Component} the filter component + * @return {Component} the filter button component */ -export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => popover( +const filtersPanelButton = (filteringModel, filtersConfiguration, configuration) => popover( filtersToggleTrigger(), filtersToggleContent(filteringModel, filtersConfiguration, configuration), { @@ -124,3 +134,67 @@ export const filtersPanelPopover = (filteringModel, filtersConfiguration, config anchor: PopoverAnchors.RIGHT_START, }, ); + +/** + * A button component that lets the user copy the url if there are active filters. + * + * @param {boolean} activeFilters if false, will disable the button + * @returns {Component} the copy button component + */ +const copyButtonOption = (activeFilters) => h( + '', + { style: activeFilters ? {} : { opacity: 0.5, pointerEvents: 'none' } }, + h(CopyToClipboardComponent, { value: location.href, id: "filter" }, 'Copy Active Filters'), +) + +/** + * A button component that lets the user paste the first entry of their clipboard as a filter url. + * + * @param {FilteringModel|OverviewPageModel} filteringModel the FilteringModel + * @returns {Component} the paste button component + */ +const pasteButtonOption = (filteringModel) => { + const clipboardSupported = navigator?.clipboard && window.isSecureContext; + + // Sometimes, the overview model is passed to filterPanelPopover instead of the filteringmodel (e.g. envirionments) + if (filteringModel.filteringModel) { + filteringModel = filteringModel.filteringModel; + } + + return h('.btn.btn-primary', { onclick: async () => { + const url = await navigator.clipboard.readText(); + filteringModel.setFilterFromURL(url); + + }, disabled: !clipboardSupported}, 'Paste filters'); +} + +/** + * Return component composed of the filter popover button and a dropdown trigger + * + * @param {FilteringModel} filteringModel the filtering model + * @param {FiltersConfiguration} filtersConfiguration filters configuration + * @param {object} [configuration] optional configuration + * @param {string} [configuration.profile] specify for which profile filtering should be enabled + * @return {Component} the filter component + */ +export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => { + const hasActiveFilters = filteringModel.isAnyFilterActive(); + + return h( + '.flex-row.items-center.btn-group', + [ + filtersPanelButton(filteringModel, filtersConfiguration, configuration), + DropdownComponent( + h('.btn.btn-group-item.last-item', iconCaretBottom()), + h( + '.flex-column.p2.g2', + [ + copyButtonOption(hasActiveFilters), + pasteButtonOption(filteringModel), + resetFiltersButton(filteringModel), + ], + ), + ), + ], + ); +} From acef2bf92da758e7a4217dfb03c9cc8cf4cbca1c Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 29 Apr 2026 17:18:41 +0200 Subject: [PATCH 03/20] fix linting issues --- .../Filters/common/FilteringModel.js | 11 ++--- .../Filters/common/filtersPanelPopover.js | 47 +++++++++---------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 5b8b6b6aa3..5ce15214d4 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -144,20 +144,17 @@ export class FilteringModel extends Observable { * @returns {object} the serach parameters object */ _computeParameters(url = null) { - let params = {}; - if (url) { try { - params = parseUrlParameters(new URL(url).searchParams); - } catch (e) { + return parseUrlParameters(new URL(url).searchParams); + } catch { this._warnings.set('Unparseable URL', `URL could not be parsed. URL: ${url}`); this.notify(); + return {}; } - } else { - params = this._router.params; } - return params; + return this._router.params; } /** diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 8aaa90df20..db75241844 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -45,16 +45,15 @@ const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primar * @returns {Component} the reset button component */ const resetFiltersButton = (filteringModel) => h( - 'button#reset-filters.btn.btn-danger', - { - disabled: !filteringModel.isAnyFilterActive(), - onclick: () => filteringModel.resetFiltering - ? filteringModel.resetFiltering(true, true) - : filteringModel.reset(true, true), - }, - 'Reset all filters', - ); - + 'button#reset-filters.btn.btn-danger', + { + disabled: !filteringModel.isAnyFilterActive(), + onclick: () => filteringModel.resetFiltering + ? filteringModel.resetFiltering(true, true) + : filteringModel.reset(true, true), + }, + 'Reset all filters', +); /** * Create main header of the filters panel @@ -144,29 +143,29 @@ const filtersPanelButton = (filteringModel, filtersConfiguration, configuration) const copyButtonOption = (activeFilters) => h( '', { style: activeFilters ? {} : { opacity: 0.5, pointerEvents: 'none' } }, - h(CopyToClipboardComponent, { value: location.href, id: "filter" }, 'Copy Active Filters'), -) + h(CopyToClipboardComponent, { value: location.href, id: 'filter' }, 'Copy Active Filters'), +); /** * A button component that lets the user paste the first entry of their clipboard as a filter url. * - * @param {FilteringModel|OverviewPageModel} filteringModel the FilteringModel + * @param {FilteringModel|OverviewPageModel} model the FilteringModel * @returns {Component} the paste button component */ -const pasteButtonOption = (filteringModel) => { +const pasteButtonOption = (model) => { const clipboardSupported = navigator?.clipboard && window.isSecureContext; // Sometimes, the overview model is passed to filterPanelPopover instead of the filteringmodel (e.g. envirionments) - if (filteringModel.filteringModel) { - filteringModel = filteringModel.filteringModel; - } + const { filteringModel = model } = model; - return h('.btn.btn-primary', { onclick: async () => { - const url = await navigator.clipboard.readText(); - filteringModel.setFilterFromURL(url); - - }, disabled: !clipboardSupported}, 'Paste filters'); -} + return h('.btn.btn-primary', { + onclick: async () => { + const url = await navigator.clipboard.readText(); + filteringModel.setFilterFromURL(url); + }, + disabled: !clipboardSupported, + }, 'Paste filters'); +}; /** * Return component composed of the filter popover button and a dropdown trigger @@ -197,4 +196,4 @@ export const filtersPanelPopover = (filteringModel, filtersConfiguration, config ), ], ); -} +}; From 52cc7499f76d835f9342f4690e734363197bc7ff Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 30 Apr 2026 09:56:19 +0200 Subject: [PATCH 04/20] chore: add tests for new buttons --- .../Filters/common/filtersPanelPopover.js | 1 + .../components/filtersPopoverPanel.test.js | 86 +++++++++++++++++++ test/public/components/index.js | 2 + 3 files changed, 89 insertions(+) create mode 100644 test/public/components/filtersPopoverPanel.test.js diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index db75241844..8f0ca39a4b 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -164,6 +164,7 @@ const pasteButtonOption = (model) => { filteringModel.setFilterFromURL(url); }, disabled: !clipboardSupported, + id: 'paste-filter', }, 'Paste filters'); }; diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js new file mode 100644 index 0000000000..7e6d7e0011 --- /dev/null +++ b/test/public/components/filtersPopoverPanel.test.js @@ -0,0 +1,86 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * 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. + */ + +const { expect } = require('chai'); +const { defaultBefore, defaultAfter, pressElement, takeScreenshot, expectInputValue } = require('../defaults.js'); + +module.exports = () => { + let page; + let browser; + let context; + let url; + + before(async () => { + [page, browser, url] = await defaultBefore(page, browser); + context = browser.defaultBrowserContext(); + context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']); + }); + + it('Should copy url when clicking filer copy button', async () => { + const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; + await page.goto(url, { waitUntil: 'load' }); + await takeScreenshot(page, 'test'); + await pressElement(page, '#copy-filter', true); + + const clipboardContents = await page.evaluate(async () => decodeURI(await navigator.clipboard.readText())); + expect(clipboardContents).to.equal(url); + }); + + it('Should set filters when pressing paste active filters button', async () => { + const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; + + await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); + await pressElement(page, '#paste-filter', true); + + const actualUrl = page.url(); + expect(actualUrl).to.equal(url); + + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'name'); + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '100'); + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); + }); + + it('Should set filters when pressing paste active filters button', async () => { + const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; + + await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); + await pressElement(page, '#paste-filter', true); + + const actualUrl = page.url(); + expect(actualUrl).to.equal(url); + + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'name'); + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '100'); + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); + }); + + it('Should reset filters when pressing the reset all filters button', async () => { + const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; + + await page.goto(url, { waitUntil: 'load' }); + + await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); + await pressElement(page, '.dropdown #reset-filters', true); + + const actualUrl = page.url(); + expect(actualUrl).to.equal('http://localhost:4000/?page=lhc-period-overview'); + + await expectInputValue(page, '.name-filter input', ''); + await expectInputValue(page, '.year-filter input', ''); + await expectInputValue(page, '.pdpBeamTypes-filter input', ''); + }); + + after(async () => { + await defaultAfter(page, browser); + }); +}; diff --git a/test/public/components/index.js b/test/public/components/index.js index 700564755c..794ae79252 100644 --- a/test/public/components/index.js +++ b/test/public/components/index.js @@ -13,8 +13,10 @@ const NavBarSuite = require('./navBar.test') const WarningSuite = require('./warnings.test') +const FiltersPanelSuite = require('./filtersPopoverPanel.test') module.exports = () => { describe('Navbar component', NavBarSuite); describe('Warning component', WarningSuite) + describe('FiltersPanelPopover component', FiltersPanelSuite) }; From 3c542a492cc52a25b70207389650237aa21eb5e9 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Mon, 25 May 2026 13:50:41 +0200 Subject: [PATCH 05/20] feat: add filter restoration button --- .../Filters/common/FilteringModel.js | 16 ++++++++++++++++ .../Filters/common/filtersPanelPopover.js | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 5ce15214d4..9cec2592a3 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -37,6 +37,7 @@ export class FilteringModel extends Observable { this._filters = {}; this._filterModels = []; Object.entries(filters).forEach(([key, model]) => this.put(key, model)); + this.previousUrl = ''; } /** @@ -58,6 +59,12 @@ export class FilteringModel extends Observable { * @return {void} */ reset(notify = false, clearUrl = false) { + if (!this.isAnyFilterActive()) { + return; + } + + this.previousUrl = location.href; + for (const model of this._filterModels) { model.reset(); } @@ -137,6 +144,15 @@ export class FilteringModel extends Observable { this.notify(); } + /** + * Will restore filters to the state before it was reset. + * @returns {undefined} + */ + restoreFilters() { + this.setFilterFromURL(this.previousUrl); + this.previousUrl = ''; + } + /** * Compute seach parameters based a url or router * diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 8f0ca39a4b..063df33ae0 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -158,7 +158,7 @@ const pasteButtonOption = (model) => { // Sometimes, the overview model is passed to filterPanelPopover instead of the filteringmodel (e.g. envirionments) const { filteringModel = model } = model; - return h('.btn.btn-primary', { + return h('button.btn.btn-primary', { onclick: async () => { const url = await navigator.clipboard.readText(); filteringModel.setFilterFromURL(url); @@ -168,6 +168,22 @@ const pasteButtonOption = (model) => { }, 'Paste filters'); }; +/** + * A button component that lets the user paste the first entry of their clipboard as a filter url. + * + * @param {FilteringModel|OverviewPageModel} model the FilteringModel + * @returns {Component} the paste button component + */ +const restoreFiltersButton = (model) => { + const { filteringModel = model } = model; + + return h('button.btn.btn-primary', { + onclick: async () => filteringModel.restoreFilters(), + disabled: !filteringModel.previousUrl, + id: 'restore-filters', + }, 'Restore Filters'); +}; + /** * Return component composed of the filter popover button and a dropdown trigger * @@ -192,6 +208,7 @@ export const filtersPanelPopover = (filteringModel, filtersConfiguration, config copyButtonOption(hasActiveFilters), pasteButtonOption(filteringModel), resetFiltersButton(filteringModel), + restoreFiltersButton(filteringModel), ], ), ), From e9d594537c9923a6f630a4d81fb28cce4cb85f1b Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 11:35:59 +0200 Subject: [PATCH 06/20] chore: remove restoreFilters --- .../components/Filters/common/FilteringModel.js | 12 ------------ .../Filters/common/filtersPanelPopover.js | 17 ----------------- 2 files changed, 29 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 9cec2592a3..0164540af1 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -37,7 +37,6 @@ export class FilteringModel extends Observable { this._filters = {}; this._filterModels = []; Object.entries(filters).forEach(([key, model]) => this.put(key, model)); - this.previousUrl = ''; } /** @@ -63,8 +62,6 @@ export class FilteringModel extends Observable { return; } - this.previousUrl = location.href; - for (const model of this._filterModels) { model.reset(); } @@ -144,15 +141,6 @@ export class FilteringModel extends Observable { this.notify(); } - /** - * Will restore filters to the state before it was reset. - * @returns {undefined} - */ - restoreFilters() { - this.setFilterFromURL(this.previousUrl); - this.previousUrl = ''; - } - /** * Compute seach parameters based a url or router * diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 063df33ae0..99a750e1e5 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -168,22 +168,6 @@ const pasteButtonOption = (model) => { }, 'Paste filters'); }; -/** - * A button component that lets the user paste the first entry of their clipboard as a filter url. - * - * @param {FilteringModel|OverviewPageModel} model the FilteringModel - * @returns {Component} the paste button component - */ -const restoreFiltersButton = (model) => { - const { filteringModel = model } = model; - - return h('button.btn.btn-primary', { - onclick: async () => filteringModel.restoreFilters(), - disabled: !filteringModel.previousUrl, - id: 'restore-filters', - }, 'Restore Filters'); -}; - /** * Return component composed of the filter popover button and a dropdown trigger * @@ -208,7 +192,6 @@ export const filtersPanelPopover = (filteringModel, filtersConfiguration, config copyButtonOption(hasActiveFilters), pasteButtonOption(filteringModel), resetFiltersButton(filteringModel), - restoreFiltersButton(filteringModel), ], ), ), From 6c1b59e111f42ec55604e344bdb94c8be6d9ed03 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 12:39:00 +0200 Subject: [PATCH 07/20] chore: pass url to setFiltersFromUrl --- lib/public/components/Filters/common/FilteringModel.js | 3 ++- .../components/Filters/common/filtersPanelPopover.js | 2 +- lib/public/views/DataPasses/DataPassesModel.js | 4 ++-- lib/public/views/DataPasses/DataPassesOverviewModel.js | 2 +- lib/public/views/Environments/EnvironmentModel.js | 2 +- .../Environments/Overview/EnvironmentOverviewModel.js | 2 +- lib/public/views/LhcFills/LhcFills.js | 2 +- .../views/LhcFills/Overview/LhcFillsOverviewModel.js | 2 +- lib/public/views/Logs/LogsModel.js | 2 +- lib/public/views/Logs/Overview/LogsOverviewModel.js | 2 +- .../QcFlagTypes/Overview/QcFlagTypesOverviewModel.js | 2 +- lib/public/views/QcFlagTypes/QcFlagTypesModel.js | 2 +- lib/public/views/Runs/Overview/RunsOverviewModel.js | 2 +- lib/public/views/Runs/RunsModel.js | 8 ++++---- .../AnchoredSimulationPassesOverviewModel.js | 2 +- .../SimulationPassesPerLhcPeriodOverviewModel.js | 2 +- .../views/SimulationPasses/SimulationPassesModel.js | 4 ++-- lib/public/views/lhcPeriods/LhcPeriodsModel.js | 2 +- .../views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js | 2 +- 19 files changed, 25 insertions(+), 24 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 0164540af1..ed543247bc 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -169,7 +169,8 @@ export class FilteringModel extends Observable { * @returns {undefined} */ setFilterFromURL(url = null, notify = false) { - const { params: { page = '', filter } } = this._router; + const params = this._computeParameters(url); + const { page, filter } = params; if (this._pageIdentifier === page) { if (!filter) { diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 99a750e1e5..ee5a370fdf 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -161,7 +161,7 @@ const pasteButtonOption = (model) => { return h('button.btn.btn-primary', { onclick: async () => { const url = await navigator.clipboard.readText(); - filteringModel.setFilterFromURL(url); + filteringModel.setFilterFromURL(url, true); }, disabled: !clipboardSupported, id: 'paste-filter', diff --git a/lib/public/views/DataPasses/DataPassesModel.js b/lib/public/views/DataPasses/DataPassesModel.js index 0ac1551358..42fed10c3a 100644 --- a/lib/public/views/DataPasses/DataPassesModel.js +++ b/lib/public/views/DataPasses/DataPassesModel.js @@ -40,7 +40,7 @@ export class DataPassesModel extends Observable { * @returns {void} */ loadPerLhcPeriodOverview({ lhcPeriodId }) { - this._perLhcPeriodOverviewModel.setFilterFromUrl(null, false); + this._perLhcPeriodOverviewModel.setFilterFromURL(false); this._perLhcPeriodOverviewModel.load({ lhcPeriodId }); } @@ -69,7 +69,7 @@ export class DataPassesModel extends Observable { */ loadPerSimulationPassOverview({ simulationPassId }) { this._perSimulationPassOverviewModel.simulationPassId = parseInt(simulationPassId, 10); - this._perSimulationPassOverviewModel.setFilterFromUrl(null, false); + this._perSimulationPassOverviewModel.setFilterFromURL(false); this._perSimulationPassOverviewModel.load(); } diff --git a/lib/public/views/DataPasses/DataPassesOverviewModel.js b/lib/public/views/DataPasses/DataPassesOverviewModel.js index c7a690f6a0..ca6c058220 100644 --- a/lib/public/views/DataPasses/DataPassesOverviewModel.js +++ b/lib/public/views/DataPasses/DataPassesOverviewModel.js @@ -61,7 +61,7 @@ export class DataPassesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/Environments/EnvironmentModel.js b/lib/public/views/Environments/EnvironmentModel.js index 0e765df8fa..1cc7fa484d 100644 --- a/lib/public/views/Environments/EnvironmentModel.js +++ b/lib/public/views/Environments/EnvironmentModel.js @@ -42,7 +42,7 @@ export class EnvironmentModel extends Observable { */ loadOverview() { if (!this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromUrl(null, false); + this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } } diff --git a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js index 1daa0025aa..49a06d8758 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -76,7 +76,7 @@ export class EnvironmentOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/LhcFills/LhcFills.js b/lib/public/views/LhcFills/LhcFills.js index ea92a4ae03..a4343be26a 100644 --- a/lib/public/views/LhcFills/LhcFills.js +++ b/lib/public/views/LhcFills/LhcFills.js @@ -42,7 +42,7 @@ export default class LhcFills extends Observable { * @returns {void} */ loadOverview() { - this._overviewModel.setFilterFromUrl(null, false); + this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 8886126f36..b09fe7f00f 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -80,7 +80,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/Logs/LogsModel.js b/lib/public/views/Logs/LogsModel.js index 67c3d9062f..7822fa4d79 100644 --- a/lib/public/views/Logs/LogsModel.js +++ b/lib/public/views/Logs/LogsModel.js @@ -55,7 +55,7 @@ export class LogsModel extends Observable { */ loadOverview() { if (!this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromUrl(null, false); + this._overviewModel.setFilterFromURL(false); this._overviewModel.fetchLogs(); } } diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index f1a89ebb0d..4f73a8cf98 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -86,7 +86,7 @@ export class LogsOverviewModel extends Observable { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js index b5e37a298c..2e25d7b67b 100644 --- a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js +++ b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js @@ -61,7 +61,7 @@ export class QcFlagTypesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js index 554cd61edf..9fe8118a76 100644 --- a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js +++ b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js @@ -38,7 +38,7 @@ export class QcFlagTypesModel extends Observable { * @return {void} */ loadOverview() { - this._overviewModel.setFilterFromUrl(null, false); + this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 7a4b7c36ac..482e9a40d5 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -168,7 +168,7 @@ export class RunsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/Runs/RunsModel.js b/lib/public/views/Runs/RunsModel.js index 799c7b355d..007a456368 100644 --- a/lib/public/views/Runs/RunsModel.js +++ b/lib/public/views/Runs/RunsModel.js @@ -48,7 +48,7 @@ export class RunsModel extends Observable { */ loadOverview() { if (! this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromUrl(null, false); + this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } } @@ -94,7 +94,7 @@ export class RunsModel extends Observable { this._perLhcPeriodOverviewModel.tabbedPanelModel.currentPanelKey = panel; if (!this._perLhcPeriodOverviewModel.pagination.isInfiniteScrollEnabled) { this._perLhcPeriodOverviewModel.lhcPeriodId = lhcPeriodId; - this._perLhcPeriodOverviewModel.setFilterFromUrl(null, false); + this._perLhcPeriodOverviewModel.setFilterFromURL(false); this._perLhcPeriodOverviewModel.load(); } } @@ -122,7 +122,7 @@ export class RunsModel extends Observable { * so the pagination trigger will not refresh the data. * Thus, we need to trigger the load here. */ - this._perDataPassOverviewModel.setFilterFromUrl(null, false); + this._perDataPassOverviewModel.setFilterFromURL(false); this._perDataPassOverviewModel.load(); } } @@ -151,7 +151,7 @@ export class RunsModel extends Observable { * so the pagination trigger will not refresh the data. * Thus, we need to trigger the load here. */ - this._perSimulationPassOverviewModel.setFilterFromUrl(null, false); + this._perSimulationPassOverviewModel.setFilterFromURL(false); this._perSimulationPassOverviewModel.load(); } } diff --git a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js index 6d6a80ff76..20fc3b725f 100644 --- a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js +++ b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js @@ -48,7 +48,7 @@ export class AnchoredSimulationPassesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js index cfff1e3d23..ac2bd40ce8 100644 --- a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js @@ -64,7 +64,7 @@ export class SimulationPassesPerLhcPeriodOverviewModel extends OverviewPageModel * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** diff --git a/lib/public/views/SimulationPasses/SimulationPassesModel.js b/lib/public/views/SimulationPasses/SimulationPassesModel.js index 5a0a9a7d17..8ba624efd8 100644 --- a/lib/public/views/SimulationPasses/SimulationPassesModel.js +++ b/lib/public/views/SimulationPasses/SimulationPassesModel.js @@ -42,7 +42,7 @@ export class SimulationPassesModel extends Observable { loadPerLhcPeriodOverview({ lhcPeriodId }) { if (!this._perLhcPeriodOverviewModel.pagination.isInfiniteScrollEnabled) { this._perLhcPeriodOverviewModel.lhcPeriodId = lhcPeriodId; - this._perLhcPeriodOverviewModel.setFilterFromUrl(null, false); + this._perLhcPeriodOverviewModel.setFilterFromURL(false); this._perLhcPeriodOverviewModel.load(); } } @@ -72,7 +72,7 @@ export class SimulationPassesModel extends Observable { */ loadAnchoredOverview({ dataPassId }) { this._anchoredOverviewModel.dataPassId = dataPassId; - this._anchoredOverviewModel.setFilterFromUrl(null, false); + this._anchoredOverviewModel.setFilterFromURL(false); this._anchoredOverviewModel.load(); } diff --git a/lib/public/views/lhcPeriods/LhcPeriodsModel.js b/lib/public/views/lhcPeriods/LhcPeriodsModel.js index a7e7da7385..74df7b9dc7 100644 --- a/lib/public/views/lhcPeriods/LhcPeriodsModel.js +++ b/lib/public/views/lhcPeriods/LhcPeriodsModel.js @@ -35,7 +35,7 @@ export class LhcPeriodsModel extends Observable { * @returns {void} */ loadOverview() { - this._overviewModel.setFilterFromUrl(null, false); + this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } diff --git a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js index f29e39ad5a..3d2f4dad19 100644 --- a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js +++ b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js @@ -61,7 +61,7 @@ export class LhcPeriodsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModelsetFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(null, notify); } /** From 5abae211ad0101deefcdef1531edfd366f8d5b33 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 12:56:51 +0200 Subject: [PATCH 08/20] chore: switch url and notify arguments --- lib/public/components/Filters/common/FilteringModel.js | 2 +- lib/public/components/Filters/common/filtersPanelPopover.js | 2 +- lib/public/views/DataPasses/DataPassesOverviewModel.js | 2 +- .../views/Environments/Overview/EnvironmentOverviewModel.js | 2 +- lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js | 2 +- lib/public/views/Logs/Overview/LogsOverviewModel.js | 2 +- .../views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js | 2 +- lib/public/views/Runs/Overview/RunsOverviewModel.js | 2 +- .../AnchoredOverview/AnchoredSimulationPassesOverviewModel.js | 2 +- .../SimulationPassesPerLhcPeriodOverviewModel.js | 2 +- lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index ed543247bc..7cf22e9065 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -168,7 +168,7 @@ export class FilteringModel extends Observable { * @param {string|null} [url=null] the url that is to be parsed into active filters * @returns {undefined} */ - setFilterFromURL(url = null, notify = false) { + setFilterFromURL(notify = false, url = null) { const params = this._computeParameters(url); const { page, filter } = params; diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index ee5a370fdf..7146d1add7 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -161,7 +161,7 @@ const pasteButtonOption = (model) => { return h('button.btn.btn-primary', { onclick: async () => { const url = await navigator.clipboard.readText(); - filteringModel.setFilterFromURL(url, true); + filteringModel.setFilterFromURL(true, url); }, disabled: !clipboardSupported, id: 'paste-filter', diff --git a/lib/public/views/DataPasses/DataPassesOverviewModel.js b/lib/public/views/DataPasses/DataPassesOverviewModel.js index ca6c058220..2834e1db1d 100644 --- a/lib/public/views/DataPasses/DataPassesOverviewModel.js +++ b/lib/public/views/DataPasses/DataPassesOverviewModel.js @@ -61,7 +61,7 @@ export class DataPassesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js index 49a06d8758..5d6eb72cb3 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -76,7 +76,7 @@ export class EnvironmentOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index b09fe7f00f..567182d3c5 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -80,7 +80,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 4f73a8cf98..f6fcb01638 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -86,7 +86,7 @@ export class LogsOverviewModel extends Observable { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js index 2e25d7b67b..a87baedaa5 100644 --- a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js +++ b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js @@ -61,7 +61,7 @@ export class QcFlagTypesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 482e9a40d5..1b15fff0dc 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -168,7 +168,7 @@ export class RunsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js index 20fc3b725f..810789c17f 100644 --- a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js +++ b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js @@ -48,7 +48,7 @@ export class AnchoredSimulationPassesOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js index ac2bd40ce8..ffc2eb5e02 100644 --- a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js @@ -64,7 +64,7 @@ export class SimulationPassesPerLhcPeriodOverviewModel extends OverviewPageModel * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** diff --git a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js index 3d2f4dad19..f517fe02e7 100644 --- a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js +++ b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js @@ -61,7 +61,7 @@ export class LhcPeriodsOverviewModel extends OverviewPageModel { * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters */ setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(null, notify); + this._filteringModel.setFilterFromURL(notify); } /** From 76284d5eaa2548403a72638f4a4e516080dc1d1a Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 21:52:19 +0200 Subject: [PATCH 09/20] refactor: change computeParameters to guarentee the presence of a url --- .../Filters/common/FilteringModel.js | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 7cf22e9065..5e67f8afc4 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -144,21 +144,17 @@ export class FilteringModel extends Observable { /** * Compute seach parameters based a url or router * - * @param {string|null} [url=null] the url that is to be parsed + * @param {string} url the url that is to be parsed * @returns {object} the serach parameters object */ - _computeParameters(url = null) { - if (url) { - try { - return parseUrlParameters(new URL(url).searchParams); - } catch { - this._warnings.set('Unparseable URL', `URL could not be parsed. URL: ${url}`); - this.notify(); - return {}; - } + _computeParameters(url) { + try { + return parseUrlParameters(new URL(url).searchParams); + } catch { + this._warnings.set(WARNING_TYPES.UNPARSABLE_URL, `URL could not be parsed. URL: ${url}`); + this.notify(); + return {}; } - - return this._router.params; } /** @@ -169,7 +165,7 @@ export class FilteringModel extends Observable { * @returns {undefined} */ setFilterFromURL(notify = false, url = null) { - const params = this._computeParameters(url); + const params = url ? this._computeParameters(url) : this._router.params; const { page, filter } = params; if (this._pageIdentifier === page) { From 89ea57c93200c58aa79b2c3e2349a0706a2d75da Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 21:53:04 +0200 Subject: [PATCH 10/20] feat: add Warning Type enum --- .../Filters/common/FilteringModel.js | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 5e67f8afc4..f5b17a59c1 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -16,6 +16,13 @@ import { SelectionModel } from '../../common/selection/SelectionModel.js'; import { FilterModel } from './FilterModel.js'; import { buildUrl, Observable, parseUrlParameters } from '/js/src/index.js'; +const WARNING_TYPES = Object.freeze({ + PAGE_MISMATCH: 'Page-Filter mismatch', + UNKNOWN_FILTERS: 'Unknown Filters', + UNPARSABLE_URL: 'Unparseable URL', + UNPARSABLE_FILTERS: 'Unparsable Filters', +}) + /** * Model representing a filtering system, including filter inputs visibility, filters values and so on */ @@ -71,6 +78,7 @@ export class FilteringModel extends Observable { } if (clearUrl) { + this._clearWarnings(); const { params } = this._router; params.filter = this.normalized; this._router.go(buildUrl('?', params), false, true); @@ -165,10 +173,16 @@ export class FilteringModel extends Observable { * @returns {undefined} */ setFilterFromURL(notify = false, url = null) { + this._clearWarnings(); + const params = url ? this._computeParameters(url) : this._router.params; const { page, filter } = params; - - if (this._pageIdentifier === page) { + + if (this._pageIdentifier !== page) { + if (url && page) { // page might be undefined if the url is unparsable + this._warnings.set(WARNING_TYPES.PAGE_MISMATCH, `The filters you tried applying were meant for ${page}`); + } + } else { if (!filter) { this.reset(); return; @@ -191,25 +205,21 @@ export class FilteringModel extends Observable { if (setFilterErrors.length > 0) { this._warnings.set( - 'Unparsable Filters', + WARNING_TYPES.UNPARSABLE_FILTERS, `The following filter-value pairs could not be parsed: [${setFilterErrors.join(', ')}]`, ); - } else { - this._warnings.delete('Unparsable Filters'); } if (unknownFilters.length > 0) { this._warnings.set( - 'Unknown Filters', + WARNING_TYPES.UNKNOWN_FILTERS, `The filters: [${unknownFilters.join(', ')}]; are not reccognised. Check if they are spelled correctly.`, ); - } else { - this._warnings.delete('Unknown Filters'); } - } - if (url) { - this._router.go(buildUrl('?', params), false, true); + if (url) { + this._router.go(buildUrl('?', params), false, true); + } } if (notify) { @@ -217,6 +227,18 @@ export class FilteringModel extends Observable { } } + /** + * Clear all filter-related warnings from the warnings map + * + * @returns {undefined} + */ + _clearWarnings() { + this._warnings.delete(WARNING_TYPES.UNPARSABLE_URL); + this._warnings.delete(WARNING_TYPES.UNPARSABLE_FILTERS); + this._warnings.delete(WARNING_TYPES.UNKNOWN_FILTERS); + this._warnings.delete(WARNING_TYPES.PAGE_MISMATCH); + } + /** * Add new filter * From accbdfff94c1615db21b588ffb80346e6ca1e853 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 22:04:04 +0200 Subject: [PATCH 11/20] refactor: move filter setting loop to it's own function --- .../Filters/common/FilteringModel.js | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index f5b17a59c1..8f39806be3 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -188,20 +188,7 @@ export class FilteringModel extends Observable { return; } - const unknownFilters = []; - const setFilterErrors = []; - - for (const [key, value] of Object.entries(filter)) { - if (key in this._filters) { - try { - this._filters[key].normalized = value; - } catch { - setFilterErrors.push(`${buildUrl('', { [key]: value }).slice(1)}`); - } - } else { - unknownFilters.push(`'${key}'`); - } - } + const { setFilterErrors, unknownFilters } = this._setFilters(filter); if (setFilterErrors.length > 0) { this._warnings.set( @@ -239,6 +226,31 @@ export class FilteringModel extends Observable { this._warnings.delete(WARNING_TYPES.PAGE_MISMATCH); } + /** + * Sets all filters using their normalized setters + * + * @param {object} an object containging the uknown filters and the filters that failed to parse + */ + _setFilters(filters) { + const unknownFilters = []; + const setFilterErrors = []; + + for (const [key, value] of Object.entries(filters)) { + if (key in this._filters) { + try { + this._filters[key].normalized = value; + } catch { + setFilterErrors.push(`${buildUrl('', { [key]: value }).slice(1)}`); + } + } else { + unknownFilters.push(`'${key}'`); + } + } + + return { unknownFilters, setFilterErrors }; + } + /** * Add new filter * From 5d81c5c02bb486434f8ea689ba80a2f1b5e99594 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 22:04:12 +0200 Subject: [PATCH 12/20] fix linter issues --- lib/public/components/Filters/common/FilteringModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 8f39806be3..8be6c32a68 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -21,7 +21,7 @@ const WARNING_TYPES = Object.freeze({ UNKNOWN_FILTERS: 'Unknown Filters', UNPARSABLE_URL: 'Unparseable URL', UNPARSABLE_FILTERS: 'Unparsable Filters', -}) +}); /** * Model representing a filtering system, including filter inputs visibility, filters values and so on @@ -177,9 +177,9 @@ export class FilteringModel extends Observable { const params = url ? this._computeParameters(url) : this._router.params; const { page, filter } = params; - + if (this._pageIdentifier !== page) { - if (url && page) { // page might be undefined if the url is unparsable + if (url && page) { // 'page' might be undefined if the url is unparsable this._warnings.set(WARNING_TYPES.PAGE_MISMATCH, `The filters you tried applying were meant for ${page}`); } } else { From b42bb373947e953565c124e9619a2e5a96116622 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 22:12:57 +0200 Subject: [PATCH 13/20] test: add warning test for page mismatch --- .../public/components/filtersPopoverPanel.test.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js index 7e6d7e0011..013407480d 100644 --- a/test/public/components/filtersPopoverPanel.test.js +++ b/test/public/components/filtersPopoverPanel.test.js @@ -12,7 +12,7 @@ */ const { expect } = require('chai'); -const { defaultBefore, defaultAfter, pressElement, takeScreenshot, expectInputValue } = require('../defaults.js'); +const { defaultBefore, defaultAfter, pressElement, takeScreenshot, expectInputValue, goToPage, getInnerText } = require('../defaults.js'); module.exports = () => { let page; @@ -69,9 +69,7 @@ module.exports = () => { await page.goto(url, { waitUntil: 'load' }); - await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); await pressElement(page, '.dropdown #reset-filters', true); - const actualUrl = page.url(); expect(actualUrl).to.equal('http://localhost:4000/?page=lhc-period-overview'); @@ -80,6 +78,17 @@ module.exports = () => { await expectInputValue(page, '.pdpBeamTypes-filter input', ''); }); + it('Should show warning if filters are pasted on the wrong page', async () => { + const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name'; + await goToPage(page, 'log-overview'); + + await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); + await pressElement(page, '.dropdown #paste-filter', true); + + const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); + expect(warningText).to.equal('Page-Filter mismatch:\nThe filters you tried applying were meant for lhc-period-overview'); + }); + after(async () => { await defaultAfter(page, browser); }); From 6f12fd0e720eaba36d9c66ba9de899b3e4d772df Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Wed, 17 Jun 2026 22:29:45 +0200 Subject: [PATCH 14/20] style: change warning message --- lib/public/components/Filters/common/FilteringModel.js | 2 +- test/public/components/filtersPopoverPanel.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 8be6c32a68..6b3c950ea3 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -180,7 +180,7 @@ export class FilteringModel extends Observable { if (this._pageIdentifier !== page) { if (url && page) { // 'page' might be undefined if the url is unparsable - this._warnings.set(WARNING_TYPES.PAGE_MISMATCH, `The filters you tried applying were meant for ${page}`); + this._warnings.set(WARNING_TYPES.PAGE_MISMATCH, `The filters provided were meant for ${page}`); } } else { if (!filter) { diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js index 013407480d..0b355247fa 100644 --- a/test/public/components/filtersPopoverPanel.test.js +++ b/test/public/components/filtersPopoverPanel.test.js @@ -86,7 +86,7 @@ module.exports = () => { await pressElement(page, '.dropdown #paste-filter', true); const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); - expect(warningText).to.equal('Page-Filter mismatch:\nThe filters you tried applying were meant for lhc-period-overview'); + expect(warningText).to.equal('Page-Filter mismatch:\nThe filters provided were meant for lhc-period-overview'); }); after(async () => { From 62b8793fab4070e8482c09654c041b8807b4514c Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 18 Jun 2026 10:37:46 +0200 Subject: [PATCH 15/20] refactor: loop through warning types in _clearWarnings --- lib/public/components/Filters/common/FilteringModel.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 6b3c950ea3..2f196d4f7c 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -220,10 +220,9 @@ export class FilteringModel extends Observable { * @returns {undefined} */ _clearWarnings() { - this._warnings.delete(WARNING_TYPES.UNPARSABLE_URL); - this._warnings.delete(WARNING_TYPES.UNPARSABLE_FILTERS); - this._warnings.delete(WARNING_TYPES.UNKNOWN_FILTERS); - this._warnings.delete(WARNING_TYPES.PAGE_MISMATCH); + for (const key in Object.keys(WARNING_TYPES)) { + this._warnings.delete(key); + } } /** From 05b082d9587118e75c55ef2a962c748b2a125d9d Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 18 Jun 2026 10:40:47 +0200 Subject: [PATCH 16/20] test: remove duplicate test --- test/public/components/filtersPopoverPanel.test.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js index 0b355247fa..220d84c741 100644 --- a/test/public/components/filtersPopoverPanel.test.js +++ b/test/public/components/filtersPopoverPanel.test.js @@ -50,20 +50,6 @@ module.exports = () => { await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); }); - it('Should set filters when pressing paste active filters button', async () => { - const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; - - await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '#paste-filter', true); - - const actualUrl = page.url(); - expect(actualUrl).to.equal(url); - - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'name'); - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '100'); - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); - }); - it('Should reset filters when pressing the reset all filters button', async () => { const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; From 767a531c557d7c29fc175dac391fe48495a0c091 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 18 Jun 2026 10:58:49 +0200 Subject: [PATCH 17/20] chore: move tests from filterPopOverPanel to warnings --- .../components/filtersPopoverPanel.test.js | 11 -------- test/public/components/warnings.test.js | 28 ++++++++++++++++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js index 220d84c741..f1af536081 100644 --- a/test/public/components/filtersPopoverPanel.test.js +++ b/test/public/components/filtersPopoverPanel.test.js @@ -64,17 +64,6 @@ module.exports = () => { await expectInputValue(page, '.pdpBeamTypes-filter input', ''); }); - it('Should show warning if filters are pasted on the wrong page', async () => { - const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name'; - await goToPage(page, 'log-overview'); - - await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '.dropdown #paste-filter', true); - - const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); - expect(warningText).to.equal('Page-Filter mismatch:\nThe filters provided were meant for lhc-period-overview'); - }); - after(async () => { await defaultAfter(page, browser); }); diff --git a/test/public/components/warnings.test.js b/test/public/components/warnings.test.js index a8a824feee..d19ef67295 100644 --- a/test/public/components/warnings.test.js +++ b/test/public/components/warnings.test.js @@ -17,6 +17,8 @@ const { defaultAfter, getInnerText, pressElement, + goToPage, + takeScreenshot, } = require('../defaults.js'); module.exports = () => { @@ -24,7 +26,9 @@ module.exports = () => { let browser; before(async () => { - [page, browser] = await defaultBefore(); + [page, browser, url] = await defaultBefore(page, browser); + context = browser.defaultBrowserContext(); + context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']); }); it('Should show warning when a filter in the url is not recognised', async () => { @@ -51,6 +55,28 @@ module.exports = () => { expect(unparsableWarningText).to.equal('Unparsable Filters:\nThe following filter-value pairs could not be parsed: [detectors[operator]=or, tags[operation]=or]'); expect(unknownFilterWarningText).to.equal('Unknown Filters:\nThe filters: [\'detecttors\', \'tagss\']; are not reccognised. Check if they are spelled correctly.'); }); + + it('Should show warning if an unparsable filter url is pasted', async () => { + const url = 'unparsable url'; + await goToPage(page, 'log-overview'); + + await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); + await pressElement(page, '.dropdown #paste-filter', true); + + const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); + expect(warningText).to.equal('Unparseable URL:\nURL could not be parsed. URL: unparsable url'); + }); + + it('Should show warning if filter url is pasted on the wong page', async () => { + const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name'; + await goToPage(page, 'log-overview'); + + await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); + await pressElement(page, '.dropdown #paste-filter', true); + + const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); + expect(warningText).to.equal('Page-Filter mismatch:\nThe filters provided were meant for lhc-period-overview'); + }); after(async () => { await defaultAfter(page, browser); From d50af66606d6e63c75acdcd9b70248ddf593a993 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 18 Jun 2026 11:02:26 +0200 Subject: [PATCH 18/20] chore: pluralise the ids of copy and paste filters --- lib/public/components/Filters/common/filtersPanelPopover.js | 4 ++-- test/public/components/filtersPopoverPanel.test.js | 4 ++-- test/public/components/warnings.test.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 7146d1add7..171dda6e8d 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -143,7 +143,7 @@ const filtersPanelButton = (filteringModel, filtersConfiguration, configuration) const copyButtonOption = (activeFilters) => h( '', { style: activeFilters ? {} : { opacity: 0.5, pointerEvents: 'none' } }, - h(CopyToClipboardComponent, { value: location.href, id: 'filter' }, 'Copy Active Filters'), + h(CopyToClipboardComponent, { value: location.href, id: 'filters' }, 'Copy Active Filters'), ); /** @@ -164,7 +164,7 @@ const pasteButtonOption = (model) => { filteringModel.setFilterFromURL(true, url); }, disabled: !clipboardSupported, - id: 'paste-filter', + id: 'paste-filters', }, 'Paste filters'); }; diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js index f1af536081..86c030cc81 100644 --- a/test/public/components/filtersPopoverPanel.test.js +++ b/test/public/components/filtersPopoverPanel.test.js @@ -30,7 +30,7 @@ module.exports = () => { const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; await page.goto(url, { waitUntil: 'load' }); await takeScreenshot(page, 'test'); - await pressElement(page, '#copy-filter', true); + await pressElement(page, '#copy-filters', true); const clipboardContents = await page.evaluate(async () => decodeURI(await navigator.clipboard.readText())); expect(clipboardContents).to.equal(url); @@ -40,7 +40,7 @@ module.exports = () => { const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '#paste-filter', true); + await pressElement(page, '#paste-filters', true); const actualUrl = page.url(); expect(actualUrl).to.equal(url); diff --git a/test/public/components/warnings.test.js b/test/public/components/warnings.test.js index d19ef67295..ebeeceae2f 100644 --- a/test/public/components/warnings.test.js +++ b/test/public/components/warnings.test.js @@ -61,7 +61,7 @@ module.exports = () => { await goToPage(page, 'log-overview'); await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '.dropdown #paste-filter', true); + await pressElement(page, '.dropdown #paste-filters', true); const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); expect(warningText).to.equal('Unparseable URL:\nURL could not be parsed. URL: unparsable url'); @@ -72,7 +72,7 @@ module.exports = () => { await goToPage(page, 'log-overview'); await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '.dropdown #paste-filter', true); + await pressElement(page, '.dropdown #paste-filters', true); const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); expect(warningText).to.equal('Page-Filter mismatch:\nThe filters provided were meant for lhc-period-overview'); From eb1954795ccfe0f4f754c44b2460a810115fc7e2 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 18 Jun 2026 11:06:19 +0200 Subject: [PATCH 19/20] fix linting issues --- test/public/components/filtersPopoverPanel.test.js | 2 +- test/public/components/warnings.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js index 86c030cc81..53df9e8ee3 100644 --- a/test/public/components/filtersPopoverPanel.test.js +++ b/test/public/components/filtersPopoverPanel.test.js @@ -12,7 +12,7 @@ */ const { expect } = require('chai'); -const { defaultBefore, defaultAfter, pressElement, takeScreenshot, expectInputValue, goToPage, getInnerText } = require('../defaults.js'); +const { defaultBefore, defaultAfter, pressElement, takeScreenshot, expectInputValue } = require('../defaults.js'); module.exports = () => { let page; diff --git a/test/public/components/warnings.test.js b/test/public/components/warnings.test.js index ebeeceae2f..b330186fe7 100644 --- a/test/public/components/warnings.test.js +++ b/test/public/components/warnings.test.js @@ -18,12 +18,12 @@ const { getInnerText, pressElement, goToPage, - takeScreenshot, } = require('../defaults.js'); module.exports = () => { let page; let browser; + let url; before(async () => { [page, browser, url] = await defaultBefore(page, browser); From 68e0918b9edc69e00109718b97a1f857c38d9152 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 18 Jun 2026 11:21:55 +0200 Subject: [PATCH 20/20] chore: add context variable --- test/public/components/warnings.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/public/components/warnings.test.js b/test/public/components/warnings.test.js index b330186fe7..5fb32457f3 100644 --- a/test/public/components/warnings.test.js +++ b/test/public/components/warnings.test.js @@ -24,6 +24,7 @@ module.exports = () => { let page; let browser; let url; + let context; before(async () => { [page, browser, url] = await defaultBefore(page, browser);