mirror of
https://github.com/usememos/memos.git
synced 2025-03-24 06:30:09 +01:00
feat: persist memo filters with url query (#4239)
* feat: persist memo filters with url query - Implemented synchronization of memo filters with URL search parameters. - Enhanced memo store to manage current request state for fetching memos, allowing for request cancellation, avoiding the race conditions. * refactor: update import
This commit is contained in:
parent
0e1ac30462
commit
e3b378b03b
@ -1,10 +1,47 @@
|
||||
import { isEqual } from "lodash-es";
|
||||
import { CalendarIcon, CheckCircleIcon, CodeIcon, EyeIcon, FilterIcon, LinkIcon, SearchIcon, TagIcon, XIcon } from "lucide-react";
|
||||
import { FilterFactor, getMemoFilterKey, MemoFilter, useMemoFilterStore } from "@/store/v1";
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import usePrevious from "react-use/lib/usePrevious";
|
||||
import { FilterFactor, getMemoFilterKey, MemoFilter, parseFilterQuery, stringifyFilters, useMemoFilterStore } from "@/store/v1";
|
||||
|
||||
const MemoFilters = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const memoFilterStore = useMemoFilterStore();
|
||||
const filters = memoFilterStore.filters;
|
||||
const prevFilters = usePrevious(filters);
|
||||
const orderByTimeAsc = memoFilterStore.orderByTimeAsc;
|
||||
const prevOrderByTimeAsc = usePrevious(orderByTimeAsc);
|
||||
|
||||
// Sync the filters and orderByTimeAsc to the search params.
|
||||
useEffect(() => {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
|
||||
if (prevOrderByTimeAsc !== orderByTimeAsc) {
|
||||
if (orderByTimeAsc) {
|
||||
newSearchParams.set("orderBy", "asc");
|
||||
} else {
|
||||
newSearchParams.delete("orderBy");
|
||||
}
|
||||
}
|
||||
|
||||
if (prevFilters && stringifyFilters(prevFilters) !== stringifyFilters(filters)) {
|
||||
if (filters.length > 0) {
|
||||
newSearchParams.set("filter", stringifyFilters(filters));
|
||||
} else {
|
||||
newSearchParams.delete("filter");
|
||||
}
|
||||
}
|
||||
|
||||
setSearchParams(newSearchParams);
|
||||
}, [prevOrderByTimeAsc, orderByTimeAsc, prevFilters, filters, searchParams]);
|
||||
|
||||
// Sync the search params to the filters and orderByTimeAsc when the component is mounted.
|
||||
useEffect(() => {
|
||||
const newFilters = parseFilterQuery(searchParams.get("filter"));
|
||||
const newOrderByTimeAsc = searchParams.get("orderBy") === "asc";
|
||||
memoFilterStore.setState({ filters: newFilters, orderByTimeAsc: newOrderByTimeAsc });
|
||||
}, []);
|
||||
|
||||
const getFilterDisplayText = (filter: MemoFilter) => {
|
||||
if (filter.value) {
|
||||
|
@ -41,7 +41,7 @@ const PagedMemoList = (props: Props) => {
|
||||
});
|
||||
setState(() => ({
|
||||
isRequesting: false,
|
||||
nextPageToken: response.nextPageToken,
|
||||
nextPageToken: response?.nextPageToken || "",
|
||||
}));
|
||||
};
|
||||
|
||||
|
@ -2,9 +2,10 @@ import { Tooltip } from "@mui/joy";
|
||||
import { Button } from "@usememos/mui";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
|
||||
import useLocalStorage from "react-use/lib/useLocalStorage";
|
||||
import usePrevious from "react-use/lib/usePrevious";
|
||||
import Navigation from "@/components/Navigation";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||
@ -16,11 +17,14 @@ import { useTranslate } from "@/utils/i18n";
|
||||
const RootLayout = () => {
|
||||
const t = useTranslate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { sm } = useResponsiveWidth();
|
||||
const currentUser = useCurrentUser();
|
||||
const memoFilterStore = useMemoFilterStore();
|
||||
const [collapsed, setCollapsed] = useLocalStorage<boolean>("navigation-collapsed", false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const pathname = useMemo(() => location.pathname, [location.pathname]);
|
||||
const prevPathname = usePrevious(pathname);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser) {
|
||||
@ -33,9 +37,11 @@ const RootLayout = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// When the route changes, remove all filters.
|
||||
memoFilterStore.removeFilter(() => true);
|
||||
}, [location.pathname]);
|
||||
// When the route changes and there is no filter in the search params, remove all filters.
|
||||
if (prevPathname !== pathname && !searchParams.has("filter")) {
|
||||
memoFilterStore.removeFilter(() => true);
|
||||
}
|
||||
}, [prevPathname, pathname, searchParams]);
|
||||
|
||||
return !initialized ? (
|
||||
<Loading />
|
||||
|
@ -9,28 +9,56 @@ interface State {
|
||||
// It should be update when any state change.
|
||||
stateId: string;
|
||||
memoMapByName: Record<string, Memo>;
|
||||
currentRequest: AbortController | null;
|
||||
}
|
||||
|
||||
const getDefaultState = (): State => ({
|
||||
stateId: uniqueId(),
|
||||
memoMapByName: {},
|
||||
currentRequest: null,
|
||||
});
|
||||
|
||||
export const useMemoStore = create(
|
||||
combine(getDefaultState(), (set, get) => ({
|
||||
setState: (state: State) => set(state),
|
||||
getState: () => get(),
|
||||
updateStateId: () => set({ stateId: uniqueId() }),
|
||||
fetchMemos: async (request: Partial<ListMemosRequest>) => {
|
||||
const { memos, nextPageToken } = await memoServiceClient.listMemos({
|
||||
...request,
|
||||
view: MemoView.MEMO_VIEW_FULL,
|
||||
});
|
||||
const memoMap = { ...get().memoMapByName };
|
||||
for (const memo of memos) {
|
||||
memoMap[memo.name] = memo;
|
||||
const currentRequest = get().currentRequest;
|
||||
if (currentRequest) {
|
||||
currentRequest.abort();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
set({ currentRequest: controller });
|
||||
|
||||
try {
|
||||
const { memos, nextPageToken } = await memoServiceClient.listMemos(
|
||||
{
|
||||
...request,
|
||||
view: MemoView.MEMO_VIEW_FULL,
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
if (!controller.signal.aborted) {
|
||||
const memoMap = request.pageToken ? { ...get().memoMapByName } : {};
|
||||
for (const memo of memos) {
|
||||
memoMap[memo.name] = memo;
|
||||
}
|
||||
set({ stateId: uniqueId(), memoMapByName: memoMap });
|
||||
return { memos, nextPageToken };
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (get().currentRequest === controller) {
|
||||
set({ currentRequest: null });
|
||||
}
|
||||
}
|
||||
set({ stateId: uniqueId(), memoMapByName: memoMap });
|
||||
return { memos, nextPageToken };
|
||||
},
|
||||
getOrFetchMemoByName: async (name: string, options?: { skipCache?: boolean; skipStore?: boolean }) => {
|
||||
const memoMap = get().memoMapByName;
|
||||
@ -99,7 +127,7 @@ export const useMemoList = () => {
|
||||
const memos = Object.values(memoStore.getState().memoMapByName);
|
||||
|
||||
const reset = () => {
|
||||
memoStore.setState({ stateId: uniqueId(), memoMapByName: {} });
|
||||
memoStore.updateStateId();
|
||||
};
|
||||
|
||||
const size = () => {
|
||||
|
@ -18,13 +18,41 @@ export interface MemoFilter {
|
||||
|
||||
export const getMemoFilterKey = (filter: MemoFilter) => `${filter.factor}:${filter.value}`;
|
||||
|
||||
export const parseFilterQuery = (query: string | null): MemoFilter[] => {
|
||||
if (!query) return [];
|
||||
try {
|
||||
return query.split(",").map((filterStr) => {
|
||||
const [factor, value] = filterStr.split(":");
|
||||
return {
|
||||
factor: factor as FilterFactor,
|
||||
value: decodeURIComponent(value),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to parse filter query:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const stringifyFilters = (filters: MemoFilter[]): string => {
|
||||
return filters.map((filter) => `${filter.factor}:${encodeURIComponent(filter.value)}`).join(",");
|
||||
};
|
||||
|
||||
interface State {
|
||||
filters: MemoFilter[];
|
||||
orderByTimeAsc: boolean;
|
||||
}
|
||||
|
||||
const getInitialState = (): State => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
filters: parseFilterQuery(searchParams.get("filter")),
|
||||
orderByTimeAsc: searchParams.get("orderBy") === "asc",
|
||||
};
|
||||
};
|
||||
|
||||
export const useMemoFilterStore = create(
|
||||
combine(((): State => ({ filters: [], orderByTimeAsc: false }))(), (set, get) => ({
|
||||
combine(getInitialState(), (set, get) => ({
|
||||
setState: (state: State) => set(state),
|
||||
getState: () => get(),
|
||||
getFiltersByFactor: (factor: FilterFactor) => get().filters.filter((f) => f.factor === factor),
|
||||
|
Loading…
x
Reference in New Issue
Block a user