From f6a82be6527dbc3dc0236b48fc03eb071fd51555 Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Sun, 24 May 2026 09:22:49 -0500 Subject: [PATCH 01/10] initial change to manage state over reloads --- frontend/src/components/FilterPanel.jsx | 3 +-- .../src/components/filterFields/SideBar.jsx | 6 ++--- .../src/pages/SearchPages/EncounterSearch.jsx | 22 +++++++++++++++---- .../SearchPages/getAllSearchParamsAndParse.js | 8 +------ .../SearchPages/stores/EncounterFormStore.js | 5 +++++ 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/FilterPanel.jsx b/frontend/src/components/FilterPanel.jsx index 5da661a54f..59aec3ac05 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,7 +142,7 @@ export default function FilterPanel({ backgroundColor={theme.primaryColors.primary700} borderColor={theme.primaryColors.primary700} onClick={() => { - setTempFormFilters([...store.formFilters]); + // setTempFormFilters([...store.formFilters]); refetch().then(({ data }) => { console.log("Refetched data:", data); }); diff --git a/frontend/src/components/filterFields/SideBar.jsx b/frontend/src/components/filterFields/SideBar.jsx index d09c157edb..919fc239b8 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 = [], store }) => { const theme = React.useContext(ThemeContext); const [show, setShow] = useState(false); const sidebarWidth = 400; @@ -18,7 +18,7 @@ const Sidebar = observer( const handleShow = () => setShow(true); const [, setSearchParams] = useSearchParams(); - const num = () => (queryID ? 1 : tempFormFilters.length); + const num = () => (queryID ? 1 : store.formFilters.length); const handleCopy = () => { if (navigator.clipboard && navigator.clipboard.writeText) { @@ -111,7 +111,7 @@ const Sidebar = observer( ) : (
- {tempFormFilters.map((filter, index) => ( + {store.formFilters.map((filter, index) => ( {filter} ))}
diff --git a/frontend/src/pages/SearchPages/EncounterSearch.jsx b/frontend/src/pages/SearchPages/EncounterSearch.jsx index 5f0099e049..1c1aeb544a 100644 --- a/frontend/src/pages/SearchPages/EncounterSearch.jsx +++ b/frontend/src/pages/SearchPages/EncounterSearch.jsx @@ -42,7 +42,6 @@ const EncounterSearch = observer(() => { const [encounterSortOrder, setEncounterSortOrder] = useState("desc"); const [searchIdSortName, setSearchIdSortName] = useState("date"); const [searchIdSortOrder, setSearchIdSortOrder] = useState("desc"); - const [tempFormFilters, setTempFormFilters] = useState([]); const [exportModalOpen, setExportModalOpen] = useState(false); useEffect(() => { @@ -50,11 +49,27 @@ const EncounterSearch = observer(() => { searchParams, store, setFilterPanel, - setTempFormFilters, + // setTempFormFilters, encounterData, ); }, [searchParams]); + useEffect(() => { + if (!queryID) { + const savedFiltersJson = sessionStorage.getItem("formData"); + if (savedFiltersJson) { + try { + const parsedFilters = JSON.parse(savedFiltersJson); + if (parsedFilters && parsedFilters.length > 0) { + store.setFormFilters(parsedFilters); + } + } catch (e) { + console.error("Failed to parse formData from session storage", e); + } + } + } + }, [queryID, store]); + useEffect(() => { if (!queryID) { setEncounterSortName(sortname); @@ -309,7 +324,7 @@ const EncounterSearch = observer(() => { handleSearch={handleSearch} setQueryID={setQueryID} refetch={refetch} - setTempFormFilters={setTempFormFilters} + // setTempFormFilters={setTempFormFilters} store={store} /> { searchQueryId={searchQueryId} queryID={false} store={store} - tempFormFilters={tempFormFilters} /> {}, -) => { +const helperFunction = async (searchParams, store, setFilterPanel) => { const params = Object.fromEntries(searchParams.entries()) || {}; if (Object.keys(params).length === 0) { return; @@ -50,7 +45,6 @@ const helperFunction = async ( ); } } - setTempFormFilters([...store.formFilters]); setFilterPanel(false); return; }; diff --git a/frontend/src/pages/SearchPages/stores/EncounterFormStore.js b/frontend/src/pages/SearchPages/stores/EncounterFormStore.js index e5b08ed995..dda9aeeeac 100644 --- a/frontend/src/pages/SearchPages/stores/EncounterFormStore.js +++ b/frontend/src/pages/SearchPages/stores/EncounterFormStore.js @@ -238,6 +238,10 @@ class EncounterFormStore { } } + setFormFilters(filters) { + this._formFilters = filters; + } + removeFilter(filterId) { this.formFilters = this.formFilters.filter((f) => f.filterId !== filterId); } @@ -250,6 +254,7 @@ class EncounterFormStore { resetFilters() { this.formFilters = []; + sessionStorage.removeItem("formData"); } async addEncountersToProject() { From 48716afba52a6846b72d8ca4e5d6fe8f5e5c636c Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Sun, 24 May 2026 12:05:30 -0500 Subject: [PATCH 02/10] finalize changes to manage filter form state --- frontend/src/components/FilterPanel.jsx | 6 +-- .../src/components/filterFields/SideBar.jsx | 4 +- .../src/pages/SearchPages/EncounterSearch.jsx | 23 ++-------- .../SearchPages/stores/EncounterFormStore.js | 45 ++++++++++++++++++- 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/FilterPanel.jsx b/frontend/src/components/FilterPanel.jsx index 59aec3ac05..2920021074 100644 --- a/frontend/src/components/FilterPanel.jsx +++ b/frontend/src/components/FilterPanel.jsx @@ -142,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 919fc239b8..8b03c04229 100644 --- a/frontend/src/components/filterFields/SideBar.jsx +++ b/frontend/src/components/filterFields/SideBar.jsx @@ -18,7 +18,7 @@ const Sidebar = observer( const handleShow = () => setShow(true); const [, setSearchParams] = useSearchParams(); - const num = () => (queryID ? 1 : store.formFilters.length); + const num = () => (queryID ? 1 : store.appliedFilters.length); const handleCopy = () => { if (navigator.clipboard && navigator.clipboard.writeText) { @@ -111,7 +111,7 @@ const Sidebar = observer( ) : (
- {store.formFilters.map((filter, index) => ( + {store.appliedFilters.map((filter, index) => ( {filter} ))}
diff --git a/frontend/src/pages/SearchPages/EncounterSearch.jsx b/frontend/src/pages/SearchPages/EncounterSearch.jsx index 1c1aeb544a..08325b1a54 100644 --- a/frontend/src/pages/SearchPages/EncounterSearch.jsx +++ b/frontend/src/pages/SearchPages/EncounterSearch.jsx @@ -45,28 +45,12 @@ const EncounterSearch = observer(() => { const [exportModalOpen, setExportModalOpen] = useState(false); useEffect(() => { - helperFunction( - searchParams, - store, - setFilterPanel, - // setTempFormFilters, - encounterData, - ); + helperFunction(searchParams, store, setFilterPanel, encounterData); }, [searchParams]); useEffect(() => { if (!queryID) { - const savedFiltersJson = sessionStorage.getItem("formData"); - if (savedFiltersJson) { - try { - const parsedFilters = JSON.parse(savedFiltersJson); - if (parsedFilters && parsedFilters.length > 0) { - store.setFormFilters(parsedFilters); - } - } catch (e) { - console.error("Failed to parse formData from session storage", e); - } - } + store.getFiltersFromStorage(); } }, [queryID, store]); @@ -85,7 +69,7 @@ const EncounterSearch = observer(() => { loading, refetch, } = useFilterEncounters({ - queries: store.formFilters, + queries: store.appliedFilters, params: { sort: encounterSortName, sortOrder: encounterSortOrder, @@ -324,7 +308,6 @@ const EncounterSearch = observer(() => { handleSearch={handleSearch} setQueryID={setQueryID} refetch={refetch} - // setTempFormFilters={setTempFormFilters} store={store} /> f.filterId === "numberMediaAssets"); if (!has) { return [...base, filterOnMediaAssets]; @@ -252,9 +263,39 @@ class EncounterFormStore { ); } + setFiltersInSessionStorage() { + if (this.appliedFilters.length > 0) { + sessionStorage.setItem( + this.FILTER_STORAGE_KEY, + JSON.stringify(this.appliedFilters), + ); + } + } + + applyFilters() { + this.appliedFilters = toJS(this.formFilters); + this.setFiltersInSessionStorage(); + } + + getFiltersFromStorage() { + const savedJson = sessionStorage.getItem(this.FILTER_STORAGE_KEY); + if (savedJson) { + try { + const parsed = JSON.parse(savedJson); + if (parsed && parsed.length > 0) { + this.formFilters = parsed; + this.appliedFilters = parsed; + } + } catch (e) { + console.error("Failed to load filters:", e); + } + } + } + resetFilters() { this.formFilters = []; - sessionStorage.removeItem("formData"); + this.appliedFilters = []; + sessionStorage.removeItem(this.FILTER_STORAGE_KEY); } async addEncountersToProject() { From 013292b592d96cacfefeb115f2176e0edad8082f Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Sun, 24 May 2026 12:09:01 -0500 Subject: [PATCH 03/10] add test coverage for filter form state management changes --- frontend/package-lock.json | 22 ------- .../EncounterFormStore.test.js | 65 +++++++++++++++++++ .../EncounterSearch.test.js | 19 ++++++ 3 files changed, 84 insertions(+), 22 deletions(-) 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..cd7cb4167a 100644 --- a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js +++ b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js @@ -1,5 +1,22 @@ import EncounterFormStore from "../../../pages/SearchPages/stores/EncounterFormStore"; +const mockSessionStorage = (() => { + let store = {}; + return { + getItem: jest.fn((key) => store[key] || null), + setItem: jest.fn((key, value) => { + store[key] = value.toString(); + }), + removeItem: jest.fn((key) => { + delete store[key]; + }), + clear: jest.fn(() => { + store = {}; + }), + }; +})(); +Object.defineProperty(window, "sessionStorage", { value: mockSessionStorage }); + describe("EncounterFormStore", () => { let store; @@ -72,5 +89,53 @@ describe("EncounterFormStore", () => { store.resetFilters(); expect(store.formFilters).toEqual([]); + expect(store.appliedFilters).toEqual([]); + }); + + 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]); + mockSessionStorage.getItem = jest.fn(() => savedData); + + store.getFiltersFromStorage(); + + expect(window.sessionStorage.getItem).toHaveBeenCalledTimes(1); + + expect(store.formFilters).toHaveLength(1); + expect(store.appliedFilters).toHaveLength(1); }); }); diff --git a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js index de7ebb9244..ebd0a86df0 100644 --- a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js +++ b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterSearch.test.js @@ -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,20 @@ describe("EncounterSearch", () => { expect(globalEncounterFormStore.setAssetOffset).toHaveBeenCalled(); }); }); + + it("calls getFiltersFromStorage on mount if no queryID is present", () => { + renderWithProviders("/search"); + + expect( + globalEncounterFormStore.getFiltersFromStorage, + ).toHaveBeenCalledTimes(1); + }); + + it("does NOT call getFiltersFromStorage if a queryID is present in the URL", () => { + renderWithProviders("/search?searchQueryId=abc123"); + + expect( + globalEncounterFormStore.getFiltersFromStorage, + ).not.toHaveBeenCalled(); + }); }); From 79c9223f3015dbf033b95ac41e473d08981684fe Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Wed, 27 May 2026 16:57:43 -0500 Subject: [PATCH 04/10] update methods to handle malformed storage data and clearing storage --- .../src/pages/SearchPages/stores/EncounterFormStore.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/SearchPages/stores/EncounterFormStore.js b/frontend/src/pages/SearchPages/stores/EncounterFormStore.js index 10eb06b2a2..26b4ad2efe 100644 --- a/frontend/src/pages/SearchPages/stores/EncounterFormStore.js +++ b/frontend/src/pages/SearchPages/stores/EncounterFormStore.js @@ -269,6 +269,8 @@ class EncounterFormStore { this.FILTER_STORAGE_KEY, JSON.stringify(this.appliedFilters), ); + } else { + sessionStorage.removeItem(this.FILTER_STORAGE_KEY); } } @@ -282,12 +284,15 @@ class EncounterFormStore { if (savedJson) { try { const parsed = JSON.parse(savedJson); - if (parsed && parsed.length > 0) { + if (Array.isArray(parsed) && parsed.length > 0) { this.formFilters = parsed; this.appliedFilters = parsed; + } else { + sessionStorage.removeItem(this.FILTER_STORAGE_KEY); } } catch (e) { console.error("Failed to load filters:", e); + sessionStorage.removeItem(this.FILTER_STORAGE_KEY); } } } From 6039f203695b030d6b4118e6d4fbfc269f48a8b7 Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Wed, 27 May 2026 17:02:07 -0500 Subject: [PATCH 05/10] update url parsing to cleanly get filters from storage and update filters after parsing --- frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js b/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js index 9365306eec..fc9c22cd4a 100644 --- a/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js +++ b/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js @@ -30,7 +30,7 @@ const helperFunction = async (searchParams, store, setFilterPanel) => { ); } if (key === "searchQueryId") { - store.formFilters = JSON.parse(sessionStorage.getItem("formData")) || []; + store.getFiltersFromStorage(); } if (key === "individualIDExact") { store.addFilter( @@ -45,6 +45,7 @@ const helperFunction = async (searchParams, store, setFilterPanel) => { ); } } + store.applyFilters(); setFilterPanel(false); return; }; From 178b6569ad09acfc318e9b57ad453ef153d9203b Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Wed, 27 May 2026 17:07:38 -0500 Subject: [PATCH 06/10] clean up sidebar query ID to false as default --- frontend/src/components/filterFields/SideBar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/filterFields/SideBar.jsx b/frontend/src/components/filterFields/SideBar.jsx index 8b03c04229..0471514ed0 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 = [], store }) => { + ({ setFilterPanel, searchQueryId, queryID = false, store }) => { const theme = React.useContext(ThemeContext); const [show, setShow] = useState(false); const sidebarWidth = 400; From 8f3cdd082f39293db3076f2897b29b406b25a0ff Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Wed, 27 May 2026 18:50:12 -0500 Subject: [PATCH 07/10] update tests to coverage for bad and empty data and passing data when no queryID is present --- .../EncounterFormStore.test.js | 82 +++++++++++++++---- .../EncounterSearch.test.js | 24 +++++- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js index cd7cb4167a..3060d6aaea 100644 --- a/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js +++ b/frontend/src/__tests__/pages/EncounterSearchPageAndFilters/EncounterFormStore.test.js @@ -1,27 +1,34 @@ import EncounterFormStore from "../../../pages/SearchPages/stores/EncounterFormStore"; -const mockSessionStorage = (() => { - let store = {}; - return { - getItem: jest.fn((key) => store[key] || null), - setItem: jest.fn((key, value) => { - store[key] = value.toString(); - }), - removeItem: jest.fn((key) => { - delete store[key]; - }), - clear: jest.fn(() => { - store = {}; - }), - }; -})(); -Object.defineProperty(window, "sessionStorage", { value: mockSessionStorage }); - 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", () => { @@ -90,6 +97,7 @@ describe("EncounterFormStore", () => { 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", () => { @@ -129,7 +137,7 @@ describe("EncounterFormStore", () => { path: "", }; const savedData = JSON.stringify([mockFilter]); - mockSessionStorage.getItem = jest.fn(() => savedData); + window.sessionStorage.setItem("formData", savedData); store.getFiltersFromStorage(); @@ -138,4 +146,42 @@ describe("EncounterFormStore", () => { 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 ebd0a86df0..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"; @@ -682,11 +682,33 @@ describe("EncounterSearch", () => { }); 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", () => { From acda5771b5b7df9047c80acdb1f5e83a2fdb7120 Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Sat, 30 May 2026 16:20:45 -0500 Subject: [PATCH 08/10] adjust function to only call applyFilters() if a new one has been added --- .../pages/SearchPages/getAllSearchParamsAndParse.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js b/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js index fc9c22cd4a..bc58017d49 100644 --- a/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js +++ b/frontend/src/pages/SearchPages/getAllSearchParamsAndParse.js @@ -4,6 +4,8 @@ const helperFunction = async (searchParams, store, setFilterPanel) => { return; } + let didAddFilter = false; + for (const [key, _] of Object.entries(params)) { if (key === "username") { store.addFilter( @@ -16,6 +18,7 @@ const helperFunction = async (searchParams, store, setFilterPanel) => { }, "Assigned User", ); + didAddFilter = true; } if (key === "state") { store.addFilter( @@ -28,6 +31,7 @@ const helperFunction = async (searchParams, store, setFilterPanel) => { }, "Encounter State", ); + didAddFilter = true; } if (key === "searchQueryId") { store.getFiltersFromStorage(); @@ -43,10 +47,14 @@ const helperFunction = async (searchParams, store, setFilterPanel) => { }, "Individual ID", ); + didAddFilter = true; } } - store.applyFilters(); - setFilterPanel(false); + if (didAddFilter) { + store.applyFilters(); + setFilterPanel(false); + } + return; }; From da9059f4202c79978adeaff0bdd63c95da17665b Mon Sep 17 00:00:00 2001 From: Jacob Mortensen Date: Sat, 30 May 2026 16:22:38 -0500 Subject: [PATCH 09/10] update error handling for malformed queryIds and verfied sidebar functionality --- .../src/components/filterFields/SideBar.jsx | 34 +++++++++++-------- .../src/pages/SearchPages/EncounterSearch.jsx | 6 +++- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/filterFields/SideBar.jsx b/frontend/src/components/filterFields/SideBar.jsx index 0471514ed0..851b23dfc6 100644 --- a/frontend/src/components/filterFields/SideBar.jsx +++ b/frontend/src/components/filterFields/SideBar.jsx @@ -135,21 +135,25 @@ const Sidebar = observer( > - { - setFilterPanel(true); - handleClose(); - }} - backgroundColor={theme.primaryColors.primary700} - borderColor={theme.primaryColors.primary700} - color="white" - noArrow={true} - > - - + + {!queryID && ( + { + setFilterPanel(true); + handleClose(); + }} + backgroundColor={theme.primaryColors.primary700} + borderColor={theme.primaryColors.primary700} + color="white" + noArrow={true} + > + + + )} + { }) .catch((error) => { console.error("Error fetching search data:", error); + alert(`Query ID: ${queryID} could not be found!`); + setQueryID(false); + setSearchParams(new URLSearchParams()); + setFilterPanel(true); }); } }, [ @@ -358,7 +362,7 @@ const EncounterSearch = observer(() => { Date: Tue, 16 Jun 2026 23:01:01 -0400 Subject: [PATCH 10/10] Copy the viewed query ID (not the regenerated one) in SideBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When viewing a result loaded from a shared searchQueryId URL param, the Copy button copied encounterData.searchQueryId — the ID freshly generated by the background filter query running on the viewer's own session filters, not the ID being viewed. This mismatched the "Search Query ID X applied" banner. Copy queryID (the URL-loaded ID) when present, falling back to the generated searchQueryId in normal filter-search mode. Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/filterFields/SideBar.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/filterFields/SideBar.jsx b/frontend/src/components/filterFields/SideBar.jsx index 851b23dfc6..1b13a29ddb 100644 --- a/frontend/src/components/filterFields/SideBar.jsx +++ b/frontend/src/components/filterFields/SideBar.jsx @@ -21,11 +21,14 @@ const Sidebar = observer( 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);