refactor: memo filter store

pull/4717/head
Steven 2 months ago
parent f12d7ae8bc
commit c23aebd648

@ -12,10 +12,10 @@
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@github/relative-time-element": "^4.4.5",
"@github/relative-time-element": "^4.4.8",
"@matejmazur/react-katex": "^3.1.3",
"@mui/joy": "5.0.0-beta.51",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-popover": "^1.1.14",
"@usememos/mui": "0.1.0-20250515140125",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
@ -23,60 +23,59 @@
"fuse.js": "^7.1.0",
"highlight.js": "^11.11.1",
"i18next": "^24.2.3",
"katex": "^0.16.21",
"katex": "^0.16.22",
"leaflet": "^1.9.4",
"lodash-es": "^4.17.21",
"lucide-react": "^0.486.0",
"mermaid": "^11.4.1",
"mermaid": "^11.6.0",
"mobx": "^6.13.7",
"mobx-react-lite": "^4.1.0",
"react": "^18.3.1",
"react-datepicker": "^8.2.1",
"react-datepicker": "^8.4.0",
"react-dom": "^18.3.1",
"react-force-graph-2d": "^1.27.0",
"react-force-graph-2d": "^1.27.1",
"react-hot-toast": "^2.5.2",
"react-i18next": "^15.4.1",
"react-i18next": "^15.5.2",
"react-leaflet": "^4.2.1",
"react-router-dom": "^7.3.0",
"react-router-dom": "^7.6.1",
"react-simple-pull-to-refresh": "^1.3.3",
"react-use": "^17.6.0",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"textarea-caret": "^3.1.0",
"uuid": "^11.1.0",
"zustand": "^5.0.3"
"uuid": "^11.1.0"
},
"devDependencies": {
"@bufbuild/protobuf": "^2.2.5",
"@eslint/js": "^9.23.0",
"@bufbuild/protobuf": "^2.5.0",
"@eslint/js": "^9.27.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/d3": "^7.4.3",
"@types/katex": "^0.16.7",
"@types/leaflet": "^1.9.16",
"@types/leaflet": "^1.9.18",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.15.3",
"@types/qs": "^6.9.18",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/textarea-caret": "^3.0.3",
"@types/node": "^22.15.21",
"@types/qs": "^6.14.0",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/textarea-caret": "^3.0.4",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-legacy": "^6.1.1",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^4.5.0",
"autoprefixer": "^10.4.21",
"code-inspector-plugin": "^0.18.3",
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-react": "^7.37.4",
"long": "^5.3.1",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"long": "^5.3.2",
"nice-grpc-web": "^3.3.7",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"terser": "^5.39.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.28.0",
"vite": "^6.2.1"
"terser": "^5.39.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
},
"pnpm": {
"onlyBuiltDependencies": [

File diff suppressed because it is too large Load Diff

@ -4,8 +4,8 @@ import { observer } from "mobx-react-lite";
import { shortcutServiceClient } from "@/grpcweb";
import useAsyncEffect from "@/hooks/useAsyncEffect";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoFilterStore } from "@/store/v1";
import { userStore } from "@/store/v2";
import memoFilterStore from "@/store/v2/memoFilter";
import { Shortcut } from "@/types/proto/api/v1/shortcut_service";
import { cn } from "@/utils";
import { useTranslate } from "@/utils/i18n";
@ -16,7 +16,6 @@ const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)$/u;
const ShortcutsSection = observer(() => {
const t = useTranslate();
const user = useCurrentUser();
const memoFilterStore = useMemoFilterStore();
const shortcuts = userStore.state.shortcuts;
useAsyncEffect(async () => {

@ -4,8 +4,8 @@ import { observer } from "mobx-react-lite";
import toast from "react-hot-toast";
import useLocalStorage from "react-use/lib/useLocalStorage";
import { memoServiceClient } from "@/grpcweb";
import { useMemoFilterStore } from "@/store/v1";
import { userStore } from "@/store/v2";
import memoFilterStore, { MemoFilter } from "@/store/v2/memoFilter";
import { cn } from "@/utils";
import { useTranslate } from "@/utils/i18n";
import showRenameTagDialog from "../RenameTagDialog";
@ -18,16 +18,15 @@ interface Props {
const TagsSection = observer((props: Props) => {
const t = useTranslate();
const memoFilterStore = useMemoFilterStore();
const [treeMode, setTreeMode] = useLocalStorage<boolean>("tag-view-as-tree", false);
const tags = Object.entries(userStore.state.tagCount)
.sort((a, b) => a[0].localeCompare(b[0]))
.sort((a, b) => b[1] - a[1]);
const handleTagClick = (tag: string) => {
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter) => filter.value === tag);
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter: MemoFilter) => filter.value === tag);
if (isActive) {
memoFilterStore.removeFilter((f) => f.factor === "tagSearch" && f.value === tag);
memoFilterStore.removeFilter((f: MemoFilter) => f.factor === "tagSearch" && f.value === tag);
} else {
memoFilterStore.addFilter({
factor: "tagSearch",

@ -2,7 +2,8 @@ import { useContext } from "react";
import { useLocation } from "react-router-dom";
import useNavigateTo from "@/hooks/useNavigateTo";
import { Routes } from "@/router";
import { stringifyFilters, useMemoFilterStore } from "@/store/v1";
import memoFilterStore, { MemoFilter } from "@/store/v2/memoFilter";
import { stringifyFilters } from "@/store/v2/memoFilter";
import { cn } from "@/utils";
import { RendererContext } from "./types";
@ -12,7 +13,6 @@ interface Props {
const Tag: React.FC<Props> = ({ content }: Props) => {
const context = useContext(RendererContext);
const memoFilterStore = useMemoFilterStore();
const location = useLocation();
const navigateTo = useNavigateTo();
@ -31,9 +31,9 @@ const Tag: React.FC<Props> = ({ content }: Props) => {
return;
}
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter) => filter.value === content);
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter: MemoFilter) => filter.value === content);
if (isActive) {
memoFilterStore.removeFilter((f) => f.factor === "tagSearch" && f.value === content);
memoFilterStore.removeFilter((f: MemoFilter) => f.factor === "tagSearch" && f.value === content);
} else {
memoFilterStore.addFilter({
factor: "tagSearch",

@ -2,13 +2,13 @@ import { isEqual } from "lodash-es";
import { CalendarIcon, CheckCircleIcon, CodeIcon, EyeIcon, HashIcon, LinkIcon, BookmarkIcon, SearchIcon, XIcon } from "lucide-react";
import { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { FilterFactor, getMemoFilterKey, MemoFilter, stringifyFilters, useMemoFilterStore } from "@/store/v1";
import memoFilterStore from "@/store/v2/memoFilter";
import { FilterFactor, getMemoFilterKey, MemoFilter, stringifyFilters } from "@/store/v2/memoFilter";
import { useTranslate } from "@/utils/i18n";
const MemoFilters = () => {
const t = useTranslate();
const [, setSearchParams] = useSearchParams();
const memoFilterStore = useMemoFilterStore();
const filters = memoFilterStore.filters;
useEffect(() => {
@ -45,11 +45,11 @@ const MemoFilters = () => {
return (
<div className="w-full mt-2 flex flex-row justify-start items-center flex-wrap gap-x-2 gap-y-1">
{filters.map((filter) => (
{filters.map((filter: MemoFilter) => (
<div
key={getMemoFilterKey(filter)}
className="w-auto leading-7 h-7 shrink-0 flex flex-row items-center gap-1 bg-white dark:bg-zinc-800 border dark:border-zinc-700 pl-1.5 pr-1 rounded-md hover:line-through cursor-pointer"
onClick={() => memoFilterStore.removeFilter((f) => isEqual(f, filter))}
onClick={() => memoFilterStore.removeFilter((f: MemoFilter) => isEqual(f, filter))}
>
<FactorIcon className="w-4 h-auto text-gray-500 dark:text-gray-400 opacity-60" factor={filter.factor} />
<span className="text-gray-500 dark:text-gray-400 text-sm max-w-32 truncate">{getFilterDisplayText(filter)}</span>

@ -1,12 +1,11 @@
import { SearchIcon } from "lucide-react";
import { useState } from "react";
import { useMemoFilterStore } from "@/store/v1";
import memoFilterStore from "@/store/v2/memoFilter";
import { useTranslate } from "@/utils/i18n";
import MemoDisplaySettingMenu from "./MemoDisplaySettingMenu";
const SearchBar = () => {
const t = useTranslate();
const memoFilterStore = useMemoFilterStore();
const [queryText, setQueryText] = useState("");
const onTextChange = (event: React.FormEvent<HTMLInputElement>) => {

@ -10,8 +10,8 @@ import useAsyncEffect from "@/hooks/useAsyncEffect";
import useCurrentUser from "@/hooks/useCurrentUser";
import i18n from "@/i18n";
import { Routes } from "@/router";
import { useMemoFilterStore } from "@/store/v1";
import { userStore } from "@/store/v2";
import memoFilterStore from "@/store/v2/memoFilter";
import { UserStats_MemoTypeStats } from "@/types/proto/api/v1/user_service";
import { cn } from "@/utils";
import { useTranslate } from "@/utils/i18n";
@ -21,7 +21,6 @@ import "react-datepicker/dist/react-datepicker.css";
const StatisticsView = observer(() => {
const t = useTranslate();
const location = useLocation();
const memoFilterStore = useMemoFilterStore();
const currentUser = useCurrentUser();
const [memoTypeStats, setMemoTypeStats] = useState<UserStats_MemoTypeStats>(UserStats_MemoTypeStats.fromPartial({}));
const [activityStats, setActivityStats] = useState<Record<string, number>>({});

@ -1,7 +1,7 @@
import { ChevronRightIcon, HashIcon } from "lucide-react";
import { useEffect, useState } from "react";
import useToggle from "react-use/lib/useToggle";
import { useMemoFilterStore } from "@/store/v1";
import memoFilterStore, { MemoFilter } from "@/store/v2/memoFilter";
interface Tag {
key: string;
@ -85,15 +85,14 @@ interface TagItemContainerProps {
const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => {
const { tag } = props;
const memoFilterStore = useMemoFilterStore();
const tagFilters = memoFilterStore.getFiltersByFactor("tagSearch");
const isActive = tagFilters.some((f) => f.value === tag.text);
const isActive = tagFilters.some((f: MemoFilter) => f.value === tag.text);
const hasSubTags = tag.subTags.length > 0;
const [showSubTags, toggleSubTags] = useToggle(false);
const handleTagClick = () => {
if (isActive) {
memoFilterStore.removeFilter((f) => f.factor === "tagSearch" && f.value === tag.text);
memoFilterStore.removeFilter((f: MemoFilter) => f.factor === "tagSearch" && f.value === tag.text);
} else {
memoFilterStore.addFilter({
factor: "tagSearch",

@ -7,8 +7,8 @@ import useCurrentUser from "@/hooks/useCurrentUser";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import Loading from "@/pages/Loading";
import { Routes } from "@/router";
import { useMemoFilterStore } from "@/store/v1";
import { workspaceStore } from "@/store/v2";
import memoFilterStore from "@/store/v2/memoFilter";
import { cn } from "@/utils";
const RootLayout = observer(() => {
@ -16,7 +16,6 @@ const RootLayout = observer(() => {
const [searchParams] = useSearchParams();
const { sm } = useResponsiveWidth();
const currentUser = useCurrentUser();
const memoFilterStore = useMemoFilterStore();
const [initialized, setInitialized] = useState(false);
const pathname = useMemo(() => location.pathname, [location.pathname]);
const prevPathname = usePrevious(pathname);

@ -3,14 +3,13 @@ import { useMemo } from "react";
import MemoView from "@/components/MemoView";
import PagedMemoList from "@/components/PagedMemoList";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoFilterStore } from "@/store/v1";
import { viewStore } from "@/store/v2";
import memoFilterStore from "@/store/v2/memoFilter";
import { Direction, State } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
const Archived = () => {
const user = useCurrentUser();
const memoFilterStore = useMemoFilterStore();
const memoListFilter = useMemo(() => {
const conditions = [];

@ -3,14 +3,13 @@ import { useMemo } from "react";
import MemoView from "@/components/MemoView";
import PagedMemoList from "@/components/PagedMemoList";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoFilterStore } from "@/store/v1";
import { viewStore } from "@/store/v2";
import memoFilterStore from "@/store/v2/memoFilter";
import { Direction, State } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
const Explore = () => {
const user = useCurrentUser();
const memoFilterStore = useMemoFilterStore();
const memoListFilter = useMemo(() => {
const conditions = [];

@ -4,14 +4,13 @@ import { useMemo } from "react";
import MemoView from "@/components/MemoView";
import PagedMemoList from "@/components/PagedMemoList";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoFilterStore } from "@/store/v1";
import { viewStore, userStore } from "@/store/v2";
import memoFilterStore from "@/store/v2/memoFilter";
import { Direction, State } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
const Home = observer(() => {
const user = useCurrentUser();
const memoFilterStore = useMemoFilterStore();
const selectedShortcut = userStore.state.shortcuts.find((shortcut) => shortcut.id === memoFilterStore.shortcut);
const memoListFilter = useMemo(() => {

@ -10,8 +10,8 @@ import MemoView from "@/components/MemoView";
import PagedMemoList from "@/components/PagedMemoList";
import UserAvatar from "@/components/UserAvatar";
import useLoading from "@/hooks/useLoading";
import { useMemoFilterStore } from "@/store/v1";
import { viewStore, userStore } from "@/store/v2";
import memoFilterStore from "@/store/v2/memoFilter";
import { Direction, State } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
import { User } from "@/types/proto/api/v1/user_service";
@ -22,7 +22,6 @@ const UserProfile = observer(() => {
const params = useParams();
const loadingState = useLoading();
const [user, setUser] = useState<User>();
const memoFilterStore = useMemoFilterStore();
useEffect(() => {
const username = params.username;

@ -1 +0,0 @@
export * from "./memoFilter";

@ -1,64 +0,0 @@
import { uniqBy } from "lodash-es";
import { create } from "zustand";
import { combine } from "zustand/middleware";
export type FilterFactor =
| "tagSearch"
| "visibility"
| "contentSearch"
| "displayTime"
| "pinned"
| "property.hasLink"
| "property.hasTaskList"
| "property.hasCode";
export interface MemoFilter {
factor: FilterFactor;
value: string;
}
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[];
// The id of selected shortcut.
shortcut?: string;
}
const getInitialState = (): State => {
const searchParams = new URLSearchParams(window.location.search);
return {
filters: parseFilterQuery(searchParams.get("filter")),
};
};
export const useMemoFilterStore = create(
combine(getInitialState(), (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)) })),
setShortcut: (shortcut?: string) => set({ shortcut }),
})),
);

@ -1,7 +1,8 @@
import memoStore from "./memo";
import memoFilterStore from "./memoFilter";
import resourceStore from "./resource";
import userStore from "./user";
import viewStore from "./view";
import workspaceStore from "./workspace";
export { memoStore, resourceStore, workspaceStore, userStore, viewStore };
export { memoFilterStore, memoStore, resourceStore, workspaceStore, userStore, viewStore };

@ -0,0 +1,93 @@
import { uniqBy } from "lodash-es";
import { makeAutoObservable } from "mobx";
export type FilterFactor =
| "tagSearch"
| "visibility"
| "contentSearch"
| "displayTime"
| "pinned"
| "property.hasLink"
| "property.hasTaskList"
| "property.hasCode";
export interface MemoFilter {
factor: FilterFactor;
value: string;
}
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(",");
};
class MemoFilterState {
filters: MemoFilter[] = [];
shortcut?: string = undefined;
constructor() {
makeAutoObservable(this);
this.init();
}
init() {
const searchParams = new URLSearchParams(window.location.search);
this.filters = parseFilterQuery(searchParams.get("filter"));
}
setState(state: Partial<MemoFilterState>) {
Object.assign(this, state);
}
getFiltersByFactor(factor: FilterFactor) {
return this.filters.filter((f) => f.factor === factor);
}
addFilter(filter: MemoFilter) {
this.filters = uniqBy([...this.filters, filter], getMemoFilterKey);
}
removeFilter(filterFn: (f: MemoFilter) => boolean) {
this.filters = this.filters.filter((f) => !filterFn(f));
}
setShortcut(shortcut?: string) {
this.shortcut = shortcut;
}
}
const memoFilterStore = (() => {
const state = new MemoFilterState();
return {
get filters() {
return state.filters;
},
get shortcut() {
return state.shortcut;
},
getFiltersByFactor: (factor: FilterFactor) => state.getFiltersByFactor(factor),
addFilter: (filter: MemoFilter) => state.addFilter(filter),
removeFilter: (filterFn: (f: MemoFilter) => boolean) => state.removeFilter(filterFn),
setShortcut: (shortcut?: string) => state.setShortcut(shortcut),
};
})();
export default memoFilterStore;
Loading…
Cancel
Save