Skip to content

Commit

Permalink
Add DateRangeFilter
Browse files Browse the repository at this point in the history
Signed-off-by: Radoslaw Szwajkowski <rszwajko@redhat.com>
  • Loading branch information
rszwajko committed May 14, 2024
1 parent f501fb8 commit 72c3e80
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 0 deletions.
133 changes: 133 additions & 0 deletions client/src/app/components/FilterToolbar/DateRangeFilter.tsx
Original file line number Diff line number Diff line change
@@ -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**:<br>
* 1) filterValue - date range encoded as ISO 8601 time interval string ("dateFrom/dateTo"). Only date part is used (no time).<br>
* 2) setFilterValue - accepts the list of ranges.<br>
*
*/

export const DateRangeFilter = <TItem,>({
category,
filterValue,
setFilterValue,
showToolbarItem,
isDisabled = false,
}: React.PropsWithChildren<
IFilterControlProps<TItem, string>
>): JSX.Element | null => {
const selectedFilters = filterValue ?? [];

const validFilters =
selectedFilters?.filter((interval) =>
isValidInterval(parseInterval(interval))
) ?? [];
const [from, setFrom] = useState<Date>();
const [to, setTo] = useState<Date>();

const rangeToOption = (range: string) => {
const abbr = abbreviateInterval(range);
return {
key: range,
node: (
<Tooltip content={range}>
<span>{abbr ?? ""}</span>
</Tooltip>
),
};
};

const clearSingleRange = (
category: string | ToolbarChipGroup,
option: string | ToolbarChip
) => {
const target = (option as ToolbarChip)?.key;
setFilterValue([...validFilters.filter((range) => range !== target)]);
};

const onFromDateChange = (
event: FormEvent<HTMLInputElement>,
value: string
) => {
if (isValidDate(value)) {
setFrom(dateParse(value));
setTo(undefined);
}
};

const onToDateChange = (even: FormEvent<HTMLInputElement>, 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 (
<ToolbarFilter
key={category.categoryKey}
chips={validFilters.map(rangeToOption)}
deleteChip={clearSingleRange}
deleteChipGroup={() => setFilterValue([])}
categoryName={category.title}
showToolbarItem={showToolbarItem}
>
<InputGroup>
<DatePicker
value={from ? dateFormat(from) : ""}
dateFormat={dateFormat}
dateParse={dateParse}
onChange={onFromDateChange}
aria-label="Interval start"
placeholder="MM/DD/YYYY"
// disable error text (no space in toolbar scenario)
invalidFormatText={""}
// default value ("parent") creates collision with sticky table header
appendTo={document.body}
isDisabled={isDisabled}
/>
<DatePicker
value={to ? dateFormat(to) : ""}
onChange={onToDateChange}
isDisabled={isDisabled || !isValidJSDate(from)}
// disable error text (no space in toolbar scenario)
invalidFormatText={""}
rangeStart={from}
aria-label="Interval end"
placeholder="MM/DD/YYYY"
appendTo={document.body}
/>
</InputGroup>
</ToolbarFilter>
);
};
4 changes: 4 additions & 0 deletions client/src/app/components/FilterToolbar/FilterControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { SelectFilterControl } from "./SelectFilterControl";
import { SearchFilterControl } from "./SearchFilterControl";
import { MultiselectFilterControl } from "./MultiselectFilterControl";
import { DateRangeFilter } from "./DateRangeFilter";

export interface IFilterControlProps<TItem, TFilterCategoryKey extends string> {
category: FilterCategory<TItem, TFilterCategoryKey>;
Expand Down Expand Up @@ -58,5 +59,8 @@ export const FilterControl = <TItem, TFilterCategoryKey extends string>({
/>
);
}
if (category.type === FilterType.dateRange) {
return <DateRangeFilter category={category} {...props} />;
}
return null;
};
1 change: 1 addition & 0 deletions client/src/app/components/FilterToolbar/FilterToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum FilterType {
multiselect = "multiselect",
search = "search",
numsearch = "numsearch",
dateRange = "dateRange",
}

export type FilterValue = string[] | undefined | null;
Expand Down
30 changes: 30 additions & 0 deletions client/src/app/components/FilterToolbar/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -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")}`;
};

0 comments on commit 72c3e80

Please sign in to comment.