feat(filters): save worklist query filters to session storage so that they persist between navigation to the viewer and back (#3749)

Co-authored-by: ladeirarodolfo <39910206+ladeirarodolfo@users.noreply.github.com>
This commit is contained in:
Joe Boccanfusoandladeirarodolfo authored and GitHub committed 2023-10-30 10:47:43 -04:00
1 parent db395852b6
commit 2a15ef0e44
13 files changed
+322 -9

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
const base = require('../../jest.config.base.js');
const pkg = require('./package');
module.exports = {
...base,
name: pkg.name,
displayName: pkg.name,
// rootDir: "../.."
// testMatch: [
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
// "<rootDir>/platform/app/**/*.test.js"
// ]
};
+3 -1
View File
@@ -50,7 +50,9 @@
"react-window": "^1.8.9",
"react-with-direction": "^1.3.1",
"swiper": "^8.4.2",
"webpack": "^5.81.0"
"webpack": "^5.81.0",
"@testing-library/react-hooks": "^3.2.1",
"react-test-renderer": "^16.12.0"
},
"devDependencies": {
"@babel/core": "^7.21.4",
@@ -175,7 +175,7 @@ DateRange.propTypes = {
/** YYYYMMDD (19921022) */
startDate: PropTypes.string,
/** YYYYMMDD (19921022) */
endDate: PropTypes.object,
endDate: PropTypes.string,
/** Callback that received { startDate: string(YYYYMMDD), endDate: string(YYYYMMDD)} */
onChange: PropTypes.func.isRequired,
};
@@ -43,6 +43,7 @@ function Header({
isReturnEnabled && 'cursor-pointer'
)}
onClick={onClickReturn}
data-cy="return-to-work-list"
>
{isReturnEnabled && (
<Icon
@@ -52,7 +52,7 @@ const StudyListFilter = ({
</div>
)}
</div>
<div className="flex flex-row">
<div className="flex h-[34px] flex-row items-center">
{/* TODO revisit the completely rounded style of button used for clearing the study list filter - for now use LegacyButton*/}
{isFiltering && (
<LegacyButton
@@ -76,7 +76,7 @@ const StudyListFilter = ({
</Typography>
<Typography
variant="h6"
className="text-primary-light self-end pb-1"
className="text-primary-light"
>
{t('Studies')}
</Typography>
@@ -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 (
<>
<tr className="select-none">
<tr
className="select-none"
data-cy={dataCY}
>
<td
className={classnames('border-0 p-0', {
'border-secondary-light bg-primary-dark border-b': isExpanded,
@@ -104,6 +107,7 @@ StudyListTableRow.propTypes = {
expandedContent: PropTypes.node.isRequired,
onClickRow: PropTypes.func.isRequired,
isExpanded: PropTypes.bool.isRequired,
dataCY: PropTypes.string,
}),
};
+2 -1
View File
@@ -1,3 +1,4 @@
import useResizeObserver from './useResizeObserver';
import useSessionStorage from './useSessionStorage';
export { useResizeObserver };
export { useResizeObserver, useSessionStorage };
@@ -0,0 +1,102 @@
import { renderHook, act } from '@testing-library/react-hooks';
import useSessionStorage from './useSessionStorage';
const SESSION_STORAGE_KEY = 'test';
describe('Hook Session Storage', () => {
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);
});
});
@@ -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<string, string> = new Map<string, string>();
/**
* 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;
+2
View File
@@ -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';