diff --git a/web/src/layouts/RootLayout.tsx b/web/src/layouts/RootLayout.tsx index 68c48c42ce859..1bb41aaa5cd15 100644 --- a/web/src/layouts/RootLayout.tsx +++ b/web/src/layouts/RootLayout.tsx @@ -4,6 +4,7 @@ import clsx from "clsx"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { Suspense, useEffect, useState } from "react"; import { Outlet, useLocation } from "react-router-dom"; +import { usePrevious } from "react-use"; import useLocalStorage from "react-use/lib/useLocalStorage"; import Navigation from "@/components/Navigation"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -21,6 +22,7 @@ const RootLayout = () => { const memoFilterStore = useMemoFilterStore(); const [collapsed, setCollapsed] = useLocalStorage("navigation-collapsed", false); const [initialized, setInitialized] = useState(false); + const prevPathname = usePrevious(location.pathname); useEffect(() => { if (!currentUser) { @@ -34,7 +36,9 @@ const RootLayout = () => { useEffect(() => { // When the route changes, remove all filters. - memoFilterStore.removeFilter(() => true); + if (prevPathname && prevPathname !== location.pathname) { + memoFilterStore.removeFilter(() => true); + } }, [location.pathname]); return !initialized ? ( diff --git a/web/src/store/v1/memoFilter.ts b/web/src/store/v1/memoFilter.ts index b3183969441bb..1a43a7cb60a76 100644 --- a/web/src/store/v1/memoFilter.ts +++ b/web/src/store/v1/memoFilter.ts @@ -1,6 +1,6 @@ import { uniqBy } from "lodash-es"; import { create } from "zustand"; -import { combine } from "zustand/middleware"; +import { persist, createJSONStorage, StateStorage } from "zustand/middleware"; export type FilterFactor = | "tagSearch" @@ -16,20 +16,103 @@ export interface MemoFilter { value: string; } +const getFilterQueryString = (filters: MemoFilter[]) => { + return filters.map(({ factor, value }) => `${factor}:${encodeURIComponent(value)}`).join(","); +}; + +const getFiltersFromQueryString = (queryString: string) => { + return queryString.split(",").map((str) => { + const [factor, value] = str.split(":"); + return { + factor: factor as FilterFactor, + value: decodeURIComponent(value), + }; + }); +}; + +const replaceState = (searchParams: URLSearchParams) => { + window.history.replaceState(null, "", `${window.location.pathname}${searchParams.toString() ? "?" + searchParams.toString() : ""}`); +}; + export const getMemoFilterKey = (filter: MemoFilter) => `${filter.factor}:${filter.value}`; +const VERSION = 0; + interface State { filters: MemoFilter[]; orderByTimeAsc: boolean; } +const urlQueryStorage: StateStorage = { + getItem: () => { + const searchParams = new URLSearchParams(window.location.search); + const filterQuery = searchParams.get("filter"); + const orderBy = searchParams.get("orderBy"); + + return JSON.stringify({ + state: { + filters: filterQuery ? getFiltersFromQueryString(filterQuery) : [], + orderByTimeAsc: orderBy === "asc", + } as State, + version: VERSION, + }); + }, + setItem: (_, newValue) => { + const { state, version } = JSON.parse(newValue); + if (!state || typeof state !== "object" || version !== VERSION) return; + + const { filters, orderByTimeAsc } = state as State; + const searchParams = new URLSearchParams(window.location.search); + + if (filters.length > 0) { + const filterStr = getFilterQueryString(filters); + searchParams.set("filter", filterStr); + } else { + searchParams.delete("filter"); + } + + if (orderByTimeAsc) { + searchParams.set("orderBy", "asc"); + } else { + searchParams.delete("orderBy"); + } + + replaceState(searchParams); + }, + removeItem: () => { + const searchParams = new URLSearchParams(window.location.search); + + searchParams.delete("filter"); + searchParams.delete("orderBy"); + replaceState(searchParams); + }, +}; + export const useMemoFilterStore = create( - combine(((): State => ({ filters: [], orderByTimeAsc: false }))(), (set, get) => ({ - setState: (state: State) => set(state), - getState: () => get(), - getFiltersByFactor: (factor: FilterFactor) => get().filters.filter((f) => f.factor === factor), - addFilter: (filter: MemoFilter) => set((state) => ({ filters: uniqBy([...state.filters, filter], getMemoFilterKey) })), - removeFilter: (filterFn: (f: MemoFilter) => boolean) => set((state) => ({ filters: state.filters.filter((f) => !filterFn(f)) })), - setOrderByTimeAsc: (orderByTimeAsc: boolean) => set({ orderByTimeAsc }), - })), + persist< + State & { + setState: (state: State) => void; + getState: () => State; + getFiltersByFactor: (factor: FilterFactor) => MemoFilter[]; + addFilter: (filter: MemoFilter) => void; + removeFilter: (filterFn: (f: MemoFilter) => boolean) => void; + setOrderByTimeAsc: (orderByTimeAsc: boolean) => void; + } + >( + (set, get) => ({ + filters: [], + orderByTimeAsc: false, + setState: (state) => set(state), + getState: () => get(), + getFiltersByFactor: (factor) => get().filters.filter((f) => f.factor === factor), + addFilter: (filter) => set((state) => ({ filters: uniqBy([...state.filters, filter], getMemoFilterKey) })), + removeFilter: (filterFn) => set((state) => ({ filters: state.filters.filter((f) => !filterFn(f)) })), + setOrderByTimeAsc: (orderByTimeAsc) => set({ orderByTimeAsc }), + }), + { + name: "memo-filter-storage", + storage: createJSONStorage(() => urlQueryStorage), + version: VERSION, + }, + ), );