Skip to content

Commit

Permalink
Merge branch 'persist-memo-filter-with-url-query'
Browse files Browse the repository at this point in the history
  • Loading branch information
chriscurrycc committed Dec 30, 2024
2 parents 1b2a484 + 023a112 commit b11f518
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 18 deletions.
39 changes: 38 additions & 1 deletion web/src/components/MemoFilters.tsx
Original file line number Diff line number Diff line change
@@ -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";
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) {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/PagedMemoList/PagedMemoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const PagedMemoList = (props: Props) => {
});
setState(() => ({
isRequesting: false,
nextPageToken: response.nextPageToken,
nextPageToken: response?.nextPageToken || "",
}));
};

Expand Down
16 changes: 11 additions & 5 deletions web/src/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ 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 { usePrevious } from "react-use";
import useLocalStorage from "react-use/lib/useLocalStorage";
import Navigation from "@/components/Navigation";
import useCurrentUser from "@/hooks/useCurrentUser";
Expand All @@ -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) {
Expand All @@ -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 />
Expand Down
48 changes: 38 additions & 10 deletions web/src/store/v1/memo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = () => {
Expand Down
30 changes: 29 additions & 1 deletion web/src/store/v1/memoFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit b11f518

Please sign in to comment.