Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions lib/public/components/Filters/common/FilteringModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { expandQueryLikeNestedKey } from '../../../utilities/expandNestedKey.js';
import { SelectionModel } from '../../common/selection/SelectionModel.js';
import { FilterModel } from './FilterModel.js';
import { buildUrl, Observable } from '/js/src/index.js';
import { buildUrl, Observable, parseUrlParameters } from '/js/src/index.js';

/**
* Model representing a filtering system, including filter inputs visibility, filters values and so on
Expand Down Expand Up @@ -142,12 +142,35 @@ export class FilteringModel extends Observable {
this.notify();
}

/**
* Compute seach parameters based a url or router
*
* @param {string|null} [url=null] the url that is to be parsed
* @returns {object} the serach parameters object
*/
_computeParameters(url = null) {
if (url) {
try {
return parseUrlParameters(new URL(url).searchParams);
} catch {
this._warnings.set('Unparseable URL', `URL could not be parsed. URL: ${url}`);
this.notify();
return {};
}
}

return this._router.params;
}

/**
* Look for parameters used for filtering in URL and apply them in the layout if it exists
*
* @param {string|null} [url=null] the url that is to be parsed into active filters
* @returns {undefined}
*/
async setFilterFromURL() {
const { params: { page = '', filter = {} } } = this._router;
setFilterFromURL(url = null) {
const params = this._computeParameters(url);
const { page, filter = {} } = params;

if (!(this._pageIdentifier === page)) {
return;
Expand All @@ -172,6 +195,10 @@ export class FilteringModel extends Observable {
this._warnings.delete('Unknown Filters');
}

if (url) {
this._router.go(buildUrl('?', params), false, true);
}

this.notify();
}

Expand Down
102 changes: 88 additions & 14 deletions lib/public/components/Filters/common/filtersPanelPopover.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
import { h, info, popover, PopoverAnchors, PopoverTriggerPreConfiguration } from '/js/src/index.js';
import { h, info, popover, PopoverAnchors, PopoverTriggerPreConfiguration, DropdownComponent, CopyToClipboardComponent } from '/js/src/index.js';
import { iconCaretBottom } from '/js/src/icons.js';
import { profiles } from '../../common/table/profiles.js';
import { applyProfile } from '../../../utilities/applyProfile.js';
import { tooltip } from '../../common/popover/tooltip.js';
Expand All @@ -35,7 +36,24 @@ import { tooltip } from '../../common/popover/tooltip.js';
*
* @return {Component} the button component
*/
const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary', 'Filters');
const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary.first-item', 'Filters');

/**
* Button component that resets all filters upon click
*
* @param {FilteringModel|OverviewPageModel} filteringModel the FilteringModel
* @returns {Component} the reset button component
*/
const resetFiltersButton = (filteringModel) => h(
'button#reset-filters.btn.btn-danger',
{
disabled: !filteringModel.isAnyFilterActive(),
onclick: () => filteringModel.resetFiltering
? filteringModel.resetFiltering(true, true)
: filteringModel.reset(true, true),
},
'Reset all filters',
);

/**
* Create main header of the filters panel
Expand All @@ -44,16 +62,7 @@ const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primar
*/
const filtersToggleContentHeader = (filteringModel) => h('.flex-row.justify-between', [
h('.f4', 'Filters'),
h(
'button#reset-filters.btn.btn-danger',
{
onclick: () => filteringModel.resetFiltering
? filteringModel.resetFiltering(true, true)
: filteringModel.reset(true, true),
disabled: !filteringModel.isAnyFilterActive(),
},
'Reset all filters',
),
resetFiltersButton(filteringModel),
]);

/**
Expand Down Expand Up @@ -114,13 +123,78 @@ const filtersToggleContent = (
* @param {FiltersConfiguration} filtersConfiguration filters configuration
* @param {object} [configuration] optional configuration
* @param {string} [configuration.profile] specify for which profile filtering should be enabled
* @return {Component} the filter component
* @return {Component} the filter button component
*/
export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => popover(
const filtersPanelButton = (filteringModel, filtersConfiguration, configuration) => popover(
filtersToggleTrigger(),
filtersToggleContent(filteringModel, filtersConfiguration, configuration),
{
...PopoverTriggerPreConfiguration.click,
anchor: PopoverAnchors.RIGHT_START,
},
);

/**
* A button component that lets the user copy the url if there are active filters.
*
* @param {boolean} activeFilters if false, will disable the button
* @returns {Component} the copy button component
*/
const copyButtonOption = (activeFilters) => h(
'',
{ style: activeFilters ? {} : { opacity: 0.5, pointerEvents: 'none' } },
h(CopyToClipboardComponent, { value: location.href, id: 'filter' }, 'Copy Active Filters'),
);

/**
* A button component that lets the user paste the first entry of their clipboard as a filter url.
*
* @param {FilteringModel|OverviewPageModel} model the FilteringModel
* @returns {Component} the paste button component
*/
const pasteButtonOption = (model) => {
const clipboardSupported = navigator?.clipboard && window.isSecureContext;

// Sometimes, the overview model is passed to filterPanelPopover instead of the filteringmodel (e.g. envirionments)
const { filteringModel = model } = model;

return h('.btn.btn-primary', {
onclick: async () => {
const url = await navigator.clipboard.readText();
filteringModel.setFilterFromURL(url);
},
disabled: !clipboardSupported,
id: 'paste-filter',
}, 'Paste filters');
};

/**
* Return component composed of the filter popover button and a dropdown trigger
*
* @param {FilteringModel} filteringModel the filtering model
* @param {FiltersConfiguration} filtersConfiguration filters configuration
* @param {object} [configuration] optional configuration
* @param {string} [configuration.profile] specify for which profile filtering should be enabled
* @return {Component} the filter component
*/
export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => {
const hasActiveFilters = filteringModel.isAnyFilterActive();

return h(
'.flex-row.items-center.btn-group',
[
filtersPanelButton(filteringModel, filtersConfiguration, configuration),
DropdownComponent(
h('.btn.btn-group-item.last-item', iconCaretBottom()),
h(
'.flex-column.p2.g2',
[
copyButtonOption(hasActiveFilters),
pasteButtonOption(filteringModel),
resetFiltersButton(filteringModel),
],
),
),
],
);
};
86 changes: 86 additions & 0 deletions test/public/components/filtersPopoverPanel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

const { expect } = require('chai');
const { defaultBefore, defaultAfter, pressElement, takeScreenshot, expectInputValue } = require('../defaults.js');

module.exports = () => {
let page;
let browser;
let context;
let url;

before(async () => {
[page, browser, url] = await defaultBefore(page, browser);
context = browser.defaultBrowserContext();
context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']);
});

it('Should copy url when clicking filer copy button', async () => {
const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb';
await page.goto(url, { waitUntil: 'load' });
await takeScreenshot(page, 'test');
await pressElement(page, '#copy-filter', true);

const clipboardContents = await page.evaluate(async () => decodeURI(await navigator.clipboard.readText()));
expect(clipboardContents).to.equal(url);
});

it('Should set filters when pressing paste active filters button', async () => {
const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb';

await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url);
await pressElement(page, '#paste-filter', true);

const actualUrl = page.url();
expect(actualUrl).to.equal(url);

await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'name');
await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '100');
await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb');
});

it('Should set filters when pressing paste active filters button', async () => {
const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb';

await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url);
await pressElement(page, '#paste-filter', true);

const actualUrl = page.url();
expect(actualUrl).to.equal(url);

await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'name');
await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '100');
await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb');
});

it('Should reset filters when pressing the reset all filters button', async () => {
const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb';

await page.goto(url, { waitUntil: 'load' });

await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url);
await pressElement(page, '.dropdown #reset-filters', true);

const actualUrl = page.url();
expect(actualUrl).to.equal('http://localhost:4000/?page=lhc-period-overview');

await expectInputValue(page, '.name-filter input', '');
await expectInputValue(page, '.year-filter input', '');
await expectInputValue(page, '.pdpBeamTypes-filter input', '');
});

after(async () => {
await defaultAfter(page, browser);
});
};
2 changes: 2 additions & 0 deletions test/public/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

const NavBarSuite = require('./navBar.test')
const WarningSuite = require('./warnings.test')
const FiltersPanelSuite = require('./filtersPopoverPanel.test')

module.exports = () => {
describe('Navbar component', NavBarSuite);
describe('Warning component', WarningSuite)
describe('FiltersPanelPopover component', FiltersPanelSuite)
};
Loading