From 587473afda38376d29e24ab90e873f9baf9176ed Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 23 Apr 2026 12:30:38 +0200 Subject: [PATCH 1/6] feat: add normalize setter to SelectionModel and FilteringModel --- .../components/Filters/common/FilterModel.js | 11 +++++ .../common/selection/SelectionModel.js | 46 +++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index cc7badb53c..2aae7b1a10 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -57,6 +57,17 @@ export class FilterModel extends Observable { throw new Error('Abstract function call'); } + /** + * Sets filters from normalised values to submodels in needed. + * + * @param {string|number|object|string[]|number[]|null} _value The value used to set filters + * @return {void} the normalized value + * @abstract + */ + set normalized(_value) { + throw new Error('Abstract function call'); + } + /** * Returns the observable notified any time there is a visual change which has no impact on the actual filter value * diff --git a/lib/public/components/common/selection/SelectionModel.js b/lib/public/components/common/selection/SelectionModel.js index 419f7c5a12..2a5f623e80 100644 --- a/lib/public/components/common/selection/SelectionModel.js +++ b/lib/public/components/common/selection/SelectionModel.js @@ -42,6 +42,12 @@ export class SelectionModel extends Observable { super(); const { availableOptions = [], defaultSelection = [], multiple = true, allowEmpty = true } = configuration || {}; + /** + * @type {SelectionOption[]} + * @protected + */ + this._selectionBacklog = []; + /** * @type {RemoteData|SelectionOption[]} * @protected @@ -152,11 +158,12 @@ export class SelectionModel extends Observable { if (typeof option === 'string' || typeof option === 'number') { if (this._availableOptions instanceof RemoteData) { selectOption = this._availableOptions.match({ - Success: (options) => options.find(({ value }) => value === option), + // String comparison in needed since select may use url parameters which are always strings, causing false negatives + Success: (options) => options.find(({ value }) => String(value) === String(option)), Other: () => null, }); } else { - selectOption = this._availableOptions.find(({ value }) => value === option); + selectOption = this._availableOptions.find(({ value }) => String(value) === String(option)); } } else { selectOption = option; @@ -243,7 +250,7 @@ export class SelectionModel extends Observable { } /** - * Defines the list of available options + * Defines the list of available options and if there is a selection backlog, these will be applied * * @param {RemoteData|SelectionOption[]} availableOptions the new available options * @return {void} @@ -251,6 +258,11 @@ export class SelectionModel extends Observable { setAvailableOptions(availableOptions) { this._availableOptions = availableOptions; this.visualChange$.notify(); + + if (this._selectionBacklog.length) { + this.selectedOptions = this._selectionBacklog; + this._selectionBacklog = []; + } } /** @@ -315,12 +327,19 @@ export class SelectionModel extends Observable { } /** - * Define (overrides) the list of currently selected options + * Define (overrides) the list of currently selected options. + * Invalid selection options are excluded * * @param {SelectionOption[]} selected the list of selected options */ set selectedOptions(selected) { - this._selectedOptions = selected; + let { options } = this; + + if (this.options instanceof RemoteData) { + options = options.isSuccess() ? options.payload : []; + } + + this._selectedOptions = selected.filter((option) => options.some(({ value }) => String(value) === String(option.value))); } /** @@ -332,6 +351,23 @@ export class SelectionModel extends Observable { return this._defaultSelection; } + /** + * Sets selected options based on a comma-seperated string. + * Accounts for the options being either RemoteData or an array. + * + * @return {string} + */ + set normalized(value) { + const options = value.split(',').map((option) => ({ value: option })); + const postponeSelection = this.options instanceof RemoteData || ! this.options?.length; + + if (postponeSelection) { + this._selectionBacklog = options; + } else { + this.selectedOptions = options; + } + } + /** * Returns the normalized value of the selection * From 5a0e8dca42a8bc928f954c2c8b41e75037ddd6c1 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 23 Apr 2026 13:28:23 +0200 Subject: [PATCH 2/6] chore: add mundain normalized setters --- .../RunsFilter/DetectorsFilterModel.js | 8 ++++++++ .../RunsFilter/EorReasonFilterModel.js | 9 +++++++++ .../Filters/RunsFilter/GaqFilterModel.js | 8 ++++++++ .../Filters/RunsFilter/TimeRangeFilter.js | 7 +++++++ .../Filters/common/TagFilterModel.js | 10 +++++++++- .../filters/NumericalComparisonFilterModel.js | 12 +++++++++-- .../common/filters/ProcessedTextInputModel.js | 20 +++++++++++++++++++ .../common/filters/RawTextFilterModel.js | 7 +++++++ .../filters/TextComparisonFilterModel.js | 12 +++++++++-- .../common/filters/TextTokensFilterModel.js | 7 +++++++ .../common/filters/TimeRangeInputModel.js | 8 ++++++++ 11 files changed, 103 insertions(+), 5 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js index 432ecc58df..0f19630570 100644 --- a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js @@ -67,6 +67,14 @@ export class DetectorsFilterModel extends FilterModel { return normalized; } + /** + * @inheritDoc + */ + set normalized({ operator, values }) { + this._combinationOperatorModel = operator; + this._dropdownModel.normalized = values; + } + /** * Return true if the current combination operator is none * diff --git a/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js index f57c810cce..b3b1e649bf 100644 --- a/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js @@ -66,6 +66,15 @@ export class EorReasonFilterModel extends FilterModel { return ret; } + /** + * @inheritDoc + */ + set normalized({ category, title, description }) { + this._category = category; + this._title = title; + this._description = description; + } + /** * Returns the EOR reason filter category * diff --git a/lib/public/components/Filters/RunsFilter/GaqFilterModel.js b/lib/public/components/Filters/RunsFilter/GaqFilterModel.js index ef2b6be122..6669a22ee1 100644 --- a/lib/public/components/Filters/RunsFilter/GaqFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/GaqFilterModel.js @@ -69,6 +69,14 @@ export class GaqFilterModel extends FilterModel { return normalized; } + /** + * @inheritDoc + */ + set normalized({ notBadFraction, mcReproducibleAsNotBad }) { + this._notBadFraction.normalized = notBadFraction; + this._mcReproducibleAsNotBad.normalized = mcReproducibleAsNotBad; + } + /** * Return the underlying notBadFraction model * diff --git a/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js index e765137afa..296e4f4753 100644 --- a/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js +++ b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js @@ -45,6 +45,13 @@ export class TimeRangeFilterModel extends FilterModel { return normalized; } + /** + * @inheritDoc + */ + set normalized({ from, to }) { + this._timeRangeInputModel.normalized = { from, to }; + } + /** * Return the underlying time range input model * diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index c3ce81e09f..489dc05709 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -58,11 +58,19 @@ export class TagFilterModel extends FilterModel { */ get normalized() { return { - values: this.selected.join(), + values: this._selectionModel.normalized, operation: this.combinationOperator, }; } + /** + * @inheritDoc + */ + set normalized({ values, operation }) { + this._selectionModel.normalized = values; + this._combinationOperatorModel.normalized = operation; + } + /** * Return the model handling tag selection state * diff --git a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js index ee00126389..95b648e762 100644 --- a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js @@ -82,11 +82,19 @@ export class NumericalComparisonFilterModel extends FilterModel { */ get normalized() { return { - operator: this._operatorSelectionModel.current, - limit: this._operandInputModel.value, + operator: this._operatorSelectionModel.normalized, + limit: this._operandInputModel.normalized, }; } + /** + * @inheritDoc + */ + set normalized({ operator, limit }) { + this._operatorSelectionModel.normalized = operator; + this._operandInputModel.normalized = limit; + } + /** * @inheritDoc */ diff --git a/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js b/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js index 9e46fe95b5..effa03a166 100644 --- a/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js +++ b/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js @@ -98,6 +98,26 @@ export class ProcessedTextInputModel extends Observable { this._value = null; } + /** + * Returns the normalized value of the filter, that can be used as URL parameter + * @returns {string} + */ + get normalized() { + return this._value; + } + + /** + * Sets filters from normalised values. + * + * @param {string} value The value used to set filters + * @return {void} + * @abstract + */ + set normalized(value) { + this._value = value; + this._raw = value; + } + /** * Return the visual change observable * diff --git a/lib/public/components/Filters/common/filters/RawTextFilterModel.js b/lib/public/components/Filters/common/filters/RawTextFilterModel.js index f996b7b976..d156c86e10 100644 --- a/lib/public/components/Filters/common/filters/RawTextFilterModel.js +++ b/lib/public/components/Filters/common/filters/RawTextFilterModel.js @@ -35,6 +35,13 @@ export class RawTextFilterModel extends FilterModel { return this._value; } + /** + * @inheritDoc + */ + set normalized(value) { + this._value = value; + } + /** * Return the filter current value * diff --git a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js index b6510f8fae..7f843d6295 100644 --- a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js @@ -64,11 +64,19 @@ export class TextComparisonFilterModel extends FilterModel { */ get normalized() { return { - operator: this._operatorSelectionModel.current, - limit: this._operandInputModel.value, + operator: this._operatorSelectionModel.normalized, + limit: this._operandInputModel.normalized, }; } + /** + * @inheritDoc + */ + set normalized({ operator, limit }) { + this._operatorSelectionModel.normalized = operator; + this._operandInputModel.normalized = limit; + } + /** * @inheritDoc */ diff --git a/lib/public/components/Filters/common/filters/TextTokensFilterModel.js b/lib/public/components/Filters/common/filters/TextTokensFilterModel.js index 60e192febe..8c838e5abf 100644 --- a/lib/public/components/Filters/common/filters/TextTokensFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextTokensFilterModel.js @@ -78,6 +78,13 @@ export class TextTokensFilterModel extends FilterModel { .filter((token) => token.length > 0); } + /** + * @inheritDoc + */ + set normalized(value) { + this._raw = value.join(TOKENS_DELIMITER); + } + /** * Returns the observable notified any time there is a visual change which has no impact on the actual filter value * @return {Observable} the observable diff --git a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js index 54ee3fe7b0..091b491171 100644 --- a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js +++ b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js @@ -142,6 +142,14 @@ export class TimeRangeInputModel extends FilterModel { }; } + /** + * @inheritDoc + */ + set normalized({ from, to }) { + this._fromTimeInputModel.setValue(parseInt(from, 10), false); + this._toTimeInputModel.setValue(parseInt(to, 10), false); + } + /** * States if the filter value is valid * From 3732a8ba4899a7c2bda63353aabc1a57898c5e61 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 23 Apr 2026 12:54:16 +0200 Subject: [PATCH 3/6] chore: remove all mentions of selectionFilterModel and replace it with SelectionModel --- .../LhcFillsFilter/BeamTypeFilterModel.js | 6 +- .../Filters/LhcFillsFilter/beamTypeFilter.js | 6 +- .../RunsFilter/RunDefinitionFilterModel.js | 10 +-- .../Filters/RunsFilter/runDefinitionFilter.js | 5 +- .../common/filters/SelectionFilterModel.js | 63 ------------------- .../ActiveColumns/dataPassesActiveColumns.js | 2 +- .../DataPasses/DataPassesOverviewModel.js | 4 +- .../environmentsActiveColumns.js | 2 +- .../Overview/EnvironmentOverviewModel.js | 4 +- .../Runs/ActiveColumns/runsActiveColumns.js | 2 +- .../views/Runs/Overview/RunsOverviewModel.js | 3 +- 11 files changed, 18 insertions(+), 89 deletions(-) delete mode 100644 lib/public/components/Filters/common/filters/SelectionFilterModel.js diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js index 18be7af40d..fc0964da04 100644 --- a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js @@ -12,12 +12,12 @@ */ import { beamTypesProvider } from '../../../services/beamTypes/beamTypesProvider.js'; -import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; +import { SelectionModel } from '../../common/selection/SelectionModel.js'; /** * Beam type filter model */ -export class BeamTypeFilterModel extends SelectionFilterModel { +export class BeamTypeFilterModel extends SelectionModel { /** * Constructor */ @@ -28,7 +28,7 @@ export class BeamTypeFilterModel extends SelectionFilterModel { beamTypesProvider.items$.getCurrent().apply({ Success: (types) => { const beamTypes = types.map((type) => ({ value: type.beam_type })); - this._selectionModel.setAvailableOptions(beamTypes); + this.setAvailableOptions(beamTypes); }, }); }); diff --git a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js index 7872734704..83f1487922 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js @@ -19,8 +19,4 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; * @param {BeamTypeFilterModel} beamTypeFilterModel beamTypeFilterModel * @return {Component} the filter */ -export const beamTypeFilter = (beamTypeFilterModel) => - checkboxes( - beamTypeFilterModel.selectionModel, - { selector: 'beam-types' }, - ); +export const beamTypeFilter = (beamTypeFilterModel) => checkboxes(beamTypeFilterModel, { selector: 'beam-types' }); diff --git a/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js index 8fb9347735..b0011177bf 100644 --- a/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js @@ -1,10 +1,10 @@ import { RUN_DEFINITIONS, RunDefinition } from '../../../domain/enums/RunDefinition.js'; -import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; +import { SelectionModel } from '../../common/selection/SelectionModel.js'; /** * Run definition filter model */ -export class RunDefinitionFilterModel extends SelectionFilterModel { +export class RunDefinitionFilterModel extends SelectionModel { /** * Constructor */ @@ -18,7 +18,7 @@ export class RunDefinitionFilterModel extends SelectionFilterModel { * @return {boolean} true if filter is physics only */ isPhysicsOnly() { - const selectedOptions = this._selectionModel.selected; + const selectedOptions = this.selected; return selectedOptions.length === 1 && selectedOptions[0] === RunDefinition.Physics; } @@ -29,8 +29,8 @@ export class RunDefinitionFilterModel extends SelectionFilterModel { */ setPhysicsOnly() { if (!this.isPhysicsOnly()) { - this._selectionModel.selectedOptions = []; - this._selectionModel.select(RunDefinition.Physics); + this.selectedOptions = []; + this.select(RunDefinition.Physics); this.notify(); } } diff --git a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js index d53ba62428..2a799ff675 100644 --- a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js +++ b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js @@ -19,7 +19,4 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; * @param {RunDefinitionFilterModel} runDefinitionFilterModel run definition filter model * @return {Component} the filter */ -export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxes( - runDefinitionFilterModel.selectionModel, - { selector: 'run-definition' }, -); +export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxes(runDefinitionFilterModel, { selector: 'run-definition' }); diff --git a/lib/public/components/Filters/common/filters/SelectionFilterModel.js b/lib/public/components/Filters/common/filters/SelectionFilterModel.js deleted file mode 100644 index 4bb602d7aa..0000000000 --- a/lib/public/components/Filters/common/filters/SelectionFilterModel.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @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. - */ - -import { FilterModel } from '../FilterModel.js'; -import { SelectionModel } from '../../../common/selection/SelectionModel.js'; - -/** - * Filter model based on a selection model - */ -export class SelectionFilterModel extends FilterModel { - /** - * Constructor - * - * @param {object} [configuration] the selection filter configuration - * @param {SelectionOption[]} [configuration.availableOptions=[]] the list of available options - */ - constructor(configuration) { - super(); - - this._selectionModel = new SelectionModel({ availableOptions: configuration.availableOptions }); - this._selectionModel.bubbleTo(this); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionModel.isEmpty; - } - - /** - * @inheritDoc - */ - get normalized() { - return this._selectionModel.selected.join(','); - } - - /** - * Return the underlying selection model - * - * @return {SelectionModel} the underlying selection model - */ - get selectionModel() { - return this._selectionModel; - } -} diff --git a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js index ca3ed1b38d..7fad95a33b 100644 --- a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js +++ b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js @@ -102,7 +102,7 @@ export const dataPassesActiveColumns = { nonPhysicsProductions: { name: 'Include nonphysics productions', - filter: (filteringModel) => checkboxes(filteringModel.get('include[byName]').selectionModel), + filter: (filteringModel) => checkboxes(filteringModel.get('include[byName]')), visible: false, }, }; diff --git a/lib/public/views/DataPasses/DataPassesOverviewModel.js b/lib/public/views/DataPasses/DataPassesOverviewModel.js index d30d5435ed..51f3cdb4e8 100644 --- a/lib/public/views/DataPasses/DataPassesOverviewModel.js +++ b/lib/public/views/DataPasses/DataPassesOverviewModel.js @@ -10,8 +10,8 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ +import { SelectionModel } from '../../components/common/selection/SelectionModel.js'; import { FilteringModel } from '../../components/Filters/common/FilteringModel.js'; -import { SelectionFilterModel } from '../../components/Filters/common/filters/SelectionFilterModel.js'; import { TextTokensFilterModel } from '../../components/Filters/common/filters/TextTokensFilterModel.js'; import { NON_PHYSICS_PRODUCTIONS_NAMES_WORDS } from '../../domain/enums/NonPhysicsProductionsNamesWords.js'; import { OverviewPageModel } from '../../models/OverviewModel.js'; @@ -31,7 +31,7 @@ export class DataPassesOverviewModel extends OverviewPageModel { router, { names: new TextTokensFilterModel(), - 'include[byName]': new SelectionFilterModel({ + 'include[byName]': new SelectionModel({ availableOptions: NON_PHYSICS_PRODUCTIONS_NAMES_WORDS.map((word) => ({ label: word.toUpperCase(), value: word })), }), }, diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index 37121a34a3..1226d7f7cb 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -117,7 +117,7 @@ export const environmentsActiveColumns = { * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus').selectionModel), + filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), }, historyItems: { name: h('.flex-row.g2.items-center', ['Status History', infoTooltip(environmentStatusHistoryLegendComponent())]), diff --git a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js index 64893e72af..3c9f176f12 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -15,11 +15,11 @@ import { buildUrl } from '/js/src/index.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; -import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { debounce } from '../../../utilities/debounce.js'; import { coloredEnvironmentStatusComponent } from '../ColoredEnvironmentStatusComponent.js'; import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; +import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; /** * Environment overview page model @@ -39,7 +39,7 @@ export class EnvironmentOverviewModel extends OverviewPageModel { created: new TimeRangeInputModel(), runNumbers: new RawTextFilterModel(), statusHistory: new RawTextFilterModel(), - currentStatus: new SelectionFilterModel({ + currentStatus: new SelectionModel({ availableOptions: Object.keys(StatusAcronym).map((status) => ({ value: status, label: coloredEnvironmentStatusComponent(status), diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 382a29f02b..41e61e2281 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -441,7 +441,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run quality filter component */ - filter: (runsOverviewModel) => checkboxes(runsOverviewModel.filteringModel.get('runQualities').selectionModel), + filter: (runsOverviewModel) => checkboxes(runsOverviewModel.filteringModel.get('runQualities')), }, nDetectors: { name: 'DETs #', diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index c27dbc5e49..47a12044af 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -31,7 +31,6 @@ import { magnetsCurrentLevelsProvider } from '../../../services/magnets/magnetsC import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { RunDefinitionFilterModel } from '../../../components/Filters/RunsFilter/RunDefinitionFilterModel.js'; import { RUN_QUALITIES } from '../../../domain/enums/RunQualities.js'; -import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; import { DataExportModel } from '../../../models/DataExportModel.js'; import { runsActiveColumns as dataExportConfiguration } from '../ActiveColumns/runsActiveColumns.js'; import { BeamModeFilterModel } from '../../../components/Filters/RunsFilter/BeamModeFilterModel.js'; @@ -76,7 +75,7 @@ export class RunsOverviewModel extends OverviewPageModel { environmentIds: new RawTextFilterModel(), runTypes: new RunTypesFilterModel(runTypesProvider.items$), beamModes: new BeamModeFilterModel(beamModesProvider.items$), - runQualities: new SelectionFilterModel({ + runQualities: new SelectionModel({ availableOptions: RUN_QUALITIES.map((quality) => ({ label: quality.toUpperCase(), value: quality, From c654de8e9854aa9c773f350a717f47d22210fea5 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 23 Apr 2026 13:10:52 +0200 Subject: [PATCH 4/6] make MagnetsfilteringModel extend RemoteDataSelectionDropdown --- .../RunsFilter/MagnetsFilteringModel.js | 75 ++++++------------- .../Runs/ActiveColumns/runsActiveColumns.js | 6 +- 2 files changed, 25 insertions(+), 56 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js index 015f991286..f0e8b158b3 100644 --- a/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js +++ b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js @@ -11,21 +11,32 @@ * or submit itself to any jurisdiction. */ -import { FilterModel } from '../common/FilterModel.js'; import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; /** * Return the option value corresponding to a given magnets current level * * @param {MagnetsCurrentLevels} currentLevels the current levels - * @return {string} the option's value + * @return {object} the option's value */ -const magnetsCurrentLevelsToOptionValue = ({ l3, dipole }) => `${l3}kA/${dipole}kA`; +const magnetsCurrentLevelsToKey = ({ l3, dipole }) => ({ value: `${l3}kA/${dipole}kA` }); + +/** + * Return the magnets current lever based on a key string + * + * @param {object} option string containing the current levels + * @param {string} option.value string containing the current levels + * @return {MagnetsCurrentLevels} + */ +const keyToMagnetsCurrentLevels = (value) => { + const [l3, dipole] = value.split('/').map((str) => parseFloat(str.slice(0, -2))); + return { l3, dipole }; +}; /** * AliceL3AndDipoleFilteringModel */ -export class MagnetsFilteringModel extends FilterModel { +export class MagnetsFilteringModel extends ObservableBasedSelectionDropdownModel { /** * Constructor * @@ -33,64 +44,24 @@ export class MagnetsFilteringModel extends FilterModel { * levels */ constructor(magnetsCurrentLevels$) { - super(); - this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel( - magnetsCurrentLevels$, - (magnetsCurrentLevels) => ({ value: magnetsCurrentLevelsToOptionValue(magnetsCurrentLevels) }), - { multiple: false }, - ); - this._addSubmodel(this._selectionDropdownModel); - - this._valueToFilteringParamsMap = new Map(); - magnetsCurrentLevels$.observe(() => { - magnetsCurrentLevels$.getCurrent().match({ - - /** - * Fill map indexing current level by their corresponding value - * - * @param {MagnetsCurrentLevels[]} currentLevels the current levels to map - * @return {void} - */ - Success: (currentLevels) => { - this._valueToFilteringParamsMap = new Map(currentLevels.map(({ l3, dipole }) => [ - magnetsCurrentLevelsToOptionValue({ l3, dipole }), - { l3, dipole }, - ])); - }, - Other: () => { - this._valueToFilteringParamsMap = new Map(); - }, - }); - }); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionDropdownModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionDropdownModel.isEmpty; + super(magnetsCurrentLevels$, magnetsCurrentLevelsToKey, { multiple: false }); } /** * @inheritDoc */ get normalized() { - return this._valueToFilteringParamsMap.get(this._selectionDropdownModel.selected[0]) ?? null; + const [selectedOption] = this.selected; + return keyToMagnetsCurrentLevels(selectedOption); } /** - * Return the underlying selection dropdown model + * Sets selected options based on a comma-seperated string. + * Accounts for the options being either RemoteData or an array. * - * @return {SelectionDropdownModel} the dropdown model + * @return {string} */ - get selectionDropdownModel() { - return this._selectionDropdownModel; + set normalized(value) { + this.normalized = magnetsCurrentLevelsToKey(value).value; } } diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 41e61e2281..d71fd6e8bf 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -667,10 +667,8 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run types filter component */ - filter: (runsOverviewModel) => selectionDropdown( - runsOverviewModel.filteringModel.get('magnets').selectionDropdownModel, - { selectorPrefix: 'l3-dipole-current' }, - ), + filter: (runsOverviewModel) => + selectionDropdown(runsOverviewModel.filteringModel.get('magnets'), { selectorPrefix: 'l3-dipole-current' }), profiles: ['runsPerLhcPeriod', 'runsPerDataPass', 'runsPerSimulationPass', profiles.none], }, From 4abf136119bac4a622bd42727449e0bd5caa03af Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 23 Apr 2026 13:23:02 +0200 Subject: [PATCH 5/6] feat: change all filters that have only ObservableBasedSelectionDropdownModel as submodels to extend it instead --- .../Filters/RunsFilter/BeamModeFilterModel.js | 37 +------------------ .../runTypes/RunTypesFilterModel.js | 28 +------------- .../Runs/ActiveColumns/runsActiveColumns.js | 8 +--- 3 files changed, 6 insertions(+), 67 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js index 0704fc684d..626644ae88 100644 --- a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -12,50 +12,17 @@ */ import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; -import { FilterModel } from '../common/FilterModel.js'; /** * Beam mode filter model */ -export class BeamModeFilterModel extends FilterModel { +export class BeamModeFilterModel extends ObservableBasedSelectionDropdownModel { /** * Constructor * * @param {ObservableData>} beamModes$ observable remote data of objects representing beam modes */ constructor(beamModes$) { - super(); - this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(beamModes$, ({ name }) => ({ value: name })); - this._addSubmodel(this._selectionDropdownModel); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionDropdownModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionDropdownModel.isEmpty; - } - - /** - * Return the underlying dropdown model - * - * @return {ObservableDropDownModel} the underlying dropdown model - */ - get selectionDropdownModel() { - return this._selectionDropdownModel; - } - - /** - * @inheritDoc - */ - get normalized() { - return this._selectionDropdownModel.selected; + super(beamModes$, ({ name }) => ({ value: name })); } } diff --git a/lib/public/components/runTypes/RunTypesFilterModel.js b/lib/public/components/runTypes/RunTypesFilterModel.js index 60a923cbc6..31e3a6d5f8 100644 --- a/lib/public/components/runTypes/RunTypesFilterModel.js +++ b/lib/public/components/runTypes/RunTypesFilterModel.js @@ -12,43 +12,19 @@ */ import { runTypeToOption } from './runTypeToOption.js'; -import { FilterModel } from '../Filters/common/FilterModel.js'; import { ObservableBasedSelectionDropdownModel } from '../detector/ObservableBasedSelectionDropdownModel.js'; /** * Model storing state of a selection of run types picked from the list of all the existing run types */ -export class RunTypesFilterModel extends FilterModel { +export class RunTypesFilterModel extends ObservableBasedSelectionDropdownModel { /** * Constructor * * @param {ObservableData>} runTypes$ observable remote data of run types list */ constructor(runTypes$) { - super(); - this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(runTypes$, runTypeToOption); - this._addSubmodel(this._selectionDropdownModel); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionDropdownModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionDropdownModel.isEmpty; - } - - /** - * @inheritDoc - */ - get normalized() { - return this._selectionDropdownModel.selected; + super(runTypes$, runTypeToOption); } /** diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index d71fd6e8bf..9992a93407 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -158,8 +158,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the beam modes filter component */ - filter: (runsOverviewModel) => - selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropdownModel, { selectorPrefix: 'beam-mode' }), + filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('beamModes'), { selectorPrefix: 'beam-mode' }), }, fillNumber: { name: 'Fill No.', @@ -407,10 +406,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run types filter component */ - filter: (runsOverviewModel) => selectionDropdown( - runsOverviewModel.filteringModel.get('runTypes').selectionDropdownModel, - { selectorPrefix: 'run-types' }, - ), + filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('runTypes'), { selectorPrefix: 'run-types' }), }, runQuality: { name: 'Quality', From 30daecdead2e4bb487b41ea8c4ae0a632f85d390 Mon Sep 17 00:00:00 2001 From: NarrowsProjects Date: Thu, 23 Apr 2026 17:32:25 +0200 Subject: [PATCH 6/6] feat: handle late filter insertion for multiCompositionFilter --- .../RunsFilter/MultiCompositionFilterModel.js | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js b/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js index 0b278e1d4b..3c83ef09e0 100644 --- a/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js @@ -28,6 +28,7 @@ export class MultiCompositionFilterModel extends FilterModel { * @type {Object} */ this._filters = {}; + this._normalizeBacklog = {}; Object.entries(filters).forEach(([key, filter]) => this.putFilter(key, filter)); } @@ -39,12 +40,13 @@ export class MultiCompositionFilterModel extends FilterModel { * @return {FilterModel} the subfilter */ putFilter(key, filterModel) { - if (key in this._filters) { + if (this.isInComposition(key)) { return; } this._filters[key] = filterModel; this._addSubmodel(filterModel); + this._applyBacklogEntry(key, filterModel); } /** @@ -54,7 +56,7 @@ export class MultiCompositionFilterModel extends FilterModel { * @param {FilterModel} filter the the subfilter */ getFilter(key) { - if (!(key in this._filters)) { + if (!this.isInComposition(key)) { throw new Error(`No filter found with key ${key}`); } @@ -89,4 +91,40 @@ export class MultiCompositionFilterModel extends FilterModel { return normalized; } + + /** + * Checks whether a filter with the given key exists in the composition. + * + * @param {string|number} key The key of the subfilter to check. + * @returns {boolean} `true` if a filter with the given key exists, otherwise `false`. + */ + isInComposition(key) { + return key in this._filters; + } + + /** + * Apply any queued normalization value for a newly added filter + * + * @param {string|number} key + * @param {FilterModel} filterModel + */ + _applyBacklogEntry(key, filterModel) { + if (key in this._normalizeBacklog) { + filterModel.normalized = this._normalizeBacklog[key]; + delete this._normalizeBacklog[key]; + } + } + + /** + * @inheritDoc + */ + set normalized(filters) { + for (const [key, value] of Object.entries(filters)) { + if (this.isInComposition(key)) { + this._filters[key].normalized = value; + } else { + this._normalizeBacklog[key] = value; + } + } + } }