+
{/* TODO revisit the completely rounded style of button used for clearing the study list filter - for now use LegacyButton*/}
{isFiltering && (
{t('Studies')}
diff --git a/platform/ui/src/components/StudyListTable/StudyListTableRow.tsx b/platform/ui/src/components/StudyListTable/StudyListTableRow.tsx
index 079a2985f4d..2235f277d1c 100644
--- a/platform/ui/src/components/StudyListTable/StudyListTableRow.tsx
+++ b/platform/ui/src/components/StudyListTable/StudyListTableRow.tsx
@@ -7,10 +7,13 @@ import Icon from '../Icon';
const StudyListTableRow = props => {
const { tableData } = props;
- const { row, expandedContent, onClickRow, isExpanded } = tableData;
+ const { row, expandedContent, onClickRow, isExpanded, dataCY } = tableData;
return (
<>
-
+
{
+ beforeEach(() => {
+ window.sessionStorage.removeItem(SESSION_STORAGE_KEY);
+ });
+
+ it('hook should return state and setState', () => {
+ const data = { test: 1 };
+ const { result } = renderHook(() =>
+ useSessionStorage({ key: SESSION_STORAGE_KEY, defaultValue: data })
+ );
+ const [hookState, setHookState] = result.current;
+ expect(hookState).toStrictEqual(data);
+ expect(typeof setHookState).toBe('function');
+ });
+
+ it('hook should store data on sessionStorage', () => {
+ const data = { test: 2 };
+ renderHook(() => useSessionStorage({ key: SESSION_STORAGE_KEY, defaultValue: data }));
+
+ const dataStr = JSON.stringify(data);
+ const dataSessionStorage = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
+ expect(dataSessionStorage).toEqual(dataStr);
+ });
+
+ it('hook should return stored data from sessionStorage', () => {
+ const data = { test: 3 };
+ const dataToCompare = { test: 4 };
+
+ window.sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(dataToCompare));
+
+ const { result } = renderHook(() =>
+ useSessionStorage({ key: SESSION_STORAGE_KEY, defaultValue: data })
+ );
+ const [hookState, setHookState] = result.current;
+
+ expect(hookState).toStrictEqual(dataToCompare);
+ });
+
+ it('hook should provide a setState method which updates its state', () => {
+ const data = { test: 5 };
+ const dataToCompare = { test: 6 };
+ const { result } = renderHook(() =>
+ useSessionStorage({ key: SESSION_STORAGE_KEY, defaultValue: data })
+ );
+ const [hookState, setHookState] = result.current;
+
+ act(() => {
+ setHookState(dataToCompare);
+ });
+
+ const dataToCompareStr = JSON.stringify(dataToCompare);
+ const dataSessionStorage = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
+
+ const [hookStateToCompare] = result.current;
+ expect(dataSessionStorage).toEqual(dataToCompareStr);
+ expect(hookStateToCompare).toStrictEqual(dataToCompare);
+ });
+
+ it('hook state must be preserved in case rerender', () => {
+ const data = { test: 7 };
+ const { result, rerender } = renderHook(() =>
+ useSessionStorage({ key: SESSION_STORAGE_KEY, defaultValue: data })
+ );
+
+ rerender();
+
+ const [hookState, setHookState] = result.current;
+
+ const dataToCompareStr = JSON.stringify(data);
+ const dataSessionStorage = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
+
+ expect(dataSessionStorage).toEqual(dataToCompareStr);
+ expect(hookState).toStrictEqual(data);
+ });
+
+ it('hook state must be preserved in case multiple operations and rerender', () => {
+ const data = { test: 8 };
+ const dataToCompare = { test: 9 };
+ const { result, rerender } = renderHook(() =>
+ useSessionStorage({ key: SESSION_STORAGE_KEY, defaultValue: data })
+ );
+ const [hookState, setHookState] = result.current;
+
+ act(() => {
+ setHookState(dataToCompare);
+ });
+
+ rerender();
+
+ const dataToCompareStr = JSON.stringify(dataToCompare);
+ const dataSessionStorage = window.sessionStorage.getItem(SESSION_STORAGE_KEY);
+
+ const [hookStateToCompare] = result.current;
+ expect(dataSessionStorage).toEqual(dataToCompareStr);
+ expect(hookStateToCompare).toStrictEqual(dataToCompare);
+ });
+});
diff --git a/platform/ui/src/hooks/useSessionStorage.tsx b/platform/ui/src/hooks/useSessionStorage.tsx
new file mode 100644
index 00000000000..e1c16282b58
--- /dev/null
+++ b/platform/ui/src/hooks/useSessionStorage.tsx
@@ -0,0 +1,71 @@
+import { useState, useEffect, useCallback } from 'react';
+
+/**
+ * A map of session storage items that should be cleared out of session storage
+ * when the page unloads.
+ */
+const sessionItemsToClearOnUnload: Map = new Map();
+
+/**
+ * This callback simulates clearing the various session items when a page unloads.
+ * When the page is hidden the session storage items are removed but maintained
+ * in the map above in case the page becomes visible again. So those pages that
+ * are hidden because they are being unloaded have their session storage disposed
+ * of for ever. For those pages that are hidden, but later return to visible,
+ * this callback restores the session storage from the map above.
+ */
+const visibilityChangeCallback = () => {
+ if (document.visibilityState === 'hidden') {
+ Array.from(sessionItemsToClearOnUnload.keys()).forEach(key => {
+ window.sessionStorage.removeItem(key);
+ });
+ } else {
+ Array.from(sessionItemsToClearOnUnload.keys()).forEach(key => {
+ window.sessionStorage.setItem(key, sessionItemsToClearOnUnload.get(key));
+ });
+ }
+};
+
+/**
+ * Technically there is no memory leak here because the listener needs to
+ * persist until the page unloads and once the page unloads it will be gone.
+ */
+document.addEventListener('visibilitychange', visibilityChangeCallback);
+
+type useSessionStorageProps = {
+ key: string;
+ defaultValue: unknown;
+ clearOnUnload: boolean;
+};
+
+const useSessionStorage = ({
+ key,
+ defaultValue = {},
+ clearOnUnload = false,
+}: useSessionStorageProps) => {
+ const valueFromStorage = window.sessionStorage.getItem(key);
+ const storageValue = valueFromStorage ? JSON.parse(valueFromStorage) : defaultValue;
+ const [sessionItem, setSessionItem] = useState({ ...storageValue });
+
+ const updateSessionItem = useCallback(value => {
+ setSessionItem({ ...value });
+
+ const valueAsStr = JSON.stringify(value);
+
+ if (!clearOnUnload || document.visibilityState === 'visible') {
+ window.sessionStorage.setItem(key, valueAsStr);
+ }
+
+ if (clearOnUnload) {
+ sessionItemsToClearOnUnload.set(key, valueAsStr);
+ }
+ }, []);
+
+ useEffect(() => {
+ updateSessionItem(sessionItem);
+ }, []);
+
+ return [sessionItem, updateSessionItem];
+};
+
+export default useSessionStorage;
diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js
index 1acacefd16d..69fc814fa53 100644
--- a/platform/ui/src/index.js
+++ b/platform/ui/src/index.js
@@ -119,6 +119,8 @@ export {
ViewportOverlay,
} from './components';
+export { useSessionStorage } from './hooks';
+
/** These are mostly used in the docs */
export { getIcon, ICONS, addIcon } from './components/Icon/getIcon';
export { BackgroundColor } from './pages/Colors/BackgroundColor';
diff --git a/yarn.lock b/yarn.lock
index 9b8ad2e7f4c..a52cf2fb73c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4860,6 +4860,14 @@
dependencies:
defer-to-connect "^2.0.1"
+"@testing-library/react-hooks@^3.2.1":
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.7.0.tgz#6d75c5255ef49bce39b6465bf6b49e2dac84919e"
+ integrity sha512-TwfbY6BWtWIHitjT05sbllyLIProcysC0dF0q1bbDa7OHLC6A6rJOYJwZ13hzfz3O4RtOuInmprBozJRyyo7/g==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ "@types/testing-library__react-hooks" "^3.4.0"
+
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -5322,6 +5330,13 @@
"@types/history" "^4.7.11"
"@types/react" "*"
+"@types/react-test-renderer@*":
+ version "18.0.5"
+ resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.5.tgz#b67a6ff37acd93d1b971ec4c838f69d52e772db0"
+ integrity sha512-PsnmF4Hpi61PTRX+dTxkjgDdtZ09kFFgPXczoF+yBfOVxn7xBLPvKP1BUrSasYHmerj33rhoJuvpIMsJuyRqHw==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-transition-group@^4.4.0":
version "4.4.6"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e"
@@ -5437,6 +5452,13 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
+"@types/testing-library__react-hooks@^3.4.0":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.1.tgz#b8d7311c6c1f7db3103e94095fe901f8fef6e433"
+ integrity sha512-G4JdzEcq61fUyV6wVW9ebHWEiLK2iQvaBuCHHn9eMSbZzVh4Z4wHnUGIvQOYCCYeu5DnUtFyNYuAAgbSaO/43Q==
+ dependencies:
+ "@types/react-test-renderer" "*"
+
"@types/tough-cookie@*":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
@@ -17331,7 +17353,7 @@ react-is@18.1.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==
-react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4:
+react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.8.6:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -17528,6 +17550,16 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
+react-test-renderer@^16.12.0:
+ version "16.14.0"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae"
+ integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.19.1"
+
react-textarea-autosize@^8.3.2:
version "8.5.2"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.2.tgz#6421df2b5b50b9ca8c5e96fd31be688ea7fa2f9d"
@@ -18323,6 +18355,14 @@ saxes@^6.0.0:
dependencies:
xmlchars "^2.2.0"
+scheduler@^0.19.1:
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
+ integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|