diff --git a/lib/public/components/Filters/RunsFilter/environmentId.js b/lib/public/components/Filters/RunsFilter/environmentId.js deleted file mode 100644 index 5ec81b42c7..0000000000 --- a/lib/public/components/Filters/RunsFilter/environmentId.js +++ /dev/null @@ -1,31 +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 { h } from '/js/src/index.js'; - -/** - * Returns a filter component to filter on environment Ids, either a coma separated list of specific ids or a substring - * search - * @param {RunsOverviewModel} runModel The global model object - * @return {vnode} A text box that allows the user to enter an environment substring to match against all runs or a - * list of environment ids - */ -const environmentIdsFilter = (runModel) => h('input.w-75.mt1', { - type: 'text', - id: 'environmentIds', - value: runModel.getEnvFilter(), - placeholder: 'e.g. Dxi029djX, TDI59So3d...', - oninput: (e) => runModel.setEnvironmentIdsFilter(e.target.value), -}, ''); - -export default environmentIdsFilter; diff --git a/lib/public/components/Filters/RunsFilter/nDetectors.js b/lib/public/components/Filters/RunsFilter/nDetectors.js deleted file mode 100644 index c5941f1608..0000000000 --- a/lib/public/components/Filters/RunsFilter/nDetectors.js +++ /dev/null @@ -1,30 +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 { amountFilter } from '../common/filters/amountFilter.js'; - -/** - * Returns the author filter component - * @param {RunsOverviewModel} runModel the runs model object - * @return {vnode} A text box that lets the user look for logs with a specific author - */ -const nDetectorsFilter = (runModel) => amountFilter(runModel.getNDetectorsFilter(), (filter) => runModel.setNDetectorsFilter(filter), { - operatorAttributes: { - id: 'nDetectors-operator', - }, - limitAttributes: { - id: 'nDetectors-limit', - }, -}); - -export default nDetectorsFilter; diff --git a/lib/public/components/Filters/RunsFilter/nEpns.js b/lib/public/components/Filters/RunsFilter/nEpns.js deleted file mode 100644 index 069a1dbaa8..0000000000 --- a/lib/public/components/Filters/RunsFilter/nEpns.js +++ /dev/null @@ -1,36 +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 { amountFilter } from '../common/filters/amountFilter.js'; - -/** - * Returns the nEpns filter component - * @param {RunsOverviewModel} runModel The runs model object - * @return {vnode} A text box and operator that lets the user look for logs with a specific number of EPNs - */ -const nEpnsFilter = (runModel) => amountFilter( - runModel.nEpnsFilter, - (filter) => { - runModel.nEpnsFilter = filter; - }, - { - operatorAttributes: { - id: 'nEpns-operator', - }, - limitAttributes: { - id: 'nEpns-limit', - }, - }, -); - -export default nEpnsFilter; diff --git a/lib/public/components/Filters/RunsFilter/nFlps.js b/lib/public/components/Filters/RunsFilter/nFlps.js deleted file mode 100644 index 8144d85a96..0000000000 --- a/lib/public/components/Filters/RunsFilter/nFlps.js +++ /dev/null @@ -1,30 +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 { amountFilter } from '../common/filters/amountFilter.js'; - -/** - * Returns the author filter component - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} A text box that lets the user look for logs with a specific author - */ -const nFlpsFilter = (runModel) => amountFilter(runModel.getNFlpsFilter(), (filter) => runModel.setNFlpsFilter(filter), { - operatorAttributes: { - id: 'nFlps-operator', - }, - limitAttributes: { - id: 'nFlps-limit', - }, -}); - -export default nFlpsFilter; diff --git a/lib/public/components/Filters/RunsFilter/odcTopologyFullName.js b/lib/public/components/Filters/RunsFilter/odcTopologyFullName.js deleted file mode 100644 index fc606874ea..0000000000 --- a/lib/public/components/Filters/RunsFilter/odcTopologyFullName.js +++ /dev/null @@ -1,30 +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 { h } from '/js/src/index.js'; - -/** - * Returns the title filter component - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} A text box that allows the user to enter a title substring to match against all logs - */ -const odcTopologyFullName = (runModel) => h('input.w-75.mt1', { - type: 'text', - id: 'Topology Full Name', - value: runModel.odcTopologyFullNameFilter, - oninput: (e) => { - runModel.odcTopologyFullNameFilter = e.target.value; - }, -}, ''); - -export default odcTopologyFullName; diff --git a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js index d53ba62428..3ce13c83cf 100644 --- a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js +++ b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { checkboxes } from '../common/filters/checkboxFilter.js'; +import { expandedSelectionFilter } from '../common/filters/checkboxFilter.js'; /** * Renders a list of checkboxes that lets the user look for runs with specific definition @@ -19,7 +19,7 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; * @param {RunDefinitionFilterModel} runDefinitionFilterModel run definition filter model * @return {Component} the filter */ -export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxes( +export const runDefinitionFilter = (runDefinitionFilterModel) => expandedSelectionFilter( runDefinitionFilterModel.selectionModel, { selector: 'run-definition' }, ); diff --git a/lib/public/components/Filters/RunsFilter/runQuality.js b/lib/public/components/Filters/RunsFilter/runQuality.js deleted file mode 100644 index 4596fea2c8..0000000000 --- a/lib/public/components/Filters/RunsFilter/runQuality.js +++ /dev/null @@ -1,29 +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 { checkboxFilter } from '../common/filters/checkboxFilter.js'; -import { RUN_QUALITIES } from '../../../domain/enums/RunQualities.js'; - -/** - * Returns a panel to be used by user to filter runs by quality - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} A text box that allows the user to enter a title substring to match against all logs - */ -const runQualityFilter = (runModel) => checkboxFilter( - 'runQuality', - RUN_QUALITIES, - (runQuality) => runModel.isRunQualityInFilter(runQuality), - (e, runQuality) => e.target.checked ? runModel.addRunQualityFilter(runQuality) : runModel.removeRunQualityFilter(runQuality), -); - -export default runQualityFilter; diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index cc7badb53c..4d3e38c9e2 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -78,6 +78,6 @@ export class FilterModel extends Observable { */ _addSubmodel(subModel) { subModel.bubbleTo(this); - subModel?.visualChange$.bubbleTo(this._visualChange$); + subModel.visualChange$?.bubbleTo(this._visualChange$); } } diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index 35f0a5975e..cb03f23d3c 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -23,7 +23,7 @@ export class TagFilterModel extends FilterModel { * Constructor * * @param {ObservableData>} tags$ observable remote data of tags list - * @param {SelectionOption} [operators] optionally the list of available operators for the filter + * @param {SelectionOption[]} [operators] optionally the list of available operators for the filter * @constructor */ constructor(tags$, operators) { diff --git a/lib/public/components/Filters/common/filters/ComparisonSelectionModel.js b/lib/public/components/Filters/common/filters/ComparisonSelectionModel.js index 8cfbf6ac87..4f234a5498 100644 --- a/lib/public/components/Filters/common/filters/ComparisonSelectionModel.js +++ b/lib/public/components/Filters/common/filters/ComparisonSelectionModel.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { SelectionDropdownModel } from '../../../common/selection/dropdown/SelectionDropdownModel.js'; +import { SelectionModel } from '../../../common/selection/SelectionModel.js'; const numericalComparisonOptions = Object.freeze([ { value: '<' }, @@ -24,7 +24,7 @@ const numericalComparisonOptions = Object.freeze([ /** * Model storing state of a selection of comparison operator */ -export class ComparisonSelectionModel extends SelectionDropdownModel { +export class ComparisonSelectionModel extends SelectionModel { /** * Constructor */ diff --git a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js index 3303597f76..004298e37a 100644 --- a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js @@ -31,14 +31,6 @@ export class NumericalComparisonFilterModel extends FilterModel { const { useOperatorAsNormalizationKey, scale = 1, integer = false } = options || {}; this._useOperatorAsNormalizationKey = Boolean(useOperatorAsNormalizationKey); - this._operatorSelectionModel = new ComparisonSelectionModel(); - this._operatorSelectionModel.visualChange$.bubbleTo(this._visualChange$); - this._operatorSelectionModel.observe(() => { - if (!this._operandInputModel.isEmpty) { - this.notify(); - } - }); - this._operandInputModel = new ProcessedTextInputModel({ parse: (raw) => { const number = integer ? parseInt(raw, 10) : parseFloat(raw); @@ -48,8 +40,10 @@ export class NumericalComparisonFilterModel extends FilterModel { return number * scale; }, }); - this._operandInputModel.visualChange$.bubbleTo(this._visualChange$); - this._operandInputModel.bubbleTo(this); + this._addSubmodel(this._operandInputModel); + + this._operatorSelectionModel = new ComparisonSelectionModel(); + this._operatorSelectionModel.observe(() => this._operandInputModel.raw ? this.notify() : this._visualChange$.notify()); } /** diff --git a/lib/public/components/Filters/common/filters/SelectionFilterModel.js b/lib/public/components/Filters/common/filters/SelectionFilterModel.js index 2b9702fd5f..3b012ba61d 100644 --- a/lib/public/components/Filters/common/filters/SelectionFilterModel.js +++ b/lib/public/components/Filters/common/filters/SelectionFilterModel.js @@ -10,6 +10,7 @@ * 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'; diff --git a/lib/public/components/Filters/common/filters/checkboxFilter.js b/lib/public/components/Filters/common/filters/checkboxFilter.js index dcfcb4a95b..8b371807eb 100644 --- a/lib/public/components/Filters/common/filters/checkboxFilter.js +++ b/lib/public/components/Filters/common/filters/checkboxFilter.js @@ -13,6 +13,7 @@ */ import { h } from '/js/src/index.js'; +import { selectionOptions } from '../../../common/selection/selectionOptions.js'; /** * A general component for generating checkboxes. @@ -47,19 +48,7 @@ export const checkboxFilter = (name, values, isChecked, onChange, additionalProp * @param {string} [additionalProperties.selector] input identifiers prefix * @return {Component} filter component */ -export const checkboxes = (selectionModel, additionalProperties = {}) => { - const { selector = 'checkboxes' } = additionalProperties; - - return h('.flex-row.flex-wrap', selectionModel.options.map((option) => h('.form-check.flex-grow', [ - h('input.form-check-input', { - id: `${selector}-checkbox-${option.value}`, - type: 'checkbox', - checked: selectionModel.isSelected(option), - onchange: () => selectionModel.isSelected(option) ? selectionModel.deselect(option) : selectionModel.select(option), - ...additionalProperties, - }), - h('label.form-check-label', { - for: `${selector}-checkbox-${option.value}`, - }, option.label || option.value), - ]))); -}; +export const expandedSelectionFilter = (selectionModel, additionalProperties = {}) => h( + '.flex-row.flex-wrap.gc4', + selectionOptions(selectionModel, { selectorPrefix: additionalProperties.selector }), +); diff --git a/lib/public/components/common/selection/FilterableRemoteSelectionModel.js b/lib/public/components/common/selection/FilterableRemoteSelectionModel.js new file mode 100644 index 0000000000..84ec9b9be5 --- /dev/null +++ b/lib/public/components/common/selection/FilterableRemoteSelectionModel.js @@ -0,0 +1,172 @@ +/** + * @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 { Observable, RemoteData } from '/js/src/index.js'; +import { SelectionModel } from './SelectionModel.js'; + +/** + * @typedef {SelectionModelConfiguration} FilterableRemoteSelectionModelConfiguration + * @property {RemoteData} [availableOptions=[]] the list of available options + */ + +/** + * Model storing a given user selection over a pre-defined list of options + */ +export class FilterableRemoteSelectionModel extends Observable { + /** + * Constructor + * @param {FilterableRemoteSelectionModelConfiguration} [configuration] the model's configuration + */ + constructor(configuration) { + super(); + const { availableOptions = [], defaultSelection = [], multiple = true, allowEmpty = true } = configuration || {}; + + this._defaultSelection = defaultSelection; + this._multiple = multiple; + this._allowEmpty = allowEmpty; + + /** + * Optional search text to filter options + * + * @type {string} + * @private + */ + this._searchInputContent = ''; + + this._visualChange$ = new Observable(); + + this._selectionModel = RemoteData.notAsked(); + this.setAvailableOptions(availableOptions); + } + + /** + * Returns an observable notified any time a visual change occurs that has no impact on the actual selection + * + * @return {Observable} the visual change observable + */ + get visualChange$() { + return this._visualChange$; + } + + /** + * Reset the selection to the default + * + * @return {void} + */ + reset() { + this._selectionModel.match({ + Success: (selectionModel) => selectionModel.reset(), + Other: () => { + // Do nothing + }, + }); + this._searchInputContent = ''; + } + + /** + * States if the current selection is empty + * + * @return {boolean} true if the selection is empty + */ + get isEmpty() { + return this._selectionModel.match({ + Success: (selectionModel) => selectionModel.isEmpty, + Other: () => true, + }); + } + + /** + * Returns the content of the search input + * + * @return {string} the search input content + */ + get searchInputContent() { + return this._searchInputContent; + } + + /** + * Stores the content of the search input + * + * @param {string} value the new search input content + */ + set searchInputContent(value) { + this._searchInputContent = value; + this.visualChange$.notify(); + } + + /** + * Defines the list of available options + * + * @param {RemoteData|SelectionOption[]} availableOptions the new available options + * @return {void} + */ + setAvailableOptions(availableOptions) { + const remoteAvailableOptions = availableOptions instanceof RemoteData + ? availableOptions + : RemoteData.success(availableOptions); + + remoteAvailableOptions.match({ + Success: (availableOptions) => { + const selectionModel = new SelectionModel({ + availableOptions, + defaultSelection: this._defaultSelection, + multiple: this._multiple, + allowEmpty: this._allowEmpty, + }); + this._selectionModel = RemoteData.success(selectionModel); + selectionModel.bubbleTo(this); + }, + Loading: () => { + this._selectionModel = RemoteData.loading(); + }, + Failure: (error) => { + this._selectionModel = RemoteData.failure(error); + }, + NotAsked: () => { + this._selectionModel = RemoteData.notAsked(); + }, + }); + + this.visualChange$.notify(); + } + + /** + * Return the **values** of the currently selected options + * + * @return {string[]|number[]} the values of the selected options + */ + get selected() { + return this._selectionModel.match({ + Success: (selectionModel) => selectionModel.selected, + Other: () => [], + }); + } + + /** + * Return the list of available options + */ + get options() { + return this._selectionModel.match({ + Success: (selectionModel) => selectionModel.options, + Other: () => [], + }); + } + + /** + * Return the underlying selection model + * + * @return {RemoteData} the selection model + */ + get selectionModel() { + return this._selectionModel; + } +} diff --git a/lib/public/components/common/selection/SelectionModel.js b/lib/public/components/common/selection/SelectionModel.js index 8b28aa28d1..26c3ed8b51 100644 --- a/lib/public/components/common/selection/SelectionModel.js +++ b/lib/public/components/common/selection/SelectionModel.js @@ -10,28 +10,29 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { Observable, RemoteData } from '/js/src/index.js'; -/** - * @typedef SelectionOption A picker option, with the actual value and its string representation - * @property {number|string} value The id of the object this is used to see if it is checked. - * @property {Component} [label] The representation of the option (if null, value is used as label) - * @property {string} [rawLabel] The string only representation of the option, useful if the label is not a string - * @property {string} [selector] If the value of the option is not a valid CSS, this is used to define option's id - */ +import { Observable } from '/js/src/index.js'; /** * @typedef SelectionModelConfiguration - * @property {RemoteData|SelectionOption[]} [availableOptions=[]] the list of available options + * @property {SelectionOption[]} [availableOptions=[]] the list of available options * @property {SelectionOption[]} [defaultSelection=[]] the default selection * @property {boolean} [multiple=true] if true, the selection can contain more than one element. Else, any selection will * discard the previous one - * @property {allowEmpty} [allowEmpty=true] if true, the selection can be empty. Else, deselect will be cancelled if it leads to + * @property {boolean} [allowEmpty=true] if true, the selection can be empty. Else, deselect will be cancelled if it leads to * empty selection */ /** - * Model storing a given user selection over a pre-defined list of options + * @typedef SelectionOption A picker option, with the actual value and its string representation + * @property {number|string} value The id of the object this is used to see if it is checked. + * @property {Component} [label] The representation of the option (if null, value is used as label) + * @property {string} [rawLabel] The string only representation of the option, useful if the label is not a string + * @property {string} [selector] If the value of the option is not a valid CSS, this is used to define option's id + */ + +/** + * Model to store any custom user selection of pre-defined options */ export class SelectionModel extends Observable { /** @@ -43,7 +44,7 @@ export class SelectionModel extends Observable { const { availableOptions = [], defaultSelection = [], multiple = true, allowEmpty = true } = configuration || {}; /** - * @type {RemoteData|SelectionOption[]} + * @type {SelectionOption[]} * @protected */ this._availableOptions = availableOptions; @@ -74,46 +75,54 @@ export class SelectionModel extends Observable { if (!this._allowEmpty && this._defaultSelection.length === 0) { throw new Error('If empty is not allowed a default selection must be provided'); } - - /** - * Optional search text to filter options - * - * @type {string} - * @private - */ - this._searchInputContent = ''; - - this._visualChange$ = new Observable(); } /** - * Returns an observable notified any time a visual change occurs that has no impact on the actual selection + * Reset the selection to the default * - * @return {Observable} the visual change observable + * @return {void} */ - get visualChange$() { - return this._visualChange$; + reset() { + this._selectedOptions = [...this._defaultSelection]; } /** - * States if the current selection is exactly the default one + * Returns the list of options currently provided by the selector + * + * Depending on the selector, this may be a filtered subset of all the available options * - * @return {boolean} true if the selection is the default one + * @return {SelectionOption[]} the list of options */ - hasOnlyDefaultSelection() { - const { selected } = this; - const defaultSelection = [...new Set(this._defaultSelection.map(({ value }) => value))]; + get options() { + return [ + ...this._availableOptions, + ...this._defaultSelection.filter((defaultOption) => !this._availableOptions.find(({ value }) => defaultOption.value === value)), + ]; + } - return selected.length === defaultSelection.length && selected.every((item) => defaultSelection.includes(item)); + /** + * Return the **values** of the currently selected options + * + * Do not use this getter to modify the selected list but use the `selected` setter to define the new selected list and to notify observers + * + * @return {string[]|number[]} the values of the selected options + */ + get selected() { + return [...new Set(this._selectedOptions.map(({ value }) => value))]; } /** - * Reset the selection to the default + * If the selection allows one and only one selection, current will return the currently selected option. In any other case it will throw an + * error * - * @return {void} + * @return {string|number} the current selection */ - reset() { - this._selectedOptions = [...this._defaultSelection]; + get current() { + if (this._allowEmpty || this._multiple) { + throw new Error('"current" is available only in non-multiple select that do not allow empty value'); + } + + return this.selected[0]; } /** @@ -135,7 +144,7 @@ export class SelectionModel extends Observable { deselect(option) { const newSelection = this._selectedOptions.filter((selectedOption) => selectedOption.value !== option.value); if (this._allowEmpty || newSelection.length > 0) { - this.selectedOptions = newSelection; + this._selectedOptions = newSelection; this.notify(); } } @@ -150,14 +159,7 @@ export class SelectionModel extends Observable { let selectOption; if (typeof option === 'string' || typeof option === 'number') { - if (this._availableOptions instanceof RemoteData) { - selectOption = this._availableOptions.match({ - Success: (options) => options.find(({ value }) => value === option), - Other: () => null, - }); - } else { - selectOption = this._availableOptions.find(({ value }) => value === option); - } + selectOption = this._availableOptions.find(({ value }) => value === option); } else { selectOption = option; } @@ -174,117 +176,22 @@ export class SelectionModel extends Observable { } /** - * Returns the content of the search input - * - * @return {string} the search input content - */ - get searchInputContent() { - return this._searchInputContent; - } - - /** - * Stores the content of the search input - * - * @param {string} value the new search input content - */ - set searchInputContent(value) { - this._searchInputContent = value; - this.visualChange$.notify(); - } - - /** - * Returns the list of options currently provided by the selector - * - * Depending on the selector, this may be a filtered subset of all the available options - * - * @return {RemoteData|SelectionOption[]} the list of options - */ - get options() { - /** - * Add the default options to the list of given options - * - * @param {SelectionOption[]} options the options to which default selection should be added - * @return {SelectionOption[]} the options list including default options - */ - const addDefaultToOptions = (options) => [ - ...options, - ...this.optionsSelectedByDefault.filter((defaultOption) => !options.find(({ value }) => defaultOption.value === value)), - ]; - - /** - * Apply the current search filtering on option - * - * @param {SelectionOption} option the option to filter - * @return {boolean} true if the option matches the current search - */ - const filter = this._searchInputContent ? - ({ rawLabel, label, value }) => (rawLabel || label || value).toUpperCase().includes(this._searchInputContent.toUpperCase()) - : null; - - /** - * Prepare the list of options by adding default and apply filter if needed - * - * @param {SelectionOption[]} options the list of options to prepare - * @return {SelectionOption[]} the prepared options - */ - const prepareOptions = (options) => { - let actualOptions = addDefaultToOptions(options); - if (filter) { - actualOptions = options.filter(filter); - } - return actualOptions; - }; - - return this._availableOptions instanceof RemoteData - ? this._availableOptions.apply({ - Success: prepareOptions, - }) - : prepareOptions(this._availableOptions); - } - - /** - * Defines the list of available options - * - * @param {RemoteData|SelectionOption[]} availableOptions the new available options - * @return {void} - */ - setAvailableOptions(availableOptions) { - this._availableOptions = availableOptions; - this.visualChange$.notify(); - } - - /** - * Return the **values** of the currently selected options - * - * Do not use this getter to modify the selected list but use the `selected` setter to define the new selected list and to notify observers - * - * @return {string[]|number[]} the values of the selected options - */ - get selected() { - return [...new Set(this._selectedOptions.map(({ value }) => value))]; - } - - /** - * States if the current selection is empty + * Return the list of currently selected options * - * @return {boolean} true if the selection is empty + * @return {SelectionOption[]} the currently selected options */ - get isEmpty() { - return this.selected.length === 0; + get selectedOptions() { + return this._selectedOptions; } /** - * If the selection allows one and only one selection, current will return the currently selected option. In any other case it will throw an - * error + * Define (overrides) the list of currently selected options * - * @return {string|number} the current selection + * @param {SelectionOption[]} selected the list of selected options */ - get current() { - if (this._allowEmpty || this._multiple) { - throw new Error('"current" is available only in non-multiple select that do not allow empty value'); - } - - return this.selected[0]; + set selectedOptions(selected) { + this._selectedOptions = selected; + this.notify(); } /** @@ -306,29 +213,11 @@ export class SelectionModel extends Observable { } /** - * Return the list of currently selected options - * - * @return {SelectionOption[]} the currently selected options - */ - get selectedOptions() { - return this._selectedOptions; - } - - /** - * Define (overrides) the list of currently selected options - * - * @param {SelectionOption[]} selected the list of selected options - */ - set selectedOptions(selected) { - this._selectedOptions = selected; - } - - /** - * Return the list of options selected by default + * States if the selection is empty * - * @return {SelectionOption[]} the list of options selected by default + * @return {boolean} true if the selection is empty */ - get optionsSelectedByDefault() { - return this._defaultSelection; + get isEmpty() { + return this.selected.length === 0; } } diff --git a/lib/public/components/common/selection/dropdown/SelectionDropdownModel.js b/lib/public/components/common/selection/dropdown/SelectionDropdownModel.js index d365c74728..a1a76ceb1e 100644 --- a/lib/public/components/common/selection/dropdown/SelectionDropdownModel.js +++ b/lib/public/components/common/selection/dropdown/SelectionDropdownModel.js @@ -10,7 +10,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { SelectionModel } from '../SelectionModel.js'; +import { FilterableRemoteSelectionModel } from '../FilterableRemoteSelectionModel.js'; /** * @typedef SelectionDropdownModelExclusiveConfiguration @@ -21,7 +21,7 @@ import { SelectionModel } from '../SelectionModel.js'; /** * Model storing the state of a dropdown component */ -export class SelectionDropdownModel extends SelectionModel { +export class SelectionDropdownModel extends FilterableRemoteSelectionModel { /** * Constructor * @@ -34,15 +34,6 @@ export class SelectionDropdownModel extends SelectionModel { this._initialize(); } - // eslint-disable-next-line valid-jsdoc - /** - * @inheritDoc - */ - reset() { - super.reset(); - this._searchInputContent = ''; - } - /** * Initialize the model, in constructor or at first opening depending on the configuration * diff --git a/lib/public/components/common/selection/dropdown/selectionDropdown.js b/lib/public/components/common/selection/dropdown/selectionDropdown.js index a3e07de2a4..e4d311894f 100644 --- a/lib/public/components/common/selection/dropdown/selectionDropdown.js +++ b/lib/public/components/common/selection/dropdown/selectionDropdown.js @@ -11,105 +11,11 @@ * or submit itself to any jurisdiction. */ -import { h, Observable, RemoteData } from '/js/src/index.js'; +import { h, Observable } from '/js/src/index.js'; import spinner from '../../spinner.js'; import { cleanPrefix } from '../../../../utilities/cleanPrefix.js'; import { dropdown } from '../../popover/dropdown.js'; - -/** - * @callback DisplayDropdownOption - * - * @param {SelectionOption} option the option to display - * @param {boolean} checked the current state of the option - * @param {function} onChange function called with the option and its new state when the option's state change - * @param {string} [selectorPrefix] prefix used to generate DOM selectors for the component - * @return {Component} the option's view - */ - -/** - * @callback DisplaySelectionItem - * - * @param {SelectionOption} option the selected option to display - * @return {Component} the item's view - */ - -/** - * Display the dropdown options component containing the search input ahd the available options - * - * @param {SelectionDropdownModel} dropdownModel the dropdown model - * @param {string} selectorPrefix the prefix used to generate DOM selectors - * @param {DisplayDropdownOption} displayOption function used to display a given option - * @param {Observable} openingObservable observable notified when the options list is opened - * @param {boolean} searchEnabled if true, options' search input is enabled - * @return {Component} the dropdown options component - */ -const dropdownOptions = (dropdownModel, selectorPrefix, displayOption, openingObservable, searchEnabled) => { - // eslint-disable-next-line require-jsdoc - const onSelectionChange = (option, state) => { - state ? dropdownModel.select(option) : dropdownModel.deselect(option); - }; - - /** - * Displays the list of available options - * - * @param {SelectionOption[]} availableOptions the list of all the available options - * @param {function} onChange callback to handle an option state change - * @return {Component} the options list - */ - const optionsList = (availableOptions, onChange) => { - if (availableOptions.length === 0) { - return h('.ph2.pv1', h('em', 'No options')); - } - - return availableOptions.map((option) => displayOption( - option, - dropdownModel.isSelected(option), - onChange, - selectorPrefix, - )); - }; - - return [ - h( - '.dropdown-head.p1', - searchEnabled && h( - `input.form-control.dropdown-search#${selectorPrefix}dropdown-search-input`, - { - type: 'search', - placeHolder: 'Search', - value: dropdownModel.searchInputContent, - oninput: (e) => { - dropdownModel.searchInputContent = e.target.value; - }, - // eslint-disable-next-line require-jsdoc - oncreate: function ({ dom }) { - this.onOpen = () => dom.focus(); - openingObservable.observe(this.onOpen); - }, - // eslint-disable-next-line require-jsdoc - onupdate: function ({ dom }) { - openingObservable.unobserve(this.onOpen); - this.onOpen = () => dom.focus(); - openingObservable.observe(this.onOpen); - }, - // eslint-disable-next-line require-jsdoc - onremove: function () { - openingObservable.unobserve(this.onOpen); - delete this.onOpen; - }, - }, - ), - ), - h('.dropdown-options', dropdownModel.options instanceof RemoteData - ? dropdownModel.options.match({ - NotAsked: () => null, - Loading: () => spinner({ size: 2, absolute: false }), - Success: (availableOptions) => optionsList(availableOptions, onSelectionChange), - Failure: () => null, - }) - : optionsList(dropdownModel.options, onSelectionChange)), - ]; -}; +import { selectionOptions } from '../selectionOptions.js'; /** * Display a selection component composed of a view of current selection plus a dropdown displaying available options @@ -118,52 +24,20 @@ const dropdownOptions = (dropdownModel, selectorPrefix, displayOption, openingOb * @param {Object} [configuration] the component's configuration * @param {string} [configuration.selectorPrefix=''] a selector prefix used to generate DOM selectors * @param {Component} [configuration.placeholder='-'] component used as trigger content when no option are selected - * @param {DisplayDropdownOption} [configuration.displayOption=null] function used to generate the option's view - * @param {DisplaySelectionItem} [configuration.displaySelectionItem=null] function called with the selected option to generate the current - * selection's items - * @param {'left'|'right'} [configuration.alignment='left'] defines the alignment of the dropdown * @param {boolean} [configuration.searchEnabled] if true, options' search input is enabled * @return {Component} the dropdown component */ export const selectionDropdown = (selectionDropdownModel, configuration) => { - let { displayOption = null, displaySelectionItem = null } = configuration || {}; const { searchEnabled = true, placeholder = '-' } = configuration || {}; - const selectorPrefix = cleanPrefix(configuration.selectorPrefix); - if (displayOption === null) { - displayOption = (option, checked, onChange, selectorPrefix) => { - const key = option.selector ?? option.value; - - return h( - 'label.dropdown-option.form-check-label.flex-row.g2.ph2.pv1', - { key }, - [ - h( - `input#${selectorPrefix}dropdown-option-${key}`, - { - type: selectionDropdownModel.multiple || selectionDropdownModel.allowEmpty ? 'checkbox' : 'radio', - name: `${selectorPrefix}dropdown-option-${selectionDropdownModel.multiple ? key : 'group'}`, - checked, - onchange: (e) => onChange(option, e.target.checked), - }, - ), - option.label || option.value, - ], - ); - }; - } - - if (displaySelectionItem === null) { - displaySelectionItem = ({ label, value }) => h('small.badge.bg-gray-light', { key: value }, label || value); - } - - const selectedPills = h( - '.flex-row.flex-wrap.dropdown-selection.g2', - selectionDropdownModel.selectedOptions.length > 0 - ? selectionDropdownModel.selectedOptions.map(displaySelectionItem) - : h('small.badge', placeholder), - ); + /** + * Display a selection item + * + * @param {SelectionOption} option the selected option to display + * @return {Component} the item's view + */ + const displaySelectionItem = ({ label, value }) => h('small.badge.bg-gray-light', { key: value }, label || value); // Create an observable notified any time the dropdown is opened const openingObservable = new Observable(); @@ -172,14 +46,71 @@ export const selectionDropdown = (selectionDropdownModel, configuration) => { h( '.dropdown-trigger.form-control', [ - h('.flex-grow', selectedPills), + h('.flex-grow', selectionDropdownModel.selectionModel.match({ + NotAsked: () => null, + Loading: () => spinner({ size: 1, absolute: false }), + Success: (selectionModel) => h( + '.flex-row.flex-wrap.dropdown-selection.g2', + selectionModel.selectedOptions.length > 0 + ? selectionModel.selectedOptions.map(displaySelectionItem) + : h('small.badge', placeholder), + ), + Failure: () => null, + })), h('.dropdown-trigger-symbol', ''), ], ), - dropdownOptions(selectionDropdownModel, selectorPrefix, displayOption, openingObservable, searchEnabled), + [ + searchEnabled && h( + '.dropdown-head.p1', + h( + `input.form-control.dropdown-search#${selectorPrefix}dropdown-search-input`, + { + type: 'search', + placeHolder: 'Search', + value: selectionDropdownModel.searchInputContent, + oninput: (e) => { + selectionDropdownModel.searchInputContent = e.target.value; + }, + // eslint-disable-next-line require-jsdoc + oncreate: function ({ dom }) { + this.onOpen = () => dom.focus(); + openingObservable.observe(this.onOpen); + }, + // eslint-disable-next-line require-jsdoc + onupdate: function ({ dom }) { + openingObservable.unobserve(this.onOpen); + this.onOpen = () => dom.focus(); + openingObservable.observe(this.onOpen); + }, + // eslint-disable-next-line require-jsdoc + onremove: function () { + openingObservable.unobserve(this.onOpen); + delete this.onOpen; + }, + }, + ), + ), + selectionDropdownModel.selectionModel.match({ + NotAsked: () => null, + Loading: () => spinner({ size: 2, absolute: false }), + Success: (selectionModel) => h( + '.dropdown-options', + selectionOptions( + selectionModel, + { + filter: selectionDropdownModel.searchInputContent, + placeholder: h('.ph2.pv1', h('em', 'No options')), + selectorPrefix, + labelClasses: ['dropdown-option', 'ph2', 'pv1'], + }, + ), + ), + Failure: () => null, + }), + ], { selectorPrefix, - alignment: configuration.alignment, onVisibilityChange: (visibility) => { if (visibility) { openingObservable.notify(); diff --git a/lib/public/components/common/selection/filterSelectionOptions.js b/lib/public/components/common/selection/filterSelectionOptions.js new file mode 100644 index 0000000000..ac7f1d4fb0 --- /dev/null +++ b/lib/public/components/common/selection/filterSelectionOptions.js @@ -0,0 +1,33 @@ +/** + * @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. + */ + +/** + * Return a subset of options for which label/value matches a given search text + * + * @param {SelectionOption[]} options options the list of options to filter + * @param {string} substring the substring that options must match + * @return {SelectionOption[]} the filtered options + */ +export const filterSelectionOptions = (options, substring) => { + /** + * Apply the current search filtering on option + * + * @param {SelectionOption} option the option to filter + * @return {boolean} true if the option matches the current search + */ + const filter = substring + ? ({ rawLabel, label, value }) => (rawLabel || label || value).toUpperCase().includes(substring.toUpperCase()) + : null; + + return filter ? options.filter(filter) : options; +}; diff --git a/lib/public/components/common/selection/picker/PickerModel.js b/lib/public/components/common/selection/picker/PickerModel.js index e8945aefe9..9ec8162ea4 100644 --- a/lib/public/components/common/selection/picker/PickerModel.js +++ b/lib/public/components/common/selection/picker/PickerModel.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { SelectionModel } from '../SelectionModel.js'; +import { FilterableRemoteSelectionModel } from '../FilterableRemoteSelectionModel.js'; /** * @typedef PickerModelExclusiveConfiguration @@ -22,7 +22,7 @@ import { SelectionModel } from '../SelectionModel.js'; /** * Model to handle the state of a tag picker */ -export class PickerModel extends SelectionModel { +export class PickerModel extends FilterableRemoteSelectionModel { /** * Constructor * diff --git a/lib/public/components/common/selection/picker/picker.js b/lib/public/components/common/selection/picker/picker.js index 9242f146b7..58641cce45 100644 --- a/lib/public/components/common/selection/picker/picker.js +++ b/lib/public/components/common/selection/picker/picker.js @@ -12,104 +12,52 @@ */ import { h } from '/js/src/index.js'; +import { filterSelectionOptions } from '../filterSelectionOptions.js'; /** * @typedef PickerConfiguration The configuration of the picker + * @property {string} [filter='picker'] if specified, this will be used to filter displayed options * @property {string} [selector='picker'] if specified, this will be used to customize picker components ids and classes - * @property {object|null} [attributes=null] if specified, picker items will be wrapped within a component with these attributes. If a limit is - * present and the items overflow, the overflowed elements will be wrapped in a separated component with the same attributes * @property {object|null} [optionsAttributes=null] attributes applied to picker elements * @property {boolean} [outlineSelection=false] if true, the current selection will be displayed at the top of the choices list - * @property {Component|null} [placeHolder=null] if not null, this component will be displayed at the top of the choices list if the selection - * is empty */ -/** - * Returns the list of input components that correspond to the given options list - * - * @param {PickerModel} pickerModel the model containing the current inputs states - * @param {SelectionOption[]} pickerOptions the list of options to display - * @param {string} selector selector to customize input components selectors - * @param {object|null} optionsAttributes attributes applied to picker elements - * @return {Component[]} the list of inputs - */ -const mapOptionsToInput = (pickerModel, pickerOptions, selector, optionsAttributes) => pickerOptions.map((pickerOption, index) => h( - `.flex-row.${selector}-option.g2`, - { key: pickerOption.rawLabel || pickerOption.label || pickerOption.value }, - [ - h( - 'input', - { - onclick: () => pickerModel.isSelected(pickerOption) - ? pickerModel.deselect(pickerOption) - : pickerModel.select(pickerOption), - id: `${selector}Checkbox${index + 1}`, - type: 'checkbox', - checked: pickerModel.isSelected(pickerOption), - disabled: optionsAttributes?.disabled, - }, - ), - h( - 'label.label.flex-row.items-center', - { - ...optionsAttributes, - for: `${selector}Checkbox${index + 1}`, - }, - pickerOption.label || pickerOption.value, - ), - ], -)); - /** * Generates a picker component * - * @param {SelectionOption[]} pickerOptions The available options of the picker - * @param {PickerModel} pickerModel The picker model. + * @param {SelectionModel} selectionModel The selection model. * @param {PickerConfiguration} [configuration] the picker's configuration * @returns {Component} A filterable collapsable picker. */ -export const picker = (pickerOptions, pickerModel, configuration) => { - const { selector = 'picker', attributes = null, optionsAttributes = null, placeHolder = null, searchEnabled = true } = configuration || {}; - - const checkboxes = mapOptionsToInput(pickerModel, pickerOptions, selector, optionsAttributes); - - const selectedPills = pickerModel.selectedOptions.length - ? h( - '.flex-row.flex-wrap.g2', - pickerModel.selectedOptions.map(({ rawLabel, label, value }) => h( - '', - { key: rawLabel || label || value }, - label || value, - )), - ) - : placeHolder; - - /** - * If attributes is not null, wrap the given content inside a component with the given attributes - * - * @param {Component} toWrap component(s) to wrap - * @return {Component} the wrapped result - */ - const applyAttributes = (toWrap) => { - if (attributes && toWrap) { - return h('', attributes, toWrap); - } else { - return toWrap; - } - }; - - const searchInput = h(`input.form-control.${selector}-search-input`, { - type: 'search', - placeHolder: '🔍 Search for tags', - value: pickerModel.searchInputContent, - oninput: (e) => { - pickerModel.searchInputContent = e.target.value; - }, - }); - - return [ - selectedPills, - searchEnabled && searchInput, - applyAttributes(checkboxes.length ? checkboxes : h('em', 'No options')), - ]; +export const picker = (selectionModel, configuration) => { + const { selector = 'picker', filter = '', optionsAttributes = null } = configuration || {}; + + const checkboxes = filterSelectionOptions(selectionModel.options, filter).map((pickerOption, index) => h( + `.flex-row.${selector}-option.g2`, + { key: pickerOption.rawLabel || pickerOption.label || pickerOption.value }, + [ + h( + 'input', + { + onclick: () => selectionModel.isSelected(pickerOption) + ? selectionModel.deselect(pickerOption) + : selectionModel.select(pickerOption), + id: `${selector}Checkbox${index + 1}`, + type: 'checkbox', + checked: selectionModel.isSelected(pickerOption), + disabled: optionsAttributes?.disabled, + }, + ), + h( + 'label.label.flex-row.items-center', + { + ...optionsAttributes, + for: `${selector}Checkbox${index + 1}`, + }, + pickerOption.label || pickerOption.value, + ), + ], + )); + + return checkboxes.length ? checkboxes : h('em', 'No options'); }; diff --git a/lib/public/components/common/selection/picker/pickerSelection.js b/lib/public/components/common/selection/picker/pickerSelection.js new file mode 100644 index 0000000000..11b0d61776 --- /dev/null +++ b/lib/public/components/common/selection/picker/pickerSelection.js @@ -0,0 +1,32 @@ +/** + * @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 { h } from '/js/src/index.js'; + +/** + * Displays the current picker selection as pills + * + * @param {SelectionModel} selectionModel the selection model + * @param {Component} placeHolder placeholder used if selection is empty + * @return {Component} the current selection + */ +export const pickerSelection = (selectionModel, placeHolder) => selectionModel.selectedOptions.length + ? h( + '.flex-row.flex-wrap.g2', + selectionModel.selectedOptions.map(({ rawLabel, label, value }) => h( + '', + { key: rawLabel || label || value }, + label || value, + )), + ) + : placeHolder; diff --git a/lib/public/components/common/selection/selectionOptions.js b/lib/public/components/common/selection/selectionOptions.js new file mode 100644 index 0000000000..11c6ce90b2 --- /dev/null +++ b/lib/public/components/common/selection/selectionOptions.js @@ -0,0 +1,65 @@ +/** + * @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 { cleanPrefix } from '../../../utilities/cleanPrefix.js'; +import { filterSelectionOptions } from './filterSelectionOptions.js'; +import { h } from '/js/src/index.js'; + +/** + * Display the options of the given selection model + * + * @param {SelectionModel} selectionModel the selection model + * @param {object} [configuration] eventual configuration + * @param {string} [configuration.filter] if specified, only options with label (or value if no label) including this filter will be displayed + * @param {Component} [configuration.placeholder] if specified, this component will be returned if there is no option to display + * @param {string} [configuration.selectorPrefix] prefix to be used to construct elements selectors + * @param {string[]} [configuration.labelClasses] additional classes applied to label + * @return {Component} filter component + */ +export const selectionOptions = (selectionModel, configuration) => { + const { filter, placeholder = null, selectorPrefix = '', labelClasses: additionalLabelClasses = [] } = configuration || {}; + + const options = filterSelectionOptions(selectionModel.options, filter); + + if (options.length === 0) { + return placeholder; + } + + return options.map((option) => { + const selector = option.selector ?? option.value; + + const uniqueSelector = `${cleanPrefix(selectorPrefix)}option-${selector}`; + + const labelClasses = ['form-check-label', 'flex-row', 'g2', ...additionalLabelClasses]; + + return h( + `label.${labelClasses.join('.')}`, + { key: uniqueSelector }, + [ + h( + `input#${uniqueSelector}`, + { + id: uniqueSelector, + type: selectionModel.multiple || selectionModel.allowEmpty ? 'checkbox' : 'radio', + name: selectionModel.multiple || selectionModel.allowEmpty + ? uniqueSelector + : `${cleanPrefix(selectorPrefix)}option-group`, + checked: selectionModel.isSelected(option), + onchange: () => selectionModel.isSelected(option) ? selectionModel.deselect(option) : selectionModel.select(option), + }, + ), + option.label || option.value, + ], + ); + }); +}; diff --git a/lib/public/components/tag/tagPicker.js b/lib/public/components/tag/tagPicker.js index 31fc97f6a0..45f6759145 100644 --- a/lib/public/components/tag/tagPicker.js +++ b/lib/public/components/tag/tagPicker.js @@ -15,7 +15,7 @@ import spinner from '../common/spinner.js'; import { picker } from '../common/selection/picker/picker.js'; import { h } from '/js/src/index.js'; import { frontLink } from '../common/navigation/frontLink.js'; -import { asRemoteData } from '../../utilities/asRemoteData.js'; +import { pickerSelection } from '../common/selection/picker/pickerSelection.js'; /** * Return a component representing a picker for a remote data list of tags, handling each possible remote data status @@ -24,19 +24,17 @@ import { asRemoteData } from '../../utilities/asRemoteData.js'; * * @return {Component} the resulting component */ -export const tagPicker = (tagPickerModel) => asRemoteData(tagPickerModel.options).match({ +export const tagPicker = (tagPickerModel) => tagPickerModel.selectionModel.match({ NotAsked: () => null, Loading: () => spinner({ size: 2, justify: 'left', absolute: false, }), - Success: (options) => picker( - options, - tagPickerModel, - { - selector: 'tag', - placeHolder: h( + Success: (selectionModel) => [ + pickerSelection( + selectionModel, + h( 'label.form-check-label.f6', { for: 'tags' }, [ @@ -45,10 +43,19 @@ export const tagPicker = (tagPickerModel) => asRemoteData(tagPickerModel.options ' screen.', ], ), - limit: null, - attributes: { class: 'scroll-y grid columns-2-lg columns-3-xl g2' }, - outlineSelection: true, - }, - ), + ), + h('input.form-control.tag-search-input', { + type: 'search', + placeHolder: '🔍 Search for tags', + value: tagPickerModel.searchInputContent, + oninput: (e) => { + tagPickerModel.searchInputContent = e.target.value; + }, + }), + h('.scroll-y.grid.columns-2-lg.columns-3-xl.g2', picker( + selectionModel, + { filter: tagPickerModel.searchInputContent, selector: 'tag' }, + )), + ], Failure: () => null, }); diff --git a/lib/public/views/Logs/Create/LogReplyModel.js b/lib/public/views/Logs/Create/LogReplyModel.js index 21745d283c..7cd47f156e 100644 --- a/lib/public/views/Logs/Create/LogReplyModel.js +++ b/lib/public/views/Logs/Create/LogReplyModel.js @@ -55,21 +55,20 @@ export class LogReplyModel extends LogCreationModel { this.title = `${title}`; - if (this.tagsPickerModel.hasOnlyDefaultSelection()) { - this.tagsPickerModel.selectedOptions = tags.map(tagToOption); - } + this.tagsPickerModel.selectionModel.match({ + Success: (selectionModel) => { + selectionModel.selectedOptions = tags.map(tagToOption); + }, + Other: () => { + // Simply do nothing + }, + }); - if (!this.runNumbers) { - this.runNumbers = runs.map(({ runNumber }) => runNumber).join(', '); - } + this.runNumbers = runs.map(({ runNumber }) => runNumber).join(', '); - if (!this.environments) { - this.environments = environments.map(({ id }) => id).join(', '); - } + this.environments = environments.map(({ id }) => id).join(', '); - if (!this.lhcFills) { - this.lhcFills = lhcFills.map(({ fillNumber }) => fillNumber).join(', '); - } + this.lhcFills = lhcFills.map(({ fillNumber }) => fillNumber).join(', '); } /** diff --git a/lib/public/views/Logs/Create/TemplatedLogCreationModel.js b/lib/public/views/Logs/Create/TemplatedLogCreationModel.js index f5ce560976..0948c3192e 100644 --- a/lib/public/views/Logs/Create/TemplatedLogCreationModel.js +++ b/lib/public/views/Logs/Create/TemplatedLogCreationModel.js @@ -23,6 +23,11 @@ import { RcDailyMeetingTemplate } from './templates/RcDailyMeetingTemplate.js'; * @typedef {'on-call'|'rc-daily-meeting'} logTemplateKey */ +const TemplateClasses = Object.freeze({ + ['on-call']: OnCallLogTemplate, + ['rc-daily-meeting']: RcDailyMeetingTemplate, +}); + /** * Return a new instance of log template for the given key * @@ -31,10 +36,7 @@ import { RcDailyMeetingTemplate } from './templates/RcDailyMeetingTemplate.js'; * @return {LogTemplate|null} the new log template */ const logTemplatesFactory = (key, templateData) => { - const templateClass = { - ['on-call']: OnCallLogTemplate, - ['rc-daily-meeting']: RcDailyMeetingTemplate, - }[key] ?? null; + const templateClass = TemplateClasses[key] ?? null; if (templateClass) { return new templateClass(templateData); } @@ -88,9 +90,16 @@ export class TemplatedLogCreationModel extends LogCreationModel { * @return {void} */ const applyTemplateTags = () => { - for (const tag of templateModel?.tags$?.getCurrent() ?? []) { - this._creationTagsPickerModel.select(tag); - } + this._creationTagsPickerModel.selectionModel.match({ + Success: (selectionModel) => { + for (const tag of templateModel?.tags$?.getCurrent() ?? []) { + selectionModel.select(tag); + } + }, + Other: () => { + // Do nothing, tags are not available yet + }, + }); }; // If template model expose tags, observe it diff --git a/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js b/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js index 7f4ae8aa69..3b412a0058 100644 --- a/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js +++ b/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js @@ -14,7 +14,7 @@ import { h } from '/js/src/index.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; -import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; +import { expandedSelectionFilter } from '../../../components/Filters/common/filters/checkboxFilter.js'; import { qcFlagTypeColoredBadge } from '../../../components/qcFlags/qcFlagTypeColoredBadge.js'; /** @@ -54,7 +54,7 @@ export const qcFlagTypesActiveColumns = { name: 'Bad', visible: true, sortable: true, - filter: ({ isBadFilterModel }) => checkboxes( + filter: ({ isBadFilterModel }) => expandedSelectionFilter( isBadFilterModel, { class: 'w-75 mt1', selector: 'qc-flag-type-bad-filter' }, ), diff --git a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js index 4e78c4cf4c..280abdc0e4 100644 --- a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js +++ b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js @@ -30,8 +30,9 @@ export class QcFlagTypesOverviewModel extends OverviewPageModel { this._registerFilter(this._namesFilterModel); this._methodsFilterModel = new TextTokensFilterModel(); this._registerFilter(this._methodsFilterModel); - this._isBadFilterModel = - new SelectionModel({ availableOptions: [{ label: 'Bad', value: true }, { label: 'Not Bad', value: false }] }); + this._isBadFilterModel = new SelectionModel({ + availableOptions: [{ label: 'Bad', value: true }, { label: 'Not Bad', value: false }], + }); this._registerFilter(this._isBadFilterModel); } @@ -75,7 +76,7 @@ export class QcFlagTypesOverviewModel extends OverviewPageModel { /** * Returns filter model for filtering bad and not bad flags * - * @return {TextTokensFilterModel} filter model for filtering bad and not bad flags + * @return {SelectionModel} filter model for filtering bad and not bad flags */ get isBadFilterModel() { return this._isBadFilterModel; @@ -89,7 +90,7 @@ export class QcFlagTypesOverviewModel extends OverviewPageModel { * @private */ _registerFilter(filterModel) { - filterModel.visualChange$.bubbleTo(this); + filterModel.visualChange$?.bubbleTo(this); filterModel.observe(() => { this._pagination.silentlySetCurrentPage(1); this.load(); diff --git a/lib/public/views/QcFlags/GaqFlags/gaqDetectorsSelectionModalTrigger.js b/lib/public/views/QcFlags/GaqFlags/gaqDetectorsSelectionModalTrigger.js index 4937395006..5ae4283bcf 100644 --- a/lib/public/views/QcFlags/GaqFlags/gaqDetectorsSelectionModalTrigger.js +++ b/lib/public/views/QcFlags/GaqFlags/gaqDetectorsSelectionModalTrigger.js @@ -15,18 +15,19 @@ import { h } from '/js/src/index.js'; import { picker } from '../../../components/common/selection/picker/picker.js'; import errorAlert from '../../../components/common/errorAlert.js'; import spinner from '../../../components/common/spinner.js'; +import { pickerSelection } from '../../../components/common/selection/picker/pickerSelection.js'; /** * Render a form for GAQ detectors selection * - * @param {GaqOverviewModel} gaqOverviewModel gaq overview model - * @param {SelectOption[]} gaqDetectorsOptions all detector selection options + * @param {GaqFlagsOverviewModel} gaqOverviewModel gaq overview model + * @param {SelectionModel} gaqDetectorsSelectionModel gaq detectors selection model * @param {ModalHandler} modalHandler The modal handler, used to dismiss modal after successful operation * * @return {Component} the form component */ -const selectionForm = (gaqOverviewModel, gaqDetectorsOptions, modalHandler) => { - const { gaqDetectorPickerModel, setGaqDetectorsRequestResult: remoteSetGaqDetectorsRequestResult } = gaqOverviewModel; +const selectionForm = (gaqOverviewModel, gaqDetectorsSelectionModel, modalHandler) => { + const { setGaqDetectorsRequestResult: remoteSetGaqDetectorsRequestResult } = gaqOverviewModel; const sendingRequest = remoteSetGaqDetectorsRequestResult.match({ Loading: () => true, Other: () => false, @@ -37,20 +38,14 @@ const selectionForm = (gaqOverviewModel, gaqDetectorsOptions, modalHandler) => { Failure: (errors) => errorAlert(errors), Other: () => null, }), - picker( - gaqDetectorsOptions, - gaqDetectorPickerModel, - { - selector: 'gaq-detectors', - limit: null, - placeHolder: 'No detectors', - attributes: { class: 'grid columns-2-lg columns-3-xl g2' }, - optionsAttributes: { class: 'f3', disabled: sendingRequest }, - outlineSelection: true, - searchEnabled: false, - onremove: () => gaqOverviewModel.reset(), - }, + pickerSelection( + gaqDetectorsSelectionModel, + 'No detectors', ), + h('.grid.columns-2-lg.columns-3-xl.g2', picker( + gaqDetectorsSelectionModel, + { selector: 'gaq-detectors', optionsAttributes: { class: 'f3', disabled: sendingRequest } }, + )), h('.flex-row.g3.justify-center ', [ h( @@ -66,8 +61,8 @@ const selectionForm = (gaqOverviewModel, gaqDetectorsOptions, modalHandler) => { { disabled: sendingRequest, onclick: async () => { - gaqDetectorPickerModel.reset(); - gaqDetectorPickerModel.notify(); + gaqDetectorsSelectionModel.reset(); + gaqOverviewModel.notify(); }, }, 'Revert', @@ -89,7 +84,7 @@ const errorDisplay = () => h('.danger', 'Data fetching failed'); /** * Render GAQ detectors selection modal - * @param {RunsOverviewModel} gaqOverviewModel GAQ overview model + * @param {GaqFlagsOverviewModel} gaqOverviewModel GAQ overview model * @param {ModalHandler} modalHandler The modal handler, used to dismiss modal * @return {Component} modal */ @@ -98,10 +93,10 @@ const gaqDetectorsSelectionModal = (gaqOverviewModel, modalHandler) => { return h('div#gaq-detector-selection-modal', [ h('h2', 'GAQ detectors selection'), - gaqDetectorPickerModel.options.match({ + gaqDetectorPickerModel.selectionModel.match({ NotAsked: () => errorDisplay(), - Loading: () => selectionForm(gaqOverviewModel, null, modalHandler), - Success: (options) => selectionForm(gaqOverviewModel, options, modalHandler), + Loading: () => spinner({ size: 3, absolute: false }), + Success: (selectionModel) => selectionForm(gaqOverviewModel, selectionModel, modalHandler), Failure: () => errorDisplay(), }), ]); @@ -110,17 +105,20 @@ const gaqDetectorsSelectionModal = (gaqOverviewModel, modalHandler) => { /** * Render buttons which triggers rendering of GAQ selection modal * - * @param {GaqOverviewModel} gaqOverviewModel GAQ overview model - * @param {ModelModel} modalModel modal model + * @param {GaqFlagsOverviewModel} gaqOverviewModel GAQ overview model + * @param {ModalModel} modalModel modal model * @param {object} [displayConfiguration] additional display options * @param {boolean} [displayConfiguration.autoMarginLeft = true] if true margin left is set to auto, otherwise not * @returns {Component} button */ -export const gaqDetectorsSelectionModalTrigger = (gaqOverviewModel, modalModel, { autoMarginLeft = true } = {}) => - h(`button.btn.btn-primary.w-15.h2${autoMarginLeft ? '.mlauto' : ''}#gaq-detectors-selection-trigger`, { +export const gaqDetectorsSelectionModalTrigger = (gaqOverviewModel, modalModel, { autoMarginLeft = true } = {}) => h( + `button.btn.btn-primary.w-15.h2${autoMarginLeft ? '.mlauto' : ''}#gaq-detectors-selection-trigger`, + { disabled: gaqOverviewModel.gaqDetectors.match({ Success: () => false, Other: () => true, }), onclick: () => modalModel.display({ content: (modalModel) => gaqDetectorsSelectionModal(gaqOverviewModel, modalModel), size: 'medium' }), - }, 'Set GAQ Detectors'); + }, + 'Set GAQ Detectors', +); diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 1fbc15872f..b0519e2444 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -13,20 +13,13 @@ import { h } from '/js/src/index.js'; import { runNumbersFilter } from '../../../components/Filters/RunsFilter/runNumbersFilter.js'; -import environmentIdFilter from '../../../components/Filters/RunsFilter/environmentId.js'; -import nDetectorsFilter from '../../../components/Filters/RunsFilter/nDetectors.js'; -import nFlpsFilter from '../../../components/Filters/RunsFilter/nFlps.js'; -import odcTopologyFullName from '../../../components/Filters/RunsFilter/odcTopologyFullName.js'; import { displayRunEorReasonsOverview } from '../format/displayRunEorReasonOverview.js'; import ddflpFilter from '../../../components/Filters/RunsFilter/ddflp.js'; import dcsFilter from '../../../components/Filters/RunsFilter/dcs.js'; import epnFilter from '../../../components/Filters/RunsFilter/epn.js'; -import runQualityFilter from '../../../components/Filters/RunsFilter/runQuality.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { displayRunDuration } from '../format/displayRunDuration.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; -import nEpnsFilter from '../../../components/Filters/RunsFilter/nEpns.js'; -import { triggerValueFilter } from '../../../components/Filters/RunsFilter/triggerValueFilter.js'; import { formatRunType } from '../../../utilities/formatting/formatRunType.js'; import { runDefinitionFilter } from '../../../components/Filters/RunsFilter/runDefinitionFilter.js'; import { profiles } from '../../../components/common/table/profiles.js'; @@ -54,6 +47,8 @@ import { detectorsFilterComponent } from '../../../components/Filters/RunsFilter import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; import { numericalComparisonFilter } from '../../../components/Filters/common/filters/numericalComparisonFilter.js'; +import { expandedSelectionFilter } from '../../../components/Filters/common/filters/checkboxFilter.js'; +import { triggerValueFilter } from '../../../components/Filters/RunsFilter/triggerValueFilter.js'; /** * List of active columns for a generic runs table @@ -386,7 +381,17 @@ export const runsActiveColumns = { visible: true, profiles: [profiles.none, 'lhcFill', 'environment', 'home'], classes: 'w-10 f6 w-wrapped', - filter: environmentIdFilter, + + /** + * Environment ids filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the environment ids filter component + */ + filter: (runsOverviewModel) => rawTextFilter( + runsOverviewModel.filteringModel.get('environmentIds'), + { classes: ['environment-ids-filter', 'w-100'], placeholder: 'e.g. Dxi029djX, TDI59So3d...' }, + ), format: (id) => id ? frontLink(id, 'env-details', { environmentId: id }) : '-', }, runType: { @@ -428,13 +433,33 @@ export const runsActiveColumns = { }, runQuality), }, }, - filter: runQualityFilter, + + /** + * Run quality filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the run quality filter component + */ + filter: (runsOverviewModel) => expandedSelectionFilter( + runsOverviewModel.filteringModel.get('runQualities').selectionModel, + { selector: 'run-quality' }, + ), }, nDetectors: { name: 'DETs #', visible: false, classes: 'w-2 f6 w-wrapped', - filter: nDetectorsFilter, + + /** + * Filter on amount of detectors in runs + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the number of detectors filter component + */ + filter: (runsOverviewModel) => numericalComparisonFilter( + runsOverviewModel.filteringModel.get('nDetectors'), + { selectorPrefix: 'nDetectors' }, + ), }, nEpns: { name: 'EPNs #', @@ -443,14 +468,28 @@ export const runsActiveColumns = { classes: 'w-2 f6 w-wrapped', // eslint-disable-next-line no-extra-parens format: (nEpns, run) => run.epn ? (typeof nEpns === 'number' ? nEpns : 'ON') : 'OFF', - filter: nEpnsFilter, + + /** + * Filter on amount of EPNs in runs + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the number of EPNs filter component + */ + filter: (runsOverviewModel) => numericalComparisonFilter(runsOverviewModel.filteringModel.get('nEpns'), { selectorPrefix: 'nEpns' }), }, nFlps: { name: 'FLPs #', visible: true, profiles: [profiles.none, 'lhcFill', 'environment'], classes: 'w-2 f6 w-wrapped', - filter: nFlpsFilter, + + /** + * Filter on amount of FLPs in runs + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the number of FLPs filter component + */ + filter: (runsOverviewModel) => numericalComparisonFilter(runsOverviewModel.filteringModel.get('nFlps'), { selectorPrefix: 'nFlps' }), }, nSubtimeframes: { name: '# of STFs', @@ -508,7 +547,14 @@ export const runsActiveColumns = { classes: 'w-15 f6', visible: false, profiles: [profiles.none, 'lhcFill', 'environment'], - filter: odcTopologyFullName, + + /** + * ODC topology full name filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filter component + */ + filter: (runsOverviewModel) => rawTextFilter(runsOverviewModel.filteringModel.get('odcTopologyFullName')), balloon: true, }, eorReasons: { diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index fe1959b867..02beff7ae0 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -34,6 +34,8 @@ import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/Tim import { magnetsCurrentLevelsProvider } from '../../../services/magnets/magnetsCurrentLevelsProvider.js'; 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'; /** * Model representing handlers for runs page @@ -65,7 +67,18 @@ export class RunsOverviewModel extends OverviewPageModel { o2end: new TimeRangeFilterModel(), definitions: new RunDefinitionFilterModel(), runDuration: new NumericalComparisonFilterModel({ scale: 60 * 1000 }), + environmentIds: new RawTextFilterModel(), runTypes: new RunTypesFilterModel(runTypesProvider.items$), + runQualities: new SelectionFilterModel({ + availableOptions: RUN_QUALITIES.map((quality) => ({ + label: quality.toUpperCase(), + value: quality, + })), + }), + nDetectors: new NumericalComparisonFilterModel({ integer: true }), + nEpns: new NumericalComparisonFilterModel({ integer: true }), + nFlps: new NumericalComparisonFilterModel({ integer: true }), + odcTopologyFullName: new RawTextFilterModel(), eorReason: new EorReasonFilterModel(eorReasonTypeProvider.items$), magnets: new MagnetsFilteringModel(magnetsCurrentLevelsProvider.items$), muInelasticInteractionRate: new NumericalComparisonFilterModel({ useOperatorAsNormalizationKey: true }), @@ -172,26 +185,14 @@ export class RunsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); - this.environmentIdsFilter = ''; - - this.runQualitiesFilters = []; - this._triggerValuesFilters = new Set(); - this.nDetectorsFilter = null; - - this._nEpnsFilter = null; - - this.nFlpsFilter = null; - this.ddflpFilter = ''; this.dcsFilter = ''; this.epnFilter = ''; - this._odcTopologyFullNameFilter = ''; - if (fetch) { this._applyFilters(true); } @@ -203,16 +204,10 @@ export class RunsOverviewModel extends OverviewPageModel { */ isAnyFilterActive() { return this._filteringModel.isAnyFilterActive() - || this.environmentIdsFilter !== '' - || this.runQualitiesFilters.length !== 0 || this._triggerValuesFilters.size !== 0 - || this.nDetectorsFilter !== null - || this._nEpnsFilter !== null - || this.nFlpsFilter !== null || this.ddflpFilter !== '' || this.dcsFilter !== '' - || this.epnFilter !== '' - || this._odcTopologyFullNameFilter !== ''; + || this.epnFilter !== ''; } /** @@ -247,59 +242,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.notify(); } - /** - * Returns the current environment id(s) filter - * @return {String} The current environment id(s) filter - */ - getEnvFilter() { - return this.environmentIdsFilter; - } - - /** - * Sets the environment id(s) filter if no new inputs were detected for 200 milliseconds - * @param {string} newEnvironment The environment id(s) to apply to the filter - * @return {undefined} - */ - setEnvironmentIdsFilter(newEnvironment) { - this.environmentIdsFilter = newEnvironment.trim(); - this._applyFilters(); - } - - /** - * States if the given run quality is currently in the run quality filter - * - * @param {string} runQuality the run quality to look for - * @return {boolean} true if the run quality is included in the filter - */ - isRunQualityInFilter(runQuality) { - return this.runQualitiesFilters.includes(runQuality); - } - - /** - * Add a given run quality in the current run quality filter if it is not already present, then refresh runs list - * - * @param {string} runQuality the run quality to add - * @return {void} - */ - addRunQualityFilter(runQuality) { - if (!this.isRunQualityInFilter(runQuality)) { - this.runQualitiesFilters.push(runQuality); - this._applyFilters(); - } - } - - /** - * Remove a given run quality from the current run quality filter if it is in it (else do nothing) then refresh - * runs list - * - * @param {string} runQuality the run quality to add - * @return {void} - */ - removeRunQualityFilter(runQuality) { - this.runQualitiesFilters = this.runQualitiesFilters.filter((existingRunQuality) => runQuality !== existingRunQuality); - this._applyFilters(); - } - /** * Getter for the trigger values filter Set * @return {Set} set of trigger filter values @@ -318,67 +260,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._applyFilters(); } - /** - * Returns the amount of detectors filters - * @return {{operator: string, limit: (number|null)}|null} The current amount of detectors filters - */ - getNDetectorsFilter() { - return this.nDetectorsFilter; - } - - /** - * Sets the limit of detectors and the comparison operator to filter if no new inputs were detected for 200 - * milliseconds - * - * @param {{operator: string, limit: (number|null)}|null} newNDetectors The new filter value - * - * @return {void} - */ - setNDetectorsFilter(newNDetectors) { - this.nDetectorsFilter = newNDetectors; - this._applyFilters(); - } - - /** - * Returns the current amount of epns filter - * @return {{operator: string, limit: (number|null)}|null} The current amount of epns filters - */ - get nEpnsFilter() { - return this._nEpnsFilter; - } - - /** - * Returns the current amount of flps filter - * @return {{operator: string, limit: (number|null)}|null} The current amount of flps filters - */ - getNFlpsFilter() { - return this.nFlpsFilter; - } - - /** - * Sets the limit of epns and the comparison operator to filter if no new inputs were detected for 200 milliseconds - * - * @param {{operator: string, limit: (number|null)}|null} newNEpns The new filter value - * - * @return {void} - */ - set nEpnsFilter(newNEpns) { - this._nEpnsFilter = newNEpns; - this._applyFilters(); - } - - /** - * Sets the limit of flps and the comparison operator to filter if no new inputs were detected for 200 milliseconds - * - * @param {{operator: string, limit: (number|null)}|null} newNFlps The new filter value - * - * @return {void} - */ - setNFlpsFilter(newNFlps) { - this.nFlpsFilter = newNFlps; - this._applyFilters(); - } - /** * Returns the boolean of ddflp * @return {Boolean} if ddflp is on @@ -461,24 +342,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._applyFilters(); } - /** - * Returns the current epnTopology substring filter - * @return {String} The current epnTopology substring filter - */ - get odcTopologyFullNameFilter() { - return this._odcTopologyFullNameFilter; - } - - /** - * Sets the epnTopology substring filter if no new inputs were detected for 200 milliseconds - * @param {string} newTopology The epnTopology substring to apply to the filter - * @return {undefined} - */ - set odcTopologyFullNameFilter(newTopology) { - this._odcTopologyFullNameFilter = newTopology.trim(); - this._applyFilters(); - } - /** * Return all the runs currently filtered, without paging * @@ -514,27 +377,9 @@ export class RunsOverviewModel extends OverviewPageModel { */ _getFilterQueryParams() { return { - ...this.environmentIdsFilter && { - 'filter[environmentIds]': this.environmentIdsFilter, - }, - ...this.runQualitiesFilters.length !== 0 && { - 'filter[runQualities]': this.runQualitiesFilters.join(), - }, ...this._triggerValuesFilters.size !== 0 && { 'filter[triggerValues]': Array.from(this._triggerValuesFilters).join(), }, - ...this.nDetectorsFilter && this.nDetectorsFilter.limit !== null && { - 'filter[nDetectors][operator]': this.nDetectorsFilter.operator, - 'filter[nDetectors][limit]': this.nDetectorsFilter.limit, - }, - ...this.nFlpsFilter && this.nFlpsFilter.limit !== null && { - 'filter[nFlps][operator]': this.nFlpsFilter.operator, - 'filter[nFlps][limit]': this.nFlpsFilter.limit, - }, - ...this.nEpnsFilter && this.nEpnsFilter.limit !== null && { - 'filter[nEpns][operator]': this.nEpnsFilter.operator, - 'filter[nEpns][limit]': this.nEpnsFilter.limit, - }, ...(this.ddflpFilter === true || this.ddflpFilter === false) && { 'filter[ddflp]': this.ddflpFilter, }, @@ -544,9 +389,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...(this.epnFilter === true || this.epnFilter === false) && { 'filter[epn]': this.epnFilter, }, - ...this._odcTopologyFullNameFilter && { - 'filter[odcTopologyFullName]': this._odcTopologyFullNameFilter, - }, }; } diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index e2f280e647..9062cd9e1e 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -173,7 +173,7 @@ module.exports = () => { await pressElement(page, '.tags-filter .dropdown-trigger'); // Select the second available filter and wait for the changes to be processed - const firstCheckboxId = 'tag-dropdown-option-DPG'; + const firstCheckboxId = 'tag-option-DPG'; await pressElement(page, `#${firstCheckboxId}`, true); await waitForTableLength(page, 1); @@ -182,7 +182,7 @@ module.exports = () => { await waitForTableLength(page, 10); // Select the first available filter and the second one at once - const secondCheckboxId = 'tag-dropdown-option-FOOD'; + const secondCheckboxId = 'tag-option-FOOD'; await pressElement(page, `#${firstCheckboxId}`, true); await pressElement(page, `#${secondCheckboxId}`, true); await waitForEmptyTable(page); diff --git a/test/public/qcFlags/forDataPassCreation.test.js b/test/public/qcFlags/forDataPassCreation.test.js index 6f0d031326..5fecb749f8 100644 --- a/test/public/qcFlags/forDataPassCreation.test.js +++ b/test/public/qcFlags/forDataPassCreation.test.js @@ -103,7 +103,7 @@ module.exports = () => { await page.waitForSelector('input[type="time"]', { hidden: true, timeout: 250 }); await pressElement(page, '#flag-type-panel .popover-trigger'); - await pressElement(page, '#flag-type-dropdown-option-2', true); + await pressElement(page, '#flag-type-option-2', true); await page.waitForSelector('button#submit[disabled]', { hidden: true, timeout: 250 }); @@ -135,7 +135,7 @@ module.exports = () => { await page.waitForSelector('input[type="time"]', { hidden: true }); await pressElement(page, '#flag-type-panel .popover-trigger'); - await pressElement(page, '#flag-type-dropdown-option-11', true); + await pressElement(page, '#flag-type-option-11', true); await page.waitForSelector('button#submit[disabled]', { hidden: true }); await pressElement(page, '#time-based-toggle', true); @@ -175,7 +175,7 @@ module.exports = () => { await page.waitForSelector('button#submit[disabled]'); await page.waitForSelector('input[type="time"]', { hidden: true, timeout: 250 }); await pressElement(page, '#flag-type-panel .popover-trigger'); - await pressElement(page, '#flag-type-dropdown-option-2', true); + await pressElement(page, '#flag-type-option-2', true); await page.waitForSelector('button#submit[disabled]', { hidden: true, timeout: 250 }); await page.waitForSelector('#time-based-toggle', { hidden: true, timeout: 250 }); @@ -264,7 +264,7 @@ module.exports = () => { await page.waitForSelector('button#submit[disabled]'); await pressElement(page, '#flag-type-panel .popover-trigger'); - await pressElement(page, '#flag-type-dropdown-option-2', true); + await pressElement(page, '#flag-type-option-2', true); await page.waitForSelector('button#submit[disabled]', { hidden: true }); await waitForNavigation(page, () => pressElement(page, 'button#submit')); diff --git a/test/public/qcFlags/forSimulationPassCreation.test.js b/test/public/qcFlags/forSimulationPassCreation.test.js index d2bb11448f..fec3581d65 100644 --- a/test/public/qcFlags/forSimulationPassCreation.test.js +++ b/test/public/qcFlags/forSimulationPassCreation.test.js @@ -103,7 +103,7 @@ module.exports = () => { await page.waitForSelector('input[type="time"]', { hidden: true, timeout: 250 }); await pressElement(page, '#flag-type-panel .popover-trigger'); - await pressElement(page, '#flag-type-dropdown-option-2', true); + await pressElement(page, '#flag-type-option-2', true); await page.waitForSelector('button#submit[disabled]', { hidden: true, timeout: 250 }); @@ -135,7 +135,7 @@ module.exports = () => { await page.waitForSelector('input[type="time"]', { hidden: true, timeout: 250 }); await pressElement(page, '#flag-type-panel .popover-trigger'); - await pressElement(page, '#flag-type-dropdown-option-11', true); + await pressElement(page, '#flag-type-option-11', true); await page.waitForSelector('button#submit[disabled]', { hidden: true, timeout: 250 }); await pressElement(page, '#time-based-toggle', true); @@ -175,7 +175,7 @@ module.exports = () => { await page.waitForSelector('button#submit[disabled]'); await page.waitForSelector('input[type="time"]', { hidden: true }); await pressElement(page, '#flag-type-panel .popover-trigger'); - await pressElement(page, '#flag-type-dropdown-option-2', true); + await pressElement(page, '#flag-type-option-2', true); await page.waitForSelector('button#submit[disabled]', { hidden: true }); await page.waitForSelector('.flex-row > .panel:nth-of-type(3) input[type="checkbox"]', { hidden: true }); diff --git a/test/public/qcFlags/gaqOverview.test.js b/test/public/qcFlags/gaqOverview.test.js index 2991d39b79..27b4089ad9 100644 --- a/test/public/qcFlags/gaqOverview.test.js +++ b/test/public/qcFlags/gaqOverview.test.js @@ -101,7 +101,7 @@ module.exports = () => { await waitForNavigation(page, () => pressElement(page, '#breadcrumb-data-pass-name a', true)); await waitForNavigation(page, () => pressElement(page, '#row106-EMC a', true)); await pressElement(page, '#flag-type-panel .popover-trigger', true); - await pressElement(page, '#flag-type-dropdown-option-3', true); + await pressElement(page, '#flag-type-option-3', true); await page.waitForSelector('button#submit[disabled]', { hidden: true, timeout: 250 }); await waitForNavigation(page, () => pressElement(page, 'button#submit', true)); diff --git a/test/public/runs/detail.test.js b/test/public/runs/detail.test.js index 8f726ade74..84da0298b9 100644 --- a/test/public/runs/detail.test.js +++ b/test/public/runs/detail.test.js @@ -25,7 +25,6 @@ const { waitForNavigation, expectLink, waitForTableLength, - getTableContent, getPopoverSelector, expectRowValues, } = require('../defaults.js'); @@ -98,10 +97,10 @@ module.exports = () => { it('successfully changed run tags in EDIT mode', async () => { await pressElement(page, '#edit-run'); await pressElement(page, '#tags .popover-trigger'); - await pressElement(page, '#tags-dropdown-option-CPV'); + await pressElement(page, '#tags-option-CPV'); await pressElement(page, '#save-run'); await pressElement(page, '#edit-run'); - await page.waitForSelector('#tags-dropdown-option-CPV:checked'); + await page.waitForSelector('#tags-option-CPV:checked'); await pressElement(page, '#cancel-run'); await page.waitForSelector('#edit-run'); }); @@ -272,8 +271,12 @@ module.exports = () => { await expectInnerText(page, '#mu-inelastic-interaction-rate', '0.009'); }); - it('can navigate to the flp panel', async () => { + it('should show lhc data in normal mode', async () => { await goToRunDetails(page, 1); + await expectInnerText(page, '#fill-number', 'Fill 5'); + }); + + it('can navigate to the flp panel', async () => { await waitForNavigation(page, () => pressElement(page, '#flps-tab')); await expectUrlParams(page, { page: 'run-detail', @@ -298,6 +301,31 @@ module.exports = () => { ); }); + it('should successfully navigate to the trigger counters panel', async () => { + await waitForNavigation(page, () => pressElement(page, '#ctp-trigger-counters-tab')); + expectUrlParams(page, { page: 'run-detail', runNumber: 1, panel: 'ctp-trigger-counters' }); + + await waitForTableLength(page, 2); + await expectRowValues( + page, + 1, + { + className: 'FIRST-CLASS-NAME', + lmb: '101', + lma: '102', + l0b: '103', + l0a: '104', + l1b: '105', + l1a: '106', + }, + ); + }); + + it('should successfully navigate to the trigger configuration panel', async () => { + await waitForNavigation(page, () => pressElement(page, '#trigger-configuration-tab')); + await expectInnerText(page, '#trigger-configuration-pane .panel', 'Raw\nTrigger\nConfiguration'); + }); + it('can navigate to the logs panel', async () => { await pressElement(page, '#logs-tab'); await page.waitForSelector('#logs-tab.active'); @@ -315,27 +343,6 @@ module.exports = () => { expectUrlParams(page, { page: 'log-detail', id: 1 }); }); - it('should successfully navigate to the trigger counters panel', async () => { - await goToRunDetails(page, 1); - - await pressElement(page, '#ctp-trigger-counters-tab'); - await waitForTableLength(page, 2); - expectUrlParams(page, { page: 'run-detail', runNumber: 1, panel: 'ctp-trigger-counters' }); - expect(await getTableContent(page)).to.deep.eql([ - ['FIRST-CLASS-NAME', '101', '102', '103', '104', '105', '106'], - ['SECOND-CLASS-NAME', '2,001', '2,002', '2,003', '2,004', '2,005', '2,006'], - ]); - }); - - it('should successfully navigate to the trigger configuration panel', async () => { - await pressElement(page, '#trigger-configuration-tab'); - await expectInnerText(page, '#trigger-configuration-pane .panel', 'Raw\nTrigger\nConfiguration'); - }); - - it('should show lhc data in normal mode', async () => { - await expectInnerText(page, '#fill-number', 'Fill 5'); - }); - it('successfully prevent from editing run quality of not ended runs', async () => { await goToRunDetails(page, 105); diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 12f92b648b..b75e399237 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -295,8 +295,8 @@ module.exports = () => { await page.waitForSelector('.detectors-filter .dropdown-trigger'); await pressElement(page, '.detectors-filter .dropdown-trigger'); - await pressElement(page, '#detector-filter-dropdown-option-ITS', true); - await pressElement(page, '#detector-filter-dropdown-option-FT0', true); + await pressElement(page, '#detector-filter-option-ITS', true); + await pressElement(page, '#detector-filter-option-FT0', true); await waitForTableLength(page, 4); table = await page.$$('tbody tr'); @@ -322,14 +322,14 @@ module.exports = () => { // Open filter toggle and wait for the dropdown to be visible await pressElement(page, '.tags-filter .dropdown-trigger'); - await pressElement(page, '#tag-dropdown-option-FOOD', true); - await pressElement(page, '#tag-dropdown-option-RUN', true); + await pressElement(page, '#tag-option-FOOD', true); + await pressElement(page, '#tag-option-RUN', true); await waitForTableLength(page, 1); await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); await pressElement(page, '.tags-filter .dropdown-trigger'); - await pressElement(page, '#tag-dropdown-option-RUN', true); - await pressElement(page, '#tag-dropdown-option-TEST-TAG-41', true); + await pressElement(page, '#tag-option-RUN', true); + await pressElement(page, '#tag-option-TEST-TAG-41', true); await waitForTableLength(page, 2); await pressElement(page, '#tag-filter-combination-operator-radio-button-none-of', true); @@ -340,7 +340,7 @@ module.exports = () => { it('should successfully filter on definition', async () => { await waitForTableTotalRowsCountToEqual(page, 108); - const filterInputSelectorPrefix = '#run-definition-checkbox-'; + const filterInputSelectorPrefix = '#run-definition-option-'; const physicsFilterSelector = `${filterInputSelectorPrefix}PHYSICS`; const cosmicsFilterSelector = `${filterInputSelectorPrefix}COSMICS`; const technicalFilterSelector = `${filterInputSelectorPrefix}TECHNICAL`; @@ -511,7 +511,7 @@ module.exports = () => { it('Should successfully filter runs by their run quality', async () => { await goToPage(page, 'run-overview'); - const filterInputSelectorPrefix = '#runQualityCheckbox'; + const filterInputSelectorPrefix = '#run-quality-option-'; const badFilterSelector = `${filterInputSelectorPrefix}bad`; const testFilterSelector = `${filterInputSelectorPrefix}test`; @@ -698,10 +698,10 @@ module.exports = () => { it('should successfully filter on a list of environment ids and inform the user about it', async () => { await waitForTableLength(page, 8); - const filterInputSelector = '#environmentIds'; + const filterInputSelector = '.environment-ids-filter'; expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. Dxi029djX, TDI59So3d...'); - await fillInput(page, filterInputSelector, 'Dxi029djX, TDI59So3d'); + await fillInput(page, filterInputSelector, 'Dxi029djX, TDI59So3d', ['change']); await waitForTableLength(page, 6); await pressElement(page, '#reset-filters'); @@ -711,8 +711,8 @@ module.exports = () => { await waitForTableLength(page, 8); await pressElement(page, '.runType-filter .dropdown-trigger'); - await pressElement(page, '#run-types-dropdown-option-2', true); - await pressElement(page, '#run-types-dropdown-option-14', true); + await pressElement(page, '#run-types-option-2', true); + await pressElement(page, '#run-types-option-14', true); await waitForTableLength(page, 5); await pressElement(page, '#reset-filters'); @@ -723,7 +723,7 @@ module.exports = () => { await expectInputValue(page, '#nDetectors-operator', '='); await page.select('#nDetectors-operator', '<='); - await fillInput(page, '#nDetectors-limit', '1'); + await fillInput(page, '#nDetectors-operand', '1', ['change']); await waitForTableLength(page, 6); const nDetectorsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { @@ -743,7 +743,7 @@ module.exports = () => { await expectInputValue(page, '#nFlps-operator', '='); await page.select('#nFlps-operator', '<='); - await fillInput(page, '#nFlps-limit', '10'); + await fillInput(page, '#nFlps-operand', '10', ['change']); await waitForTableLength(page, 5); const nFlpsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { @@ -761,7 +761,7 @@ module.exports = () => { await expectInputValue(page, '#nEpns-operator', '='); await page.select('#nEpns-operator', '<='); - await fillInput(page, '#nEpns-limit', '10'); + await fillInput(page, '#nEpns-operand', '10', ['change']); await waitForTableLength(page, 5); await expectColumnValues(page, 'nEpns', ['10', '10', 'OFF', 'OFF', '10']); @@ -937,7 +937,7 @@ module.exports = () => { // Second export // Apply filtering - const filterInputSelectorPrefix = '#runQualityCheckbox'; + const filterInputSelectorPrefix = '#run-quality-option-'; const badFilterSelector = `${filterInputSelectorPrefix}bad`; await pressElement(page, '#openFilterToggle'); diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 2425d39f96..6693c26b5f 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -327,7 +327,7 @@ module.exports = () => { await pressElement(page, '#openFilterToggle'); await pressElement(page, '.detectors-filter .dropdown-trigger'); - await pressElement(page, '#detector-filter-dropdown-option-CPV', true); + await pressElement(page, '#detector-filter-option-CPV', true); await expectColumnValues(page, 'runNumber', ['2', '1']); await pressElement(page, '#reset-filters'); @@ -340,8 +340,8 @@ module.exports = () => { await pressElement(page, '.tags-filter .dropdown-trigger'); - await pressElement(page, '#tag-dropdown-option-FOOD', true); - await pressElement(page, '#tag-dropdown-option-RUN', true); + await pressElement(page, '#tag-option-FOOD', true); + await pressElement(page, '#tag-option-RUN', true); await expectColumnValues(page, 'runNumber', ['106']);