3459235: Add POC of urlParams
Track UI state using URL params, allows deep linking.
@jessebaker this is the kind of thing I was eluding to instead of a router.
It looks like there's some hard-coded stuff in the state that isn't being respected - e.g. the load event is clobbering the selected component - but its a good start - panning/zooming etc is deep-linking.
Merge request reports
Activity
requested review from @jessebaker
assigned to @jessebaker
29 30 // Infer the `RootState` type from the root reducer 30 31 export type RootState = ReturnType<typeof rootReducer>; 31 32 33 let timeout: number | null; 34 32 35 // The store setup is wrapped in `makeStore` to allow reuse 33 36 // when setting up tests that need the same store config 34 export const makeStore = (preloadedState?: Partial<RootState>) => { 37 export const makeStore = (preloadedState?: Partial<RootState>, thisWindow?: PartialWindow) => { 44 previewApi.middleware 45 ]; 46 if (thisWindow) { 47 const urlParamListener = urlParamListenerFactory(thisWindow); 48 additionalMiddlewares.push(urlParamListener.middleware); 49 } 50 return getDefaultMiddleware().prepend(additionalMiddlewares) 51 }, 45 52 preloadedState, 46 53 }); 47 54 // configure listeners using the provided defaults 48 55 // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors 49 56 setupListeners(store.dispatch); 57 // Keep UI state in sync with back/forward from user. 58 if (thisWindow !== undefined) { 59 thisWindow.addEventListener('popstate', () => { - ui/src/app/urlParamListener.ts 0 → 100644
11 } from "@/features/ui/uiSlice"; 12 13 export type PartialWindow = Pick<Window, 'location' | 'history' | 'addEventListener' | 'setTimeout' | 'clearTimeout'>; 14 15 export type UrlParamStartListening = TypedStartListening<RootState, AppDispatch, PartialWindow> 16 17 /* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */ 18 const urlParamListenerFactory = (thisWindow: PartialWindow) => { 19 const urlParamListener = createListenerMiddleware({extra: thisWindow}); 20 21 const startUrlParamListening = urlParamListener.startListening as UrlParamStartListening; 22 23 let timeout: number | null; 24 25 startUrlParamListening({ 26 matcher: isAnyOf(setSelectedComponent, setPrimaryPanelOpen, setContextualPanelOpen, setCanvasViewPort, canvasViewPortZoomIn, canvasViewPortZoomOut), - ui/src/app/urlParamListener.ts 0 → 100644
20 21 const startUrlParamListening = urlParamListener.startListening as UrlParamStartListening; 22 23 let timeout: number | null; 24 25 startUrlParamListening({ 26 matcher: isAnyOf(setSelectedComponent, setPrimaryPanelOpen, setContextualPanelOpen, setCanvasViewPort, canvasViewPortZoomIn, canvasViewPortZoomOut), 27 effect: async (action, listenerApi) => { 28 const filterState = selectUiState(listenerApi.getState()); 29 const {history, location, setTimeout, clearTimeout} = listenerApi.extra; 30 if (timeout) { 31 clearTimeout(timeout); 32 timeout = null; 33 } 34 // Debounce this by 300ms. 35 timeout = setTimeout(() => history.pushState(null, '', `${location.pathname}?${asUrlSearchParams(filterState).toString()}`), 300); Yes, please.
Also: can we take advantage of https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share on browsers that support it?
- ui/src/app/urlParamListener.ts 0 → 100644
71 canvasViewport.y = Number(params.get('y')); 72 } 73 if (params.get('scale')) { 74 canvasViewport.scale = Number(params.get('scale')); 75 } 76 let selectedComponent = uiState.selectedComponent; 77 if (params.get('component')) { 78 selectedComponent = params.get('component') || undefined; 79 } 80 let primaryPanelOpen = uiState.primaryPanelOpen; 81 if (params.get('primary-panel')) { 82 primaryPanelOpen = Boolean(params.get('primary-panel')); 83 } 84 let contextualPanelOpen = uiState.contextualPanelOpen 85 if (params.get('contextual-panel')) { 86 contextualPanelOpen = Boolean(params.get('contextual-panel')); - Comment on lines +62 to +86
for some reason the props on the currentState were immutable, possibly because of the use of immer and not return new instances in the reducers, so I had to put these into variables instead of mutating the passed object
it works but its not as clean as what we normally do in client work IDK about the reason behind the immutability, but this suggests this MR should document why those are immutable, so that we can justify @larowlan's work-around.
!90 (merged) was merged.