Skip to content

Commit

Permalink
Merge pull request #408 from CBIIT/CRDCDH-1123
Browse files Browse the repository at this point in the history
CRDCDH-1123 CRDCDH-1124 Navigate back to Manage Users/Org page with search params
  • Loading branch information
amattu2 authored Jun 25, 2024
2 parents 69c2f75 + 1c34252 commit 4763134
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 29 deletions.
119 changes: 110 additions & 9 deletions src/components/Contexts/SearchParamsContext.test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { Link, MemoryRouter, Route, Routes } from "react-router-dom";
import { SearchParamsProvider, useSearchParamsContext } from "./SearchParamsContext";

const TestChild = () => {
const [searchParams, setSearchParams] = useSearchParamsContext();
const { searchParams, setSearchParams, lastSearchParams } = useSearchParamsContext();

return (
<>
<div data-testid="current-query">{searchParams.toString()}</div>
<div data-testid="current-query">{searchParams?.toString()}</div>
<div data-testid="last-search-params">{JSON.stringify(lastSearchParams)}</div>
<button
type="button"
onClick={() => setSearchParams(new URLSearchParams({ page: "2" }), { replace: true })}
>
Set Page
</button>
<button
type="button"
onClick={() =>
setSearchParams((prev) => {
prev.set("display", "2");
return prev;
})
}
>
Add Display
</button>
<button type="button" onClick={() => setSearchParams(new URLSearchParams({ other: "foo" }))}>
Set Other
</button>
Expand All @@ -24,28 +36,32 @@ const TestChild = () => {
>
Clear Params
</button>
<Link to="/test?link=true">Go to test page</Link>
<Link to="/test">Go to test page without search params</Link>
<Link to="/another?link=true">Go to another page</Link>
</>
);
};

const TestParent = ({ children }) => (
<MemoryRouter initialEntries={["/test?initial=true"]}>
const TestParent = ({ initialEntries = ["/test?initial=true"] }) => (
<MemoryRouter initialEntries={initialEntries}>
<SearchParamsProvider>
<Routes>
<Route path="/test" element={children} />
<Route path="/test" element={<TestChild />} />
<Route path="/another" element={<TestChild />} />
</Routes>
</SearchParamsProvider>
</MemoryRouter>
);

describe("SearchParamsContext", () => {
it("initializes with provided search parameters", () => {
render(<TestChild />, { wrapper: TestParent });
render(<TestChild />, { wrapper: () => <TestParent /> });
expect(screen.getByTestId("current-query").textContent).toBe("initial=true");
});

it("updates search parameters correctly", async () => {
render(<TestChild />, { wrapper: TestParent });
render(<TestChild />, { wrapper: () => <TestParent /> });

userEvent.click(screen.getByText("Set Page"));

Expand All @@ -61,7 +77,7 @@ describe("SearchParamsContext", () => {
});

it("clears search parameters correctly", async () => {
render(<TestChild />, { wrapper: TestParent });
render(<TestChild />, { wrapper: () => <TestParent /> });

userEvent.click(screen.getByText("Clear Params"));

Expand All @@ -70,6 +86,91 @@ describe("SearchParamsContext", () => {
});
});

it("initializes and retains initial search parameters", () => {
render(<TestChild />, { wrapper: () => <TestParent /> });
expect(screen.getByTestId("current-query").textContent).toBe("initial=true");
expect(screen.getByTestId("last-search-params").textContent).toBe('{"/test":"?initial=true"}');
});

it("updates lastSearchParams when navigating with new parameters", async () => {
render(<TestChild />, {
wrapper: () => <TestParent initialEntries={["/test?test=true", "/another?updated=true"]} />,
});

userEvent.click(screen.getByText("Add Display"));

await waitFor(() => {
expect(screen.getByTestId("last-search-params").textContent).toContain(
'{"/another":"?updated=true&display=2"}'
);
});

userEvent.click(screen.getByText("Go to test page"));

await waitFor(() => {
expect(screen.getByTestId("last-search-params").textContent).toContain(
'{"/another":"?updated=true&display=2","/test":"?link=true"}'
);
});
});

it("should not update lastSearchParams when value is the same", async () => {
render(<TestChild />, {
wrapper: () => <TestParent initialEntries={["/another?page=2"]} />,
});

userEvent.click(screen.getByText("Set Page"));

await waitFor(() => {
expect(screen.getByTestId("current-query").textContent).toBe("page=2");
expect(screen.getByTestId("last-search-params").textContent).toContain(
'{"/another":"?page=2"}'
);
});
});

it("should not update lastSearchParams with empty string before it has previously been assigned", async () => {
render(<TestChild />, {
wrapper: () => <TestParent initialEntries={["/another?initial=true"]} />,
});

userEvent.click(screen.getByText("Go to test page without search params"));

await waitFor(() => {
expect(screen.getByTestId("current-query").textContent).toBe("");
expect(screen.getByTestId("last-search-params").textContent).toContain(
'{"/another":"?initial=true"}'
);
});
});

it("updates lastSearchParams with empty string after it has previously been assigned", async () => {
render(<TestChild />, {
wrapper: () => <TestParent initialEntries={["/another?initial=true"]} />,
});

userEvent.click(screen.getByText("Set Page"));
userEvent.click(screen.getByText("Go to test page"));
userEvent.click(screen.getByText("Set Other"));

await waitFor(() => {
expect(screen.getByTestId("current-query").textContent).toBe("other=foo");
expect(screen.getByTestId("last-search-params").textContent).toContain(
'{"/another":"?page=2","/test":"?other=foo"}'
);
});

userEvent.click(screen.getByText("Go to another page"));
userEvent.click(screen.getByText("Go to test page without search params"));

await waitFor(() => {
expect(screen.getByTestId("current-query").textContent).toBe("");
expect(screen.getByTestId("last-search-params").textContent).toContain(
'{"/another":"?link=true","/test":""}'
);
});
});

it("throws an error when used outside of provider", () => {
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
expect(() => render(<TestChild />)).toThrow(
Expand Down
31 changes: 25 additions & 6 deletions src/components/Contexts/SearchParamsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { createContext, useContext, useMemo } from "react";
import { SetURLSearchParams, useSearchParams } from "react-router-dom";
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import { SetURLSearchParams, useLocation, useSearchParams } from "react-router-dom";

export type LastSearchParams = { [key: string]: string } | null;

type ContextState = {
lastSearchParams: LastSearchParams;
searchParams: URLSearchParams;
setSearchParams: SetURLSearchParams;
};
Expand Down Expand Up @@ -31,17 +34,33 @@ type ProviderProps = {
*/
export const SearchParamsProvider: React.FC<ProviderProps> = ({ children }) => {
const [searchParams, setSearchParamsBase] = useSearchParams();
const location = useLocation();

const [lastSearchParams, setLastSearchParams] = useState<LastSearchParams>(null);

useEffect(() => {
if (location?.search === lastSearchParams?.[location?.pathname]) {
return;
}
// if no previous search params for pathname, don't store empty search params
if (!lastSearchParams?.[location.pathname] && !location?.search) {
return;
}

setLastSearchParams((prev) => ({ ...prev, [location.pathname]: location.search }));
}, [location]);

const setSearchParams: SetURLSearchParams = (newSearchParams, options = {}) => {
setSearchParamsBase(newSearchParams, { replace: true, ...options });
};

const value = useMemo(
() => ({
lastSearchParams,
searchParams,
setSearchParams,
}),
[searchParams, setSearchParams]
[lastSearchParams, searchParams, setSearchParams]
);

return <Context.Provider value={value}>{children}</Context.Provider>;
Expand All @@ -55,9 +74,9 @@ export const SearchParamsProvider: React.FC<ProviderProps> = ({ children }) => {
*
* @see SearchParamsProvider – Must be wrapped in a SearchParamsProvider component
* @see ContextState – Search Params context state returned by the hook
* @returns {[URLSearchParams, SetURLSearchParams]} - Search Params context and setter
* @returns {ContextState} - Search Params context and setter
*/
export const useSearchParamsContext = (): [URLSearchParams, SetURLSearchParams] => {
export const useSearchParamsContext = (): ContextState => {
const context = useContext<ContextState>(Context);

if (!context) {
Expand All @@ -66,5 +85,5 @@ export const useSearchParamsContext = (): [URLSearchParams, SetURLSearchParams]
);
}

return [context.searchParams, context.setSearchParams];
return context;
};
2 changes: 1 addition & 1 deletion src/components/GenericTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ const GenericTable = <T,>(
ref: React.Ref<TableMethods>
) => {
const showDelayedLoading = useDelayedLoading(loading, 200);
const [searchParams, setSearchParams] = useSearchParamsContext();
const { searchParams, setSearchParams } = useSearchParamsContext();
const defaultColumn: Column<T> =
columns.find((c) => c.default) || columns.find((c) => c.fieldKey ?? c.field);
const initialTableParams: TableParams = {
Expand Down
2 changes: 1 addition & 1 deletion src/content/organizations/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ const ListingView: FC = () => {

const { state } = useLocation();
const { data, status: orgStatus } = useOrganizationListContext();
const [searchParams, setSearchParams] = useSearchParamsContext();
const { searchParams, setSearchParams } = useSearchParamsContext();
const { watch, register, control, setValue } = useForm<FilterForm>({
defaultValues: {
organization: "",
Expand Down
8 changes: 6 additions & 2 deletions src/content/organizations/OrganizationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import ConfirmDialog from "../../components/Organizations/ConfirmDialog";
import usePageTitle from "../../hooks/usePageTitle";
import { formatFullStudyName, mapOrganizationStudyToId } from "../../utils";
import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";

type Props = {
/**
Expand Down Expand Up @@ -173,12 +174,14 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {

const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
const { lastSearchParams } = useSearchParamsContext();

const [organization, setOrganization] = useState<Organization | null>(null);
const [dataSubmissions, setDataSubmissions] = useState<Partial<Submission>[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState<boolean>(false);
const [confirmOpen, setConfirmOpen] = useState<boolean>(false);
const manageOrgPageUrl = `/organizations${lastSearchParams?.["/organizations"] ?? ""}`;

const assignedStudies: string[] = useMemo(() => {
const activeStudies = {};
Expand Down Expand Up @@ -295,6 +298,7 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {
}

setError(null);
navigate(manageOrgPageUrl);
};

const handleBypassWarning = () => {
Expand Down Expand Up @@ -348,7 +352,7 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {
variables: { orgID: _id, organization: _id },
});
if (error || !data?.getOrganization) {
navigate("/organizations", {
navigate(manageOrgPageUrl, {
state: { error: "Unable to fetch organization" },
});
return;
Expand Down Expand Up @@ -507,7 +511,7 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {
</StyledButton>
<StyledButton
type="button"
onClick={() => navigate("/organizations")}
onClick={() => navigate(manageOrgPageUrl)}
txt="#666666"
border="#828282"
>
Expand Down
2 changes: 1 addition & 1 deletion src/content/users/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ const ListingView: FC = () => {
const { user, status: authStatus } = useAuthContext();
const { state } = useLocation();
const { data: orgData, activeOrganizations, status: orgStatus } = useOrganizationListContext();
const [searchParams, setSearchParams] = useSearchParamsContext();
const { searchParams, setSearchParams } = useSearchParamsContext();
const [dataset, setDataset] = useState<T[]>([]);
const [count, setCount] = useState<number>(0);
const [touchedFilters, setTouchedFilters] = useState<TouchedState>(initialTouchedFields);
Expand Down
Loading

0 comments on commit 4763134

Please sign in to comment.