diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/useAssetSelectionState.oss.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/useAssetSelectionState.oss.tsx index 080cc61ff0b20..501871204a37b 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/useAssetSelectionState.oss.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/useAssetSelectionState.oss.tsx @@ -4,5 +4,6 @@ export function useAssetSelectionState() { return useQueryPersistedState({ queryKey: 'asset-selection', defaults: {['asset-selection']: ''}, + behavior: 'push', }); } diff --git a/js_modules/dagster-ui/packages/ui-core/src/hooks/__tests__/useQueryPersistedState.test.tsx b/js_modules/dagster-ui/packages/ui-core/src/hooks/__tests__/useQueryPersistedState.test.tsx index 890991cced98a..6f571a4bc58ec 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/hooks/__tests__/useQueryPersistedState.test.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/hooks/__tests__/useQueryPersistedState.test.tsx @@ -1,7 +1,7 @@ -import {render, screen, waitFor} from '@testing-library/react'; +import {act, render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {useCallback, useMemo} from 'react'; -import {MemoryRouter} from 'react-router-dom'; +import {MemoryRouter, useHistory} from 'react-router-dom'; import {Route} from '../../app/Route'; import {useQueryPersistedState} from '../useQueryPersistedState'; @@ -348,4 +348,112 @@ describe('useQueryPersistedState', () => { ); }); }); + + it('supports push behavior', async () => { + let querySearch: string | undefined; + + let goback: () => void; + + const Test = ({options}: {options: Parameters[0]}) => { + const [_, setQuery] = useQueryPersistedState(options); + const history = useHistory(); + goback = () => history.goBack(); + return ( + <> +
setQuery('one')}>one
+
setQuery('two')}>two
+
setQuery('three')}>three
+ + ); + }; + + render( + + + (querySearch = location.search) && } /> + , + ); + + await userEvent.click(screen.getByText(`one`)); + await waitFor(() => { + expect(querySearch).toEqual('?q=one'); + }); + + await userEvent.click(screen.getByText(`two`)); + await waitFor(() => { + expect(querySearch).toEqual('?q=two'); + }); + + await userEvent.click(screen.getByText(`three`)); + await waitFor(() => { + expect(querySearch).toEqual('?q=three'); + }); + + await act(() => { + goback(); + }); + + await waitFor(() => { + expect(querySearch).toEqual('?q=two'); + }); + + await act(() => { + goback(); + }); + + await waitFor(() => { + expect(querySearch).toEqual('?q=one'); + }); + }); + + it('supports replace behavior', async () => { + let querySearch: string | undefined; + + let goback: () => void; + let push: (...args: Parameters['push']>) => void; + + const Test = ({options}: {options: Parameters[0]}) => { + const [_, setQuery] = useQueryPersistedState(options); + const history = useHistory(); + goback = () => history.goBack(); + push = history.push.bind(history); + return ( + <> +
setQuery('one')}>one
+
setQuery('two')}>two
+
setQuery('three')}>three
+ + ); + }; + + render( + + + (querySearch = location.search) && } /> + , + ); + + push!('/page?'); + + await userEvent.click(screen.getByText(`one`)); + await waitFor(() => { + expect(querySearch).toEqual('?q=one'); + }); + + await userEvent.click(screen.getByText(`two`)); + await waitFor(() => { + expect(querySearch).toEqual('?q=two'); + }); + + await userEvent.click(screen.getByText(`three`)); + await waitFor(() => { + expect(querySearch).toEqual('?q=three'); + }); + + await act(() => { + goback(); + }); + + expect(querySearch).toEqual('?q=B'); // end up back on initial route + }); }); diff --git a/js_modules/dagster-ui/packages/ui-core/src/hooks/useQueryPersistedState.tsx b/js_modules/dagster-ui/packages/ui-core/src/hooks/useQueryPersistedState.tsx index e540d3acd32c8..63cccfe5a3f1d 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/hooks/useQueryPersistedState.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/hooks/useQueryPersistedState.tsx @@ -21,6 +21,7 @@ export type QueryPersistedStateConfig = { defaults?: {[key: string]: any}; decode?: (raw: {[key: string]: any}) => T; encode?: (raw: T) => {[key: string]: any}; + behavior?: 'push' | 'replace'; }; const defaultEncode = memoize((queryKey: string) => (raw: T) => ({[queryKey]: raw})); @@ -63,7 +64,7 @@ const defaultDecode = memoize( export function useQueryPersistedState( options: QueryPersistedStateConfig, ): [T, React.Dispatch>] { - const {queryKey, defaults} = options; + const {queryKey, defaults, behavior = 'replace'} = options; let {encode, decode} = options; if (queryKey) { @@ -113,12 +114,15 @@ export function useQueryPersistedState( // the `replace` so that we surface any unwanted loops during development. if (process.env.NODE_ENV !== 'production' || !areQueriesEqual(currentQueryString, next)) { currentQueryString = next; - history.replace( - `${history.location.pathname}?${qs.stringify(next, {arrayFormat: 'brackets'})}`, - ); + const nextPath = `${history.location.pathname}?${qs.stringify(next, {arrayFormat: 'brackets'})}`; + if (behavior === 'replace') { + history.replace(nextPath); + } else { + history.push(nextPath); + } } }, - [history, encode, options], + [encode, options.defaults, behavior, history], ); if (!isEqual(valueRef.current, qsDecoded)) {