diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index a0c3b17193..79bc54e501 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -14,6 +14,7 @@ import { Observable } from '/js/src/index.js'; /** * Model storing the state of a given filter + * * @abstract */ export class FilterModel extends Observable { @@ -49,7 +50,7 @@ export class FilterModel extends Observable { /** * Returns the normalized value of the filter, that can be used as URL parameter * - * @return {string|number|object|array|null} the normalized value + * @return {string|number|object|string[]|number[]|null} the normalized value * @abstract */ get normalized() { diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js new file mode 100644 index 0000000000..d8f46cb932 --- /dev/null +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -0,0 +1,145 @@ +/** + * @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 } from '/js/src/index.js'; +import { ToggleableModel } from '../../common/toggle/TogglableModel.js'; + +/** + * Model representing a filtering system, including filter inputs visibility, filters values and so on + */ +export class FilteringModel extends Observable { + /** + * Constructor + * + * @param {Map} filters the filter models indexed by their unique key + */ + constructor(filters) { + super(); + + this._visualChange$ = new Observable(); + + this._toggleModel = new ToggleableModel(); + this._toggleModel.bubbleTo(this._visualChange$); + + /** + * @type {Map} + * @private + */ + this._filtersMeta = new Map(); + for (const propertyKey in filters) { + this._addFilter(propertyKey, filters[propertyKey]); + } + + this._filtersStore = filters; + } + + /** + * Reset the filters + * + * @return {void} + */ + reset() { + this._filtersMeta.forEach(({ filter }) => filter.reset()); + } + + /** + * Returns the normalized value of all the filters, without null values + * + * @return {Object} the normalized values + */ + get normalized() { + const ret = {}; + for (const [filterKey, { filter }] of this._filtersMeta) { + if (!filter.isEmpty) { + ret[filterKey] = filter.normalized; + } + } + return ret; + } + + /** + * States if there is currently at least one filter active + * + * @return {boolean} true if at least one filter is active + */ + isAnyFilterActive() { + for (const [, { filter }] of this._filtersMeta) { + if (!filter.isEmpty) { + return true; + } + } + return false; + } + + /** + * Returns the list of human-readable names of currently active filters + * + * @return {string} the active filters names + */ + get activeFiltersNames() { + const ret = []; + for (const [, { filter, humanName }] of this._filtersMeta) { + if (!filter.isEmpty) { + ret.push(humanName); + } + } + return ret.join(', '); + } + + /** + * Returns the observable notified any time there is a visual change which has no impact on the actual filtering + * + * @return {Observable} the filters visibility observable + */ + get visualChange$() { + return this._visualChange$; + } + + /** + * Returns the object storing all the filters models + * + * @return {Object} the filters store + */ + get filters() { + return this._filtersStore; + } + + /** + * The visibility state of the filters popup + * + * @return {ToggleableModel} the toggle model + */ + get toggleModel() { + return this._toggleModel; + } + + /** + * Add a filter to the list of registered filters, and bubble filters events (global and visual) to this model + * + * @param {string} filterKey the key of the filter, used to normalize filtering request + * @param {FilterModel} filter the filter model + * @return {void} + * @private + */ + _addFilter(filterKey, filter) { + this._filtersMeta.set( + filterKey, + { + filter, + humanName: `${filterKey[0].toUpperCase()}${filterKey.slice(1).replaceAll(/([A-Z])/g, ' $1').toLowerCase()}`, + }, + ); + filter.bubbleTo(this); + filter.visualChange$.bubbleTo(this._visualChange$); + } +} diff --git a/lib/public/components/Filters/common/filters/remoteDataTagFilter.js b/lib/public/components/Filters/common/filters/remoteDataTagFilter.js deleted file mode 100644 index 89f50cb4f4..0000000000 --- a/lib/public/components/Filters/common/filters/remoteDataTagFilter.js +++ /dev/null @@ -1,34 +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 spinner from '../../../common/spinner.js'; -import { tagFilter } from './tagFilter.js'; - -/** - * Return a filter component to apply filtering on a remote data list of tags, handling each possible remote data status - * - * @param {RemoteData} tagsRemoteData the remote data tags list - * @param {TagFilterModel} filter The model storing the filter's state - * - * @return {vnode|vnode[]|null} A collection of checkboxes to toggle table rows by tags - */ -export const remoteDataTagFilter = (tagsRemoteData, filter) => tagsRemoteData.match({ - NotAsked: () => null, - Loading: () => spinner({ - size: 2, - justify: 'left', - absolute: false, - }), - Success: (tags) => tagFilter(tags, filter), - Failure: () => null, -}); diff --git a/lib/public/utilities/serializeQueryParameters.js b/lib/public/utilities/serializeQueryParameters.js new file mode 100644 index 0000000000..6a9ba73404 --- /dev/null +++ b/lib/public/utilities/serializeQueryParameters.js @@ -0,0 +1,69 @@ +/** + * @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. + */ + +/** + * Given a value and a query param prefix, returns a list of key => values representing the corresponding query parameters (null or undefined + * values are dropped) + * + * for example [1, 3] with prefix 'myPrefix' will result in [{key: 'myPrefix[]', value: 1}, {key: 'myPrefix[]', value: 3] + * for example {foo: 1, bar: 3} with prefix 'myPrefix' will result in [{key: 'myPrefix[foo]', value: 1}, {key: 'myPrefix[bar]', value: 3] + * + * @param {string|boolean|number|null|array|object} parameters the parameter to convert to query param + * @param {string} key the query parameter's key + * @return {({key: string, value: (string|number)}|null)[]} the query parameters definition + */ +export const serializeQueryParameters = (parameters, key) => { + if (parameters === null || parameters === undefined) { + return [null]; + } + + if (Array.isArray(parameters)) { + return parameters.map((parameter) => serializeQueryParameters(parameter, `${key}[]`)).flat(); + } + + switch (typeof parameters) { + case 'boolean': + return [{ key, value: parameters ? 'true' : 'false' }]; + case 'number': + case 'string': + return [{ key, value: parameters }]; + case 'object': + return Object.entries(parameters) + .map(([parameterKey, parameter]) => serializeQueryParameters(parameter, `${key}[${parameterKey}]`)) + .flat(); + default: + return [null]; + } +}; + +/** + * Generate a {URLSearchParams} from an object representing the query parameters + * + * Parameters can be nested ({foo: {bar: 23}}) and values can be an array ({foo: ['bar', 'baz']}) + * + * @param {Object} parameters the query parameters + * @return {URLSearchParams} the generated search params + */ +export const generateURLSearchParams = (parameters) => { + const ret = new URLSearchParams(); + + for (const mainKey in parameters) { + const serializedQueryParameters = serializeQueryParameters(parameters[mainKey], mainKey); + for (const serializedQueryParameter of serializedQueryParameters) { + if (serializedQueryParameter) { + ret.append(serializedQueryParameter.key, serializedQueryParameter.value); + } + } + } + return ret; +};