Skip to content

Commit

Permalink
[O2B-1043] Add pre-filled options for time range filter (#1242)
Browse files Browse the repository at this point in the history
* [O2B-1043] Add pre-filled options for time range filter

* Fix normalized value

* Fix titles

* lint

---------

Co-authored-by: xsalonx <[email protected]>
Co-authored-by: xsalonx <[email protected]>
  • Loading branch information
3 people authored Nov 23, 2023
1 parent a34a5d4 commit fbc5024
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 142 deletions.
18 changes: 17 additions & 1 deletion lib/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ html, body {
scroll-behavior: smooth;

--ui-component-medium: 550px;
--ui-component-large: 850px;
}

.cell-xs { max-width: 2rem; }
Expand Down Expand Up @@ -122,6 +123,10 @@ html, body {
gap: var(--space-xl)
}

.flex-grid {
display: grid;
}

/* Float */
.float-right { float: right }

Expand Down Expand Up @@ -475,7 +480,7 @@ label {
top: 0;
left: 0;
z-index: 1002;
max-width: var(--ui-component-medium);
max-width: var(--ui-component-large);
}

.no-events {
Expand Down Expand Up @@ -621,6 +626,17 @@ label {
stroke-width: 0.5px;
}

/* Custom styles */
.time-range-selector .month-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
}

.time-range-selector .month-option:nth-last-child(-n+3), .time-range-selector .dropdown-option:last-child {
border-bottom: 1px solid var(--color-gray-dark);
}


/**
* Breakpoints :
* small : x < 600 (default styles)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ export class TimeRangeFilterModel extends FilterModel {
* Constructor
*
* @param {Partial<Period>} [value] the initial value of the filter
* @param {string|null} [periodLabel] the label of the initial value if it applies (eg. current month)
* @param {object} [configuration] the inputs configuration
* @param {(boolean|{from: boolean, to: boolean})} [configuration.required] defines if the from/to dates are required (true means both are
* required)
*/
constructor(value, configuration) {
constructor(value, periodLabel, configuration) {
super();

const { required = false } = configuration || {};
Expand All @@ -56,27 +57,30 @@ export class TimeRangeFilterModel extends FilterModel {
this._fromTimeInputModel.visualChange$.bubbleTo(this.visualChange$);
// eslint-disable-next-line no-return-assign
this._fromTimeInputModel.observe(() => {
this._periodLabel = null;
this._toTimeInputModel.min = this._fromTimeInputModel.value ? this._fromTimeInputModel.value + TIME_STEP : null;
});
this._fromTimeInputModel.bubbleTo(this);

this._toTimeInputModel.visualChange$.bubbleTo(this.visualChange$);
// eslint-disable-next-line no-return-assign
this._toTimeInputModel.observe(() => {
this._periodLabel = null;
this._fromTimeInputModel.max = this._toTimeInputModel.value ? this._toTimeInputModel.value - TIME_STEP : null;
});
this._toTimeInputModel.bubbleTo(this);

this.setValue(value, false);
this.setValue(value, periodLabel, false);
}

/**
* Set the current value of the filter
* @param {Partial<Period>} value the new value of the filter
* @param {string|null} periodLabel if the specified value correspond to a specific period (fox example current month), this is its label
* @param {boolean} [silent] if true, the observers will not be notified
* @return {void}
*/
setValue(value, silent = false) {
setValue(value, periodLabel = null, silent = false) {
const { from, to } = value || {};

if (from === this._fromTimeInputModel.value && to === this._toTimeInputModel.value) {
Expand All @@ -92,6 +96,8 @@ export class TimeRangeFilterModel extends FilterModel {
this._fromTimeInputModel.max = to - TIME_STEP;
}

this._periodLabel = periodLabel;

!silent && this.notify();
}

Expand All @@ -113,6 +119,14 @@ export class TimeRangeFilterModel extends FilterModel {
return this._toTimeInputModel;
}

/**
* If it applies, returns the label of the currently selected period (eg. current month)
* @return {string|null} the period label
*/
get periodLabel() {
return this._periodLabel;
}

// eslint-disable-next-line valid-jsdoc
/**
* @inheritDoc
Expand Down
126 changes: 102 additions & 24 deletions lib/public/components/Filters/common/filters/timeRangeFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,78 @@
import { dropdown } from '../../../common/popover/dropdown.js';
import { h } from '/js/src/index.js';
import { dateTimeInput } from '../../../common/form/inputs/dateTimeInput.js';
import { getLocaleDateAndTime } from '../../../../utilities/dateUtils.js';
import {
getLocaleDateAndTime,
getStartOfMonth,
getStartOfWeek,
getStartOfYear,
getWeeksCount,
MONTH_NAMES,
} from '../../../../utilities/dateUtils.js';
import { formatTimeRange } from '../../../common/formatting/formatTimeRange.js';

/**
* Format the current timestamp range display
* Amount of years to display as pre-defined options
*
* @param {Partial<Period>} the current period
* @return {Component} the resulting component
* @type {number}
*/
const formatTimeRange = ({ from, to }) => {
let parts = [];
const YEARS_OPTIONS_COUNT = 3;

// eslint-disable-next-line require-jsdoc
const formatTimestamp = (timestamp) => {
const { date, time } = getLocaleDateAndTime(timestamp);
return h('.flex-column.g1', [h('.f6', date), h('', time)]);
};
/**
* Returns a pre-defined list of years period selectors
*
* @param {function} onChange function called with the clicked period and its label
* @return {Component} the list of years selectors
*/
const yearsOptions = ({ onChange }) => {
const currentYear = new Date().getFullYear();
return h('', new Array(YEARS_OPTIONS_COUNT).fill(0).map((_, index) => {
const year = currentYear - YEARS_OPTIONS_COUNT + index + 1;
return h(
'.dropdown-option.ph3.pv2',
{
onclick: () => onChange(
{
from: getStartOfYear(year).getTime(),
to: getStartOfYear(year + 1).getTime(),
},
year,
),
},
year,
);
}));
};

// eslint-disable-next-line require-jsdoc
const label = (content) => h('.gray-darker', content);
/**
* Return a pre-defined list of months period selectors
*
* @param {function} onChange function called with the clicked period and its label
* @return {Component} the list of months selectors
*/
const monthsOptions = ({ onChange }) => {
const now = new Date();
const currentMonthIndex = now.getUTCMonth();
const currentYear = now.getFullYear();

if (from === undefined && to === undefined) {
parts = '-';
} else if (from === undefined) {
parts = [label('Before'), formatTimestamp(to)];
} else if (to === undefined) {
parts = [label('After'), formatTimestamp(from)];
} else {
parts = [label('From'), formatTimestamp(from), label('to'), formatTimestamp(to)];
}
return h('.month-options', new Array(MONTH_NAMES.length).fill(0).map((_, index) => {
const monthIndex = (index + currentMonthIndex + 1) % MONTH_NAMES.length;
const year = monthIndex > currentMonthIndex ? currentYear - 1 : currentYear;
let label = MONTH_NAMES[monthIndex];
if (currentYear !== year) {
label += ` (${year})`;
}

return h('.flex-row.items-center.text-center.g3', parts);
return h('.dropdown-option.month-option.ph3.pv2', {
onclick: () => onChange(
{
from: getStartOfMonth(monthIndex + 1, year).getTime(),
to: getStartOfMonth(monthIndex + 2, year).getTime(),
},
label,
),
}, label);
}));
};

/**
Expand All @@ -56,7 +97,17 @@ export const timeRangeFilter = (timeRangeFilterModel) => dropdown(
h(
`.dropdown-trigger.form-control${timeRangeFilterModel.isValid ? '' : '.invalid'}`,
[
h('.flex-grow', formatTimeRange(timeRangeFilterModel.normalized)),
h('.flex-grow', formatTimeRange(
timeRangeFilterModel.normalized,
{
formatTimestamp: (timestamp) => {
const { date, time } = getLocaleDateAndTime(timestamp);
return h('.flex-column.g1', [h('.f6', date), h('', time)]);
},
formatText: (content) => h('.gray-darker', content),
formatParts: (parts) => h('.flex-row.items-center.text-center.g3', parts),
},
)),
h('.dropdown-trigger-symbol', ''),
],
),
Expand All @@ -71,5 +122,32 @@ export const timeRangeFilter = (timeRangeFilterModel) => dropdown(
dateTimeInput(timeRangeFilterModel.toTimeInputModel),
]),
]),
h(
'.flex-column.dropdown-options.text-center',
{ style: { 'border-left': '1px solid var(--color-gray)' } },
[
yearsOptions({ onChange: (period, label) => timeRangeFilterModel.setValue(period, label) }),
monthsOptions({ onChange: (period, label) => timeRangeFilterModel.setValue(period, label) }),
h(
'.dropdown-option.flex-row.g2.items-center.ph3.pv2',
[
'Week ',
h('input.form-control', {
type: 'number',
step: 1,
min: 1,
max: getWeeksCount(),
onchange: (e) => timeRangeFilterModel.setValue(
{
from: getStartOfWeek(e.target.value).getTime(),
to: getStartOfWeek(parseInt(e.target.value, 10) + 1).getTime(),
},
`Week ${e.target.value}`,
),
}),
],
),
],
),
]),
);
47 changes: 47 additions & 0 deletions lib/public/components/common/formatting/formatTimeRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @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 { formatTimestamp as defaultFormatTimestamp } from '../../../utilities/formatting/formatTimestamp.js';

/**
* Format the current timestamp range display
*
* @param {Partial<Period>} period the current period
* @param {object} [configuration] the formatting configuration
* @param {function} [configuration.formatTimestamp] the function to format the period's timestamps
* @param {function} [configuration.formatText] the function to format the time-range texts, such as `From`, `to`...
* @param {function} [configuration.formatParts] the function to format all the formatted timeRange parts, which are the timestamps and the
* keywords, for example [`From`, `2023-01-01`]
* @return {Component} the resulting component
*/
export const formatTimeRange = ({ from, to }, configuration) => {
const {
formatTimestamp = (timestamp) => defaultFormatTimestamp(timestamp, true),
formatText = (content) => content,
formatParts = (parts) => parts.join(' '),
} = configuration || {};

let parts = [];

if (from === undefined && to === undefined) {
parts = '-';
} else if (from === undefined) {
parts = [formatText('Before'), formatTimestamp(to)];
} else if (to === undefined) {
parts = [formatText('After'), formatTimestamp(from)];
} else {
parts = [formatText('From'), formatTimestamp(from), formatText('to'), formatTimestamp(to)];
}

return formatParts(parts);
};
Loading

0 comments on commit fbc5024

Please sign in to comment.