diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9d3bf33195..4b0e3e945e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5583,17 +5583,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "18.2.19", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", - "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-reconciler": { "version": "0.28.9", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", @@ -27163,17 +27152,6 @@ "csstype": "^3.0.2" } }, - "@types/react-dom": { - "version": "18.2.19", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", - "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@types/react": "*" - } - }, "@types/react-reconciler": { "version": "0.28.9", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", diff --git a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js index 515be95466..3060d6aaea 100644 --- a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js +++ b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js @@ -2,9 +2,33 @@ import EncounterFormStore from "../../../pages/SearchPages/stores/EncounterFormS describe("EncounterFormStore", () => { let store; + let mockStorage = {}; + + beforeAll(() => { + jest + .spyOn(Storage.prototype, "getItem") + .mockImplementation((key) => mockStorage[key] || null); + jest + .spyOn(Storage.prototype, "setItem") + .mockImplementation((key, value) => { + mockStorage[key] = value ? value.toString() : ""; + }); + jest.spyOn(Storage.prototype, "removeItem").mockImplementation((key) => { + delete mockStorage[key]; + }); + jest.spyOn(Storage.prototype, "clear").mockImplementation(() => { + mockStorage = {}; + }); + }); beforeEach(() => { store = new EncounterFormStore(); + mockStorage = {}; + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.restoreAllMocks(); }); test("initializes with empty formFilters", () => { @@ -72,5 +96,92 @@ describe("EncounterFormStore", () => { store.resetFilters(); expect(store.formFilters).toEqual([]); + expect(store.appliedFilters).toEqual([]); + expect(window.sessionStorage.removeItem).toHaveBeenCalledWith("formData"); + }); + + test("applyFilters deep copies formFilters to appliedFilters and saves to storage", () => { + const mockFilter = { + filterId: "f1", + clause: "AND", + query: "q1", + filterKey: "k1", + path: "", + }; + store.addFilter( + mockFilter.filterId, + mockFilter.clause, + mockFilter.query, + mockFilter.filterKey, + ); + + store.applyFilters(); + + expect(store.appliedFilters).toHaveLength(1); + expect(store.appliedFilters[0]).toEqual(mockFilter); + + expect(store.appliedFilters).not.toBe(store.formFilters); + + expect(window.sessionStorage.setItem).toHaveBeenCalledWith( + store.FILTER_STORAGE_KEY, + JSON.stringify([mockFilter]), + ); + }); + + test("getFiltersFromStorage loads valid JSON into formFilters and appliedFilters", () => { + const mockFilter = { + filterId: "f1", + clause: "AND", + query: "q1", + filterKey: "k1", + path: "", + }; + const savedData = JSON.stringify([mockFilter]); + window.sessionStorage.setItem("formData", savedData); + + store.getFiltersFromStorage(); + + expect(window.sessionStorage.getItem).toHaveBeenCalledTimes(1); + + expect(store.formFilters).toHaveLength(1); + expect(store.appliedFilters).toHaveLength(1); + }); + + test("getFiltersFromStorage ignores strings/non-arrays and clears storage", () => { + window.sessionStorage.setItem("formData", JSON.stringify("abc")); + store.getFiltersFromStorage(); + + expect(store.formFilters).toEqual([]); + expect(window.sessionStorage.removeItem).toHaveBeenCalledWith("formData"); + }); + + test("setFiltersInSessionStorage clears the storage data when called with no data", () => { + const mockFilter = { + filterId: "f1", + clause: "AND", + query: "q1", + filterKey: "k1", + path: "", + }; + window.sessionStorage.setItem("formData", JSON.stringify(mockFilter)); + store.setFiltersInSessionStorage(); + + expect(store.formFilters).toEqual([]); + expect(window.sessionStorage.removeItem).toHaveBeenCalledWith("formData"); + }); + + test("getFiltersFromStorage handles malformed JSON gracefully and clears storage", () => { + const consoleSpy = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + window.sessionStorage.setItem("formData", "{ bad_json ]"); + + store.getFiltersFromStorage(); + + expect(store.formFilters).toEqual([]); + expect(consoleSpy).toHaveBeenCalled(); + expect(window.sessionStorage.removeItem).toHaveBeenCalledWith("formData"); + + consoleSpy.mockRestore(); }); }); diff --git a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js index de7ebb9244..e1446f7f98 100644 --- a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js +++ b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js @@ -8,7 +8,7 @@ import { } from "@testing-library/react"; import EncounterSearch from "../../../pages/SearchPages/EncounterSearch"; import { MemoryRouter } from "react-router-dom"; -import * as useFilterEncountersHook from "../../../models/encounters/useFilterEncounters"; +import useFilterEncounters, * as useFilterEncountersHook from "../../../models/encounters/useFilterEncounters"; import * as useFilterEncountersWithMediaAssetsHook from "../../../models/encounters/useFilterEncountersWithMediaAssets"; import * as useEncounterSearchSchemasHook from "../../../models/encounters/useEncounterSearchSchemas"; import * as getAllSearchParams from "../../../pages/SearchPages/getAllSearchParamsAndParse"; @@ -18,6 +18,7 @@ import axios from "axios"; jest.mock("../../../pages/SearchPages/stores/EncounterFormStore", () => ({ globalEncounterFormStore: { formFilters: [], + appliedFilters: [], mediaAssetsSearchQuery: [], pageSize: 20, start: 0, @@ -32,6 +33,7 @@ jest.mock("../../../pages/SearchPages/stores/EncounterFormStore", () => ({ setGalleryLoading: jest.fn(), setLoadingAll: jest.fn(), setGalleryExhausted: jest.fn(), + getFiltersFromStorage: jest.fn(), }, })); @@ -120,6 +122,7 @@ describe("EncounterSearch", () => { jest.clearAllMocks(); globalEncounterFormStore.formFilters = []; + globalEncounterFormStore.appliedFilters = []; globalEncounterFormStore.mediaAssetsSearchQuery = []; globalEncounterFormStore.pageSize = 20; globalEncounterFormStore.start = 0; @@ -677,4 +680,42 @@ describe("EncounterSearch", () => { expect(globalEncounterFormStore.setAssetOffset).toHaveBeenCalled(); }); }); + + it("calls getFiltersFromStorage on mount if no queryID is present", () => { + const params = { + from: 0, + size: 20, + sort: "date", + sortOrder: "desc", + }; + const mockFilter = [ + { + filterId: "f1", + clause: "AND", + query: "q1", + filterKey: "k1", + path: "", + }, + ]; + + globalEncounterFormStore.appliedFilters = mockFilter; + + renderWithProviders("/search"); + + expect( + globalEncounterFormStore.getFiltersFromStorage, + ).toHaveBeenCalledTimes(1); + expect(useFilterEncounters).toHaveBeenCalledWith({ + queries: mockFilter, + params: params, + }); + }); + + it("does NOT call getFiltersFromStorage if a queryID is present in the URL", () => { + renderWithProviders("/search?searchQueryId=abc123"); + + expect( + globalEncounterFormStore.getFiltersFromStorage, + ).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/components/FilterPanel.jsx b/frontend/src/components/FilterPanel.jsx index 5da661a54f..2920021074 100644 --- a/frontend/src/components/FilterPanel.jsx +++ b/frontend/src/components/FilterPanel.jsx @@ -15,7 +15,6 @@ export default function FilterPanel({ style = {}, handleSearch = () => {}, refetch = () => {}, - setTempFormFilters = () => {}, store, }) { const { data } = useSiteSettings(); @@ -143,16 +142,12 @@ export default function FilterPanel({ backgroundColor={theme.primaryColors.primary700} borderColor={theme.primaryColors.primary700} onClick={() => { - setTempFormFilters([...store.formFilters]); refetch().then(({ data }) => { console.log("Refetched data:", data); }); store.resetGallery(); setSearchParams(new URLSearchParams()); - sessionStorage.setItem( - "formData", - JSON.stringify(store.formFilters), - ); + store.applyFilters(); setFilterPanel(false); handleSearch(); store.setActiveStep(0); diff --git a/frontend/src/components/filterFields/SideBar.jsx b/frontend/src/components/filterFields/SideBar.jsx index d09c157edb..1b13a29ddb 100644 --- a/frontend/src/components/filterFields/SideBar.jsx +++ b/frontend/src/components/filterFields/SideBar.jsx @@ -9,7 +9,7 @@ import { useSearchParams } from "react-router-dom"; import { observer } from "mobx-react-lite"; const Sidebar = observer( - ({ setFilterPanel, searchQueryId, queryID, tempFormFilters = [], store }) => { + ({ setFilterPanel, searchQueryId, queryID = false, store }) => { const theme = React.useContext(ThemeContext); const [show, setShow] = useState(false); const sidebarWidth = 400; @@ -18,14 +18,17 @@ const Sidebar = observer( const handleShow = () => setShow(true); const [, setSearchParams] = useSearchParams(); - const num = () => (queryID ? 1 : tempFormFilters.length); + const num = () => (queryID ? 1 : store.appliedFilters.length); const handleCopy = () => { + // When viewing a result loaded from a shared query ID, copy that ID; + // otherwise copy the ID freshly generated for the current filter search. + const idToCopy = queryID || searchQueryId; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard - .writeText(searchQueryId) + .writeText(idToCopy) .then(() => { - alert(`Query ID: ${searchQueryId} copied to clipboard!`); + alert(`Query ID: ${idToCopy} copied to clipboard!`); }) .catch((err) => { console.error("Failed to copy text: ", err); @@ -111,7 +114,7 @@ const Sidebar = observer( ) : (