Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui): add local storage to filters #1113

Merged
merged 20 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7ded6a2
renamed some variables; added some comments
andieswift Jan 29, 2025
83a3f01
added local storage/fixed weird UI bug
andieswift Feb 4, 2025
4680a30
Merge branch 'main' into filter-state
andieswift Feb 4, 2025
6a073d6
mocked local storage
andieswift Feb 4, 2025
7016886
Merge branch 'main' into filter-state
andieswift Feb 4, 2025
8c25cc1
updated parsing logic to be after getting storage/removed comment
andieswift Feb 4, 2025
b07bfe4
added tests for hooks useEffect where local storage logic was added
andieswift Feb 4, 2025
7c8992c
removed elint ignore
andieswift Feb 4, 2025
318c9d5
removed ignore lint but added comments of why i didnt change it
andieswift Feb 4, 2025
61402c9
removed filter local storage; replaced wiht query local storage & col…
andieswift Feb 6, 2025
f85485e
switching the order of clearing cache
andieswift Feb 6, 2025
4a5b11d
added some typing/renamed
andieswift Feb 6, 2025
f703158
removing vitest file & fixing a new bug i made lol
andieswift Feb 6, 2025
f6ef122
actually you cannot clear all cache right before logining out, so I u…
andieswift Feb 6, 2025
24ffb73
Merge branch 'main' into filter-state
andieswift Feb 6, 2025
4fe66ac
added tiffs local storage class to affected files
andieswift Feb 6, 2025
d92663c
added some tests based on code added
andieswift Feb 6, 2025
29238a6
Merge branch 'main' into filter-state
andieswift Feb 6, 2025
8b4d237
Merge branch 'main' into filter-state
andieswift Feb 6, 2025
a13f4b2
reset column storage on tab change
andieswift Feb 7, 2025
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
2 changes: 2 additions & 0 deletions react-app/src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ScrollToTop, SimplePageContainer, UserPrompt, Banner } from "@/componen
import { isFaqPage, isProd } from "@/utils";
import MMDLAlertBanner from "@/components/Banner/MMDLSpaBanner";
import { UserRoles } from "shared-types";
import { removeItemLocalStorage } from "@/hooks/useLocalStorage";
/**
* Custom hook that generates a list of navigation links based on the user's status and whether the current page is the FAQ page.
*
Expand Down Expand Up @@ -81,6 +82,7 @@ const UserDropdownMenu = () => {
};

const handleLogout = async () => {
removeItemLocalStorage();
await Auth.signOut();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,13 @@ export const FilterChips: FC = () => {
});
};

const handleChipClick = () =>
const handleChipClick = () => {
url.onSet((s) => ({
...s,
filters: [],
pagination: { ...s.pagination, number: 0 },
}));
};

return (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,19 @@ export function FilterableDateRange({ value, onChange, ...props }: Props) {
side="left"
sideOffset={1}
>
<Calendar
disabled={disableDates}
initialFocus
mode="range"
defaultMonth={selectedDate?.from}
selected={selectedDate}
numberOfMonths={2}
className="bg-white"
onSelect={onSelect}
{...props}
/>
<div className="hidden lg:block">
<Calendar
disabled={disableDates}
initialFocus
mode="range"
defaultMonth={selectedDate?.from}
selected={selectedDate}
numberOfMonths={2}
className="bg-white"
onSelect={onSelect}
{...props}
/>
</div>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is old code that was removed I added back for this ticket: https://jiraent.cms.gov/browse/OY2-32770

<div className="lg:hidden flex align-center">
<Calendar
disabled={disableDates}
Expand Down
21 changes: 14 additions & 7 deletions react-app/src/components/Opensearch/main/Filtering/Drawer/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
return (value: opensearch.FilterValue) => {
setFilters((state) => {
const updateState = { ...state, [field]: { ...state[field], value } };
// find all filter values to update
const updateFilters = Object.values(updateState).filter((FIL) => {
if (FIL.type === "terms") {
const value = FIL.value as string[];
Expand All @@ -92,6 +93,7 @@
return true;
});

// this changes the tanstack query; which is used to query the data
url.onSet((state) => ({
...state,
filters: updateFilters,
Expand All @@ -107,24 +109,26 @@
setAccordionValues(updateAccordion);
};

const onFilterReset = () =>
const onFilterReset = () => {
url.onSet((s) => ({
...s,
filters: [],
pagination: { ...s.pagination, number: 0 },
}));
};

const filtersApplied = checkMultiFilter(url.state.filters, 1);

// update initial filter state + accordion default open items
// update filter display based on url query
useEffect(() => {
if (!drawer.drawerOpen) return;
const updateAccordions = [...accordionValues] as any[];

setFilters((s) => {
return Object.entries(s).reduce((STATE, [KEY, VAL]) => {
setFilters((currentFilters) => {
// Set the new filters state based on the current filter data
return Object.entries(currentFilters).reduce((STATE, [KEY, VAL]) => {
const updateFilter = url.state.filters.find((FIL) => FIL.field === KEY);

// Determine the new value for the filter based on the URL state
const value = (() => {
if (updateFilter) {
updateAccordions.push(KEY);
Expand All @@ -135,12 +139,15 @@
return { gte: undefined, lte: undefined } as opensearch.RangeValue;
})();

// Update the state with the new value for this filter
STATE[KEY] = { ...VAL, value };

return STATE;
}, {} as any);
});
setAccordionValues(updateAccordions);
}, [url.state.filters, drawer.drawerOpen]);
// accordionValues is intensionally left out of this dendency array because it could cause looping
}, [url.state.filters, drawer.drawerOpen, setFilters]);

Check warning on line 150 in react-app/src/components/Opensearch/main/Filtering/Drawer/hooks.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'accordionValues'. Either include it or remove the dependency array

const aggs = useMemo(() => {
return Object.entries(_aggs || {}).reduce(
Expand All @@ -157,7 +164,7 @@
},
{} as Record<opensearch.main.Field, { label: string; value: string }[]>,
);
}, [_aggs]);
}, [_aggs, labelMap]);

return {
aggs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ describe("OsFilterDrawer", () => {

const chip = screen.queryByLabelText("CHIP SPA");
expect(chip).toBeInTheDocument();

expect(chip.getAttribute("data-state")).toEqual("unchecked");

const med = screen.queryByLabelText("Medicaid SPA");
Expand Down
52 changes: 29 additions & 23 deletions react-app/src/components/Opensearch/main/Filtering/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ import * as F from "./Filterable";
import { useFilterDrawer } from "./hooks";

export const OsFilterDrawer = () => {
const hook = useFilterDrawer();
// how filterDrawerHook looks
// filterDrawerHook: {accordionValues: {}, aggs: filter opts, drawer: drawer state, fitlers: name of filters & status's, filtersApplied: boolean,
// onFilterChange, onFilterReset}
const filterDrawerHook = useFilterDrawer();

return (
<Sheet open={hook.drawer.drawerOpen} onOpenChange={hook.drawer.setDrawerState}>
<Sheet
open={filterDrawerHook.drawer.drawerOpen}
onOpenChange={filterDrawerHook.drawer.setDrawerState}
>
<SheetTrigger asChild>
<Button
variant="outline"
Expand All @@ -37,44 +43,44 @@ export const OsFilterDrawer = () => {
<Button
className="w-full my-2"
variant="outline"
disabled={!hook.filtersApplied}
onClick={hook.onFilterReset}
disabled={!filterDrawerHook.filtersApplied}
onClick={filterDrawerHook.onFilterReset}
>
Reset
</Button>
<Accordion
value={hook.accordionValues}
onValueChange={hook.onAccordionChange}
value={filterDrawerHook.accordionValues}
onValueChange={filterDrawerHook.onAccordionChange}
type="multiple"
>
{Object.values(hook.filters).map((PK) => (
<AccordionItem key={`filter-${PK.field}`} value={PK.field}>
<AccordionTrigger className="underline">{PK.label}</AccordionTrigger>
{Object.values(filterDrawerHook.filters).map((filter) => (
<AccordionItem key={`filter-${filter.field}`} value={filter.field}>
<AccordionTrigger className="underline">{filter.label}</AccordionTrigger>
<AccordionContent className="px-0">
{PK.component === "multiSelect" && (
{filter.component === "multiSelect" && (
<F.FilterableSelect
value={hook.filters[PK.field]?.value as string[]}
onChange={hook.onFilterChange(PK.field)}
options={hook.aggs?.[PK.field]}
value={filterDrawerHook.filters[filter.field]?.value as string[]}
onChange={filterDrawerHook.onFilterChange(filter.field)}
options={filterDrawerHook.aggs?.[filter.field]}
/>
)}
{PK.component === "multiCheck" && (
{filter.component === "multiCheck" && (
<F.FilterableMultiCheck
value={hook.filters[PK.field]?.value as string[]}
onChange={hook.onFilterChange(PK.field)}
options={hook.aggs?.[PK.field]}
value={filterDrawerHook.filters[filter.field]?.value as string[]}
onChange={filterDrawerHook.onFilterChange(filter.field)}
options={filterDrawerHook.aggs?.[filter.field]}
/>
)}
{PK.component === "dateRange" && (
{filter.component === "dateRange" && (
<F.FilterableDateRange
value={hook.filters[PK.field]?.value as opensearch.RangeValue}
onChange={hook.onFilterChange(PK.field)}
value={filterDrawerHook.filters[filter.field]?.value as opensearch.RangeValue}
onChange={filterDrawerHook.onFilterChange(filter.field)}
/>
)}
{PK.component === "boolean" && (
{filter.component === "boolean" && (
<F.FilterableBoolean
value={hook.filters[PK.field]?.value as boolean}
onChange={hook.onFilterChange(PK.field)}
value={filterDrawerHook.filters[filter.field]?.value as boolean}
onChange={filterDrawerHook.onFilterChange(filter.field)}
/>
)}
</AccordionContent>
Expand Down
46 changes: 45 additions & 1 deletion react-app/src/components/Opensearch/main/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, beforeEach } from "vitest";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { opensearch } from "shared-types";
Expand All @@ -14,6 +14,7 @@ import {
verifyChips,
verifyPagination,
EMPTY_HITS,
Storage,
} from "@/utils/test-helpers";
import { OsMainView, OsTableColumn } from "@/components";

Expand All @@ -27,6 +28,8 @@ const verifyTable = (recordCount: number) => {
};

describe("OsMainView", () => {
global.localStorage = new Storage();

const setup = (
columns: OsTableColumn[],
hits: opensearch.Hits<opensearch.main.Document>,
Expand All @@ -48,6 +51,9 @@ describe("OsMainView", () => {
};
};

beforeEach(() => {
global.localStorage.clear();
});
describe("SPAs", () => {
it("should display without filters", async () => {
const spaHits = getFilteredHits(["CHIP SPA", "Medicaid SPA"]);
Expand Down Expand Up @@ -286,4 +292,42 @@ describe("OsMainView", () => {
verifyPagination(recordCount);
});
});

describe("Local Storage to display Columns", () => {
it("should store hidden column in local storage", async () => {
const spaHits = getFilteredHits(["CHIP SPA", "Medicaid SPA"]);
setup(
[...DEFAULT_COLUMNS, HIDDEN_COLUMN],
spaHits,
getDashboardQueryString({
filters: DEFAULT_FILTERS,
tab: "spas",
}),
);
expect(global.localStorage.getItem("osColumns")).toBe(JSON.stringify(["origin.keyword"]));
});

it("should load hidden columns based on local storage", async () => {
const spaHits = getFilteredHits(["CHIP SPA", "Medicaid SPA"]);
expect(global.localStorage.setItem("osColumns", JSON.stringify(["authority.keyword"])));
const { user } = setup(
[...DEFAULT_COLUMNS, HIDDEN_COLUMN],
spaHits,
getDashboardQueryString({
filters: DEFAULT_FILTERS,
tab: "spas",
}),
);

expect(screen.queryByRole("dialog")).toBeNull();
await user.click(screen.queryByRole("button", { name: "Columns (1 hidden)" }));
const columns = screen.queryByRole("dialog");
expect(within(columns).getByText("Submission Source")).toBeInTheDocument();
expect(within(columns).getByText("Submission Source").parentElement).toHaveClass(
"text-gray-800",
);
expect(within(columns).getByText("Authority")).toBeInTheDocument();
expect(within(columns).getByText("Authority").parentElement).toHaveClass("text-gray-400");
});
});
});
21 changes: 20 additions & 1 deletion react-app/src/components/Opensearch/main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,41 @@ import { useOsContext } from "./Provider";
import { useOsUrl } from "./useOpensearch";
import { OsTableColumn } from "./types";
import { FilterChips } from "./Filtering";
import { useLocalStorage } from "@/hooks/useLocalStorage";

const createLSColumns = (columns: OsTableColumn[]): string[] => {
const columnsVisalbe = columns.filter((col) => col.hidden);
const columnFields = columnsVisalbe.reduce((acc, curr) => {
if (curr.field) acc.push(curr.field);
return acc;
}, []);
return columnFields;
};

export const OsMainView: FC<{
columns: OsTableColumn[];
}> = (props) => {
const context = useOsContext();
const url = useOsUrl();

const [localStorageCol, setLocalStorageCol] = useLocalStorage(
"osColumns",
createLSColumns(props.columns),
);

const [osColumns, setOsColumns] = useState(
props.columns.map((COL) => ({
...COL,
hidden: !!COL?.hidden,
hidden: localStorageCol.includes(COL.field),
locked: COL?.locked ?? false,
})),
);

const onToggle = (field: string) => {
if (localStorageCol.includes(field))
setLocalStorageCol(() => localStorageCol.filter((x) => x != field));
else setLocalStorageCol([...localStorageCol, field]);

setOsColumns((state) => {
return state?.map((S) => {
if (S.field !== field) return S;
Expand Down
2 changes: 2 additions & 0 deletions react-app/src/components/TimeoutModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Auth } from "aws-amplify";
import { intervalToDuration } from "date-fns";
import pluralize from "pluralize";
import { useEffect, useState } from "react";
import { removeItemLocalStorage } from "@/hooks/useLocalStorage";

const TWENTY_MINS_IN_MILS = 1000 * 60 * 20;
const TEN_MINS_IN_MILS = 60 * 10;
Expand All @@ -30,6 +31,7 @@ export const TimeoutModal = () => {
const onLogOut = () => {
setIsModalOpen(false);
Auth.signOut();
removeItemLocalStorage();
};

const onExtendSession = () => {
Expand Down
2 changes: 2 additions & 0 deletions react-app/src/features/dashboard/Lists/spas/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
APPROVED_ITEM_EXPORT,
BLANK_ITEM,
BLANK_ITEM_EXPORT,
Storage,
} from "@/utils/test-helpers";
import {
TEST_STATE_SUBMITTER_USER,
Expand Down Expand Up @@ -227,6 +228,7 @@ const verifyRow = (

describe("SpasList", () => {
const setup = async (hits: opensearch.Hits<opensearch.main.Document>, queryString: string) => {
global.localStorage = new Storage();
const user = userEvent.setup();
const rendered = renderDashboard(
<SpasList />,
Expand Down
2 changes: 2 additions & 0 deletions react-app/src/features/dashboard/Lists/waivers/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
APPROVED_ITEM_EXPORT,
BLANK_ITEM,
BLANK_ITEM_EXPORT,
Storage,
} from "@/utils/test-helpers";
import {
TEST_STATE_SUBMITTER_USER,
Expand Down Expand Up @@ -249,6 +250,7 @@ const verifyRow = (

describe("WaiversList", () => {
const setup = async (hits: opensearch.Hits<opensearch.main.Document>, queryString: string) => {
global.localStorage = new Storage();
const user = userEvent.setup();
const rendered = renderDashboard(
<WaiversList />,
Expand Down
Loading
Loading