diff --git a/client/src/app/components/FilterToolbar/DateRangeFilter.tsx b/client/src/app/components/FilterToolbar/DateRangeFilter.tsx new file mode 100644 index 000000000..be39d1604 --- /dev/null +++ b/client/src/app/components/FilterToolbar/DateRangeFilter.tsx @@ -0,0 +1,133 @@ +import React, { FormEvent, useState } from "react"; + +import { + DatePicker, + InputGroup, + isValidDate as isValidJSDate, + ToolbarChip, + ToolbarChipGroup, + ToolbarFilter, + Tooltip, +} from "@patternfly/react-core"; + +import { IFilterControlProps } from "./FilterControl"; +import { + abbreviateInterval, + dateFormat, + dateParse, + isValidDate, + isValidInterval, + parseInterval, + toISODateInterval, +} from "./dateUtils"; + +/** + * This Filter type enables selecting an closed date range. + * Precisely given range [A,B] a date X in the range if A <= X <= B. + * + * **Props are interpreted as follows**:
+ * 1) filterValue - date range encoded as ISO 8601 time interval string ("dateFrom/dateTo"). Only date part is used (no time).
+ * 2) setFilterValue - accepts the list of ranges.
+ * + */ + +export const DateRangeFilter = ({ + category, + filterValue, + setFilterValue, + showToolbarItem, + isDisabled = false, +}: React.PropsWithChildren< + IFilterControlProps +>): JSX.Element | null => { + const selectedFilters = filterValue ?? []; + + const validFilters = + selectedFilters?.filter((interval) => + isValidInterval(parseInterval(interval)) + ) ?? []; + const [from, setFrom] = useState(); + const [to, setTo] = useState(); + + const rangeToOption = (range: string) => { + const abbr = abbreviateInterval(range); + return { + key: range, + node: ( + + {abbr ?? ""} + + ), + }; + }; + + const clearSingleRange = ( + category: string | ToolbarChipGroup, + option: string | ToolbarChip + ) => { + const target = (option as ToolbarChip)?.key; + setFilterValue([...validFilters.filter((range) => range !== target)]); + }; + + const onFromDateChange = ( + event: FormEvent, + value: string + ) => { + if (isValidDate(value)) { + setFrom(dateParse(value)); + setTo(undefined); + } + }; + + const onToDateChange = (even: FormEvent, value: string) => { + if (isValidDate(value)) { + const newTo = dateParse(value); + setTo(newTo); + const target = toISODateInterval(from, newTo); + if (target) { + setFilterValue([ + ...validFilters.filter((range) => range !== target), + target, + ]); + } + } + }; + + return ( + setFilterValue([])} + categoryName={category.title} + showToolbarItem={showToolbarItem} + > + + + + + + ); +}; diff --git a/client/src/app/components/FilterToolbar/FilterControl.tsx b/client/src/app/components/FilterToolbar/FilterControl.tsx index 52a0f73b4..71940b608 100644 --- a/client/src/app/components/FilterToolbar/FilterControl.tsx +++ b/client/src/app/components/FilterToolbar/FilterControl.tsx @@ -11,6 +11,7 @@ import { import { SelectFilterControl } from "./SelectFilterControl"; import { SearchFilterControl } from "./SearchFilterControl"; import { MultiselectFilterControl } from "./MultiselectFilterControl"; +import { DateRangeFilter } from "./DateRangeFilter"; export interface IFilterControlProps { category: FilterCategory; @@ -58,5 +59,8 @@ export const FilterControl = ({ /> ); } + if (category.type === FilterType.dateRange) { + return ; + } return null; }; diff --git a/client/src/app/components/FilterToolbar/FilterToolbar.tsx b/client/src/app/components/FilterToolbar/FilterToolbar.tsx index eaa169a2d..0e0af511c 100644 --- a/client/src/app/components/FilterToolbar/FilterToolbar.tsx +++ b/client/src/app/components/FilterToolbar/FilterToolbar.tsx @@ -18,6 +18,7 @@ export enum FilterType { multiselect = "multiselect", search = "search", numsearch = "numsearch", + dateRange = "dateRange", } export type FilterValue = string[] | undefined | null; diff --git a/client/src/app/components/FilterToolbar/dateUtils.ts b/client/src/app/components/FilterToolbar/dateUtils.ts new file mode 100644 index 000000000..cd74a69b1 --- /dev/null +++ b/client/src/app/components/FilterToolbar/dateUtils.ts @@ -0,0 +1,30 @@ +import dayjs from "dayjs"; + +export const isValidDate = (val: string) => dayjs(val).isValid(); + +export const dateFormat = (val: Date) => dayjs(val).format("MM/DD/YYYY"); + +export const dateParse = (val: string) => dayjs(val).toDate(); + +// i.e.'1970-01-01/1970-01-01' +export const toISODateInterval = (from?: Date, to?: Date) => { + const [start, end] = [dayjs(from), dayjs(to)]; + if (!isValidInterval([start, end])) { + return undefined; + } + return `${start.format("YYYY-MM-dd")}/${end.format("YYYY-MM-dd")}`; +}; + +export const parseInterval = (interval: string): dayjs.Dayjs[] => + interval?.split("/").map(dayjs) ?? []; + +export const isValidInterval = ([from, to]: dayjs.Dayjs[]) => + from?.isValid() && to?.isValid() && from?.isSameOrBefore(to); + +export const abbreviateInterval = (interval: string) => { + const [start, end] = parseInterval(interval); + if (!isValidInterval([start, end])) { + return undefined; + } + return `${start.format("MM-dd")}/${end.format("MM-dd")}`; +};