From cd38ec93ed41166d791a1def02a125434ca3f558 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 26 Jul 2024 00:46:48 +0800 Subject: [PATCH] feat: implement memo filters --- web/src/components/MemoContent/Tag.tsx | 15 +++++--- web/src/components/MemoFilters.tsx | 40 ++++++++++++++++++++ web/src/components/RenameTagDialog.tsx | 3 -- web/src/components/SearchBar.tsx | 38 +++++++++---------- web/src/components/TagTree.tsx | 23 ++++++------ web/src/components/UserStatisticsView.tsx | 15 +++----- web/src/hooks/index.ts | 1 - web/src/hooks/useFilterWithUrlParams.ts | 45 ----------------------- web/src/pages/Archived.tsx | 20 +++++----- web/src/pages/Explore.tsx | 20 +++++----- web/src/pages/Home.tsx | 37 +++++++++---------- web/src/pages/UserProfile.tsx | 20 +++++----- web/src/store/v1/index.ts | 1 + web/src/store/v1/memoFilter.ts | 40 ++++++++++++++++++++ 14 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 web/src/components/MemoFilters.tsx delete mode 100644 web/src/hooks/useFilterWithUrlParams.ts create mode 100644 web/src/store/v1/memoFilter.ts diff --git a/web/src/components/MemoContent/Tag.tsx b/web/src/components/MemoContent/Tag.tsx index 26066b2b..fa5e50fd 100644 --- a/web/src/components/MemoContent/Tag.tsx +++ b/web/src/components/MemoContent/Tag.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { useContext } from "react"; -import { useFilterStore } from "@/store/module"; +import { useMemoFilterStore } from "@/store/v1"; import { RendererContext } from "./types"; interface Props { @@ -9,18 +9,21 @@ interface Props { const Tag: React.FC = ({ content }: Props) => { const context = useContext(RendererContext); - const filterStore = useFilterStore(); + const memoFilterStore = useMemoFilterStore(); const handleTagClick = () => { if (context.disableFilter) { return; } - const currTagQuery = filterStore.getState().tag; - if (currTagQuery === content) { - filterStore.setTagFilter(undefined); + const isActive = memoFilterStore.getFiltersByFactor("tag").some((filter) => filter.value === content); + if (isActive) { + memoFilterStore.removeFilter((f) => f.factor === "tag" && f.value === content); } else { - filterStore.setTagFilter(content); + memoFilterStore.addFilter({ + factor: "tag", + value: content, + }); } }; diff --git a/web/src/components/MemoFilters.tsx b/web/src/components/MemoFilters.tsx new file mode 100644 index 00000000..106e63f7 --- /dev/null +++ b/web/src/components/MemoFilters.tsx @@ -0,0 +1,40 @@ +import { isEqual } from "lodash-es"; +import { useMemoFilterStore } from "@/store/v1"; +import Icon from "./Icon"; + +const MemoFilters = () => { + const memoFilterStore = useMemoFilterStore(); + const filters = memoFilterStore.filters; + + if (filters.length === 0) { + return undefined; + } + + return ( +
+ + + Filters + +
+ {filters.map((filter) => ( +
+ {filter.factor} + {filter.value && {filter.value}} + +
+ ))} +
+
+ ); +}; + +export default MemoFilters; diff --git a/web/src/components/RenameTagDialog.tsx b/web/src/components/RenameTagDialog.tsx index 6ec8cd4f..ce41443f 100644 --- a/web/src/components/RenameTagDialog.tsx +++ b/web/src/components/RenameTagDialog.tsx @@ -4,7 +4,6 @@ import { toast } from "react-hot-toast"; import { memoServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; import useLoading from "@/hooks/useLoading"; -import { useFilterStore } from "@/store/module"; import { useTagStore } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; import { generateDialog } from "./Dialog"; @@ -18,7 +17,6 @@ const RenameTagDialog: React.FC = (props: Props) => { const { tag, destroy } = props; const t = useTranslate(); const tagStore = useTagStore(); - const filterStore = useFilterStore(); const [newName, setNewName] = useState(tag); const requestState = useLoading(false); const user = useCurrentUser(); @@ -44,7 +42,6 @@ const RenameTagDialog: React.FC = (props: Props) => { newTag: newName, }); toast.success("Rename tag successfully"); - filterStore.setTagFilter(newName); tagStore.fetchTags({ user }, { skipCache: true }); } catch (error: any) { console.error(error); diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index b18d3f34..19321580 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -1,31 +1,30 @@ -import { useEffect, useState } from "react"; -import useDebounce from "react-use/lib/useDebounce"; -import { useFilterStore } from "@/store/module"; +import { useState } from "react"; +import { useMemoFilterStore } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; import Icon from "./Icon"; const SearchBar = () => { const t = useTranslate(); - const filterStore = useFilterStore(); + const memoFilterStore = useMemoFilterStore(); const [queryText, setQueryText] = useState(""); - useEffect(() => { - const text = filterStore.getState().text; - setQueryText(text === undefined ? "" : text); - }, [filterStore.state.text]); - - useDebounce( - () => { - filterStore.setTextFilter(queryText.length === 0 ? undefined : queryText); - }, - 1000, - [queryText], - ); - - const handleTextQueryInput = (event: React.FormEvent) => { + const onTextChange = (event: React.FormEvent) => { setQueryText(event.currentTarget.value); }; + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (queryText !== "") { + memoFilterStore.removeFilter((f) => f.factor === "contentSearch"); + memoFilterStore.addFilter({ + factor: "contentSearch", + value: queryText, + }); + } + } + }; + return (
@@ -33,7 +32,8 @@ const SearchBar = () => { className="w-full text-gray-500 dark:text-gray-400 bg-zinc-50 dark:bg-zinc-900 border dark:border-zinc-800 text-sm leading-7 rounded-lg p-1 pl-8 outline-none" placeholder={t("memo.search-placeholder")} value={queryText} - onChange={handleTextQueryInput} + onChange={onTextChange} + onKeyDown={onKeyDown} />
); diff --git a/web/src/components/TagTree.tsx b/web/src/components/TagTree.tsx index d21abe4d..573d20f0 100644 --- a/web/src/components/TagTree.tsx +++ b/web/src/components/TagTree.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import useToggle from "react-use/lib/useToggle"; -import { useFilterStore } from "@/store/module"; +import { useMemoFilterStore } from "@/store/v1"; import Icon from "./Icon"; interface Tag { @@ -14,8 +14,6 @@ interface Props { } const TagTree = ({ tags: rawTags }: Props) => { - const filterStore = useFilterStore(); - const filter = filterStore.state; const [tags, setTags] = useState([]); useEffect(() => { @@ -67,7 +65,7 @@ const TagTree = ({ tags: rawTags }: Props) => { return (
{tags.map((t, idx) => ( - + ))}
); @@ -75,21 +73,24 @@ const TagTree = ({ tags: rawTags }: Props) => { interface TagItemContainerProps { tag: Tag; - tagQuery?: string; } const TagItemContainer: React.FC = (props: TagItemContainerProps) => { - const filterStore = useFilterStore(); - const { tag, tagQuery } = props; - const isActive = tagQuery === tag.text; + const { tag } = props; + const memoFilterStore = useMemoFilterStore(); + const tagFilters = memoFilterStore.getFiltersByFactor("tag"); + const isActive = tagFilters.some((f) => f.value === tag.text); const hasSubTags = tag.subTags.length > 0; const [showSubTags, toggleSubTags] = useToggle(false); const handleTagClick = () => { if (isActive) { - filterStore.setTagFilter(undefined); + memoFilterStore.removeFilter((f) => f.factor === "tag" && f.value === tag.text); } else { - filterStore.setTagFilter(tag.text); + memoFilterStore.addFilter({ + factor: "tag", + value: tag.text, + }); } }; @@ -131,7 +132,7 @@ const TagItemContainer: React.FC = (props: TagItemContain }`} > {tag.subTags.map((st, idx) => ( - + ))} ) : null} diff --git a/web/src/components/UserStatisticsView.tsx b/web/src/components/UserStatisticsView.tsx index 9e707b9c..55fa3076 100644 --- a/web/src/components/UserStatisticsView.tsx +++ b/web/src/components/UserStatisticsView.tsx @@ -7,8 +7,7 @@ import { memoServiceClient } from "@/grpcweb"; import useAsyncEffect from "@/hooks/useAsyncEffect"; import useCurrentUser from "@/hooks/useCurrentUser"; import i18n from "@/i18n"; -import { useFilterStore } from "@/store/module"; -import { useMemoStore } from "@/store/v1"; +import { useMemoFilterStore, useMemoStore } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; import ActivityCalendar from "./ActivityCalendar"; import Icon from "./Icon"; @@ -25,14 +24,13 @@ const UserStatisticsView = () => { const t = useTranslate(); const currentUser = useCurrentUser(); const memoStore = useMemoStore(); - const filterStore = useFilterStore(); + const memoFilterStore = useMemoFilterStore(); const [memoAmount, setMemoAmount] = useState(0); const [memoStats, setMemoStats] = useState({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 }); const [activityStats, setActivityStats] = useState>({}); const [selectedDate] = useState(new Date()); const [monthString, setMonthString] = useState(dayjs(selectedDate.toDateString()).format("YYYY-MM")); const days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000); - const filter = filterStore.state; useAsyncEffect(async () => { const { properties } = await memoServiceClient.listMemoProperties({ @@ -120,9 +118,8 @@ const UserStatisticsView = () => {
filterStore.setMemoPropertyFilter({ hasLink: !filter.memoPropertyFilter?.hasLink })} + onClick={() => memoFilterStore.addFilter({ factor: "property.hasLink", value: "" })} >
@@ -133,9 +130,8 @@ const UserStatisticsView = () => {
filterStore.setMemoPropertyFilter({ hasTaskList: !filter.memoPropertyFilter?.hasTaskList })} + onClick={() => memoFilterStore.addFilter({ factor: "property.hasTaskList", value: "" })} >
{memoStats.incompleteTasks > 0 ? ( @@ -160,9 +156,8 @@ const UserStatisticsView = () => {
filterStore.setMemoPropertyFilter({ hasCode: !filter.memoPropertyFilter?.hasCode })} + onClick={() => memoFilterStore.addFilter({ factor: "property.hasCode", value: "" })} >
diff --git a/web/src/hooks/index.ts b/web/src/hooks/index.ts index 881f9458..eec8f214 100644 --- a/web/src/hooks/index.ts +++ b/web/src/hooks/index.ts @@ -2,5 +2,4 @@ export * from "./useLoading"; export * from "./useCurrentUser"; export * from "./useNavigateTo"; export * from "./useAsyncEffect"; -export * from "./useFilterWithUrlParams"; export * from "./useResponsiveWidth"; diff --git a/web/src/hooks/useFilterWithUrlParams.ts b/web/src/hooks/useFilterWithUrlParams.ts deleted file mode 100644 index 6b907c4c..00000000 --- a/web/src/hooks/useFilterWithUrlParams.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useEffect } from "react"; -import { useLocation } from "react-router-dom"; -import { useFilterStore } from "@/store/module"; - -const useFilterWithUrlParams = () => { - const location = useLocation(); - const filterStore = useFilterStore(); - const { tag, text, memoPropertyFilter } = filterStore.state; - - useEffect(() => { - const urlParams = new URLSearchParams(location.search); - const tag = urlParams.get("tag"); - const text = urlParams.get("text"); - if (tag) { - filterStore.setTagFilter(tag); - } - if (text) { - filterStore.setTextFilter(text); - } - }, []); - - useEffect(() => { - const urlParams = new URLSearchParams(location.search); - if (tag) { - urlParams.set("tag", tag); - } else { - urlParams.delete("tag"); - } - if (text) { - urlParams.set("text", text); - } else { - urlParams.delete("text"); - } - const params = urlParams.toString(); - window.history.replaceState({}, "", `${location.pathname}${params?.length > 0 ? `?${params}` : ""}`); - }, [tag, text]); - - return { - tag, - text, - memoPropertyFilter, - }; -}; - -export default useFilterWithUrlParams; diff --git a/web/src/pages/Archived.tsx b/web/src/pages/Archived.tsx index c36d358b..417d1937 100644 --- a/web/src/pages/Archived.tsx +++ b/web/src/pages/Archived.tsx @@ -5,13 +5,13 @@ import toast from "react-hot-toast"; import Empty from "@/components/Empty"; import Icon from "@/components/Icon"; import MemoContent from "@/components/MemoContent"; +import MemoFilters from "@/components/MemoFilters"; import MobileHeader from "@/components/MobileHeader"; import SearchBar from "@/components/SearchBar"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import { getTimeStampByDate } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; -import useFilterWithUrlParams from "@/hooks/useFilterWithUrlParams"; -import { useMemoList, useMemoStore } from "@/store/v1"; +import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1"; import { RowStatus } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; @@ -21,9 +21,9 @@ const Archived = () => { const user = useCurrentUser(); const memoStore = useMemoStore(); const memoList = useMemoList(); + const memoFilterStore = useMemoFilterStore(); const [isRequesting, setIsRequesting] = useState(true); const [nextPageToken, setNextPageToken] = useState(""); - const { tag: tagQuery, text: textQuery } = useFilterWithUrlParams(); const sortedMemos = memoList.value .filter((memo) => memo.rowStatus === RowStatus.ARCHIVED) .sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); @@ -31,21 +31,22 @@ const Archived = () => { useEffect(() => { memoList.reset(); fetchMemos(""); - }, [tagQuery, textQuery]); + }, [memoFilterStore.filters]); const fetchMemos = async (nextPageToken: string) => { setIsRequesting(true); const filters = [`creator == "${user.name}"`, `row_status == "ARCHIVED"`]; const contentSearch: string[] = []; - if (textQuery) { - contentSearch.push(JSON.stringify(textQuery)); + for (const filter of memoFilterStore.filters) { + if (filter.factor === "contentSearch") { + contentSearch.push(`"${filter.value}"`); + } else if (filter.factor === "tag") { + filters.push(`tag == "${filter.value}"`); + } } if (contentSearch.length > 0) { filters.push(`content_search == [${contentSearch.join(", ")}]`); } - if (tagQuery) { - filters.push(`tag == "${tagQuery}"`); - } const response = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), @@ -92,6 +93,7 @@ const Archived = () => {
+ {sortedMemos.map((memo) => (
{ @@ -20,29 +20,30 @@ const Explore = () => { const user = useCurrentUser(); const memoStore = useMemoStore(); const memoList = useMemoList(); + const memoFilterStore = useMemoFilterStore(); const [isRequesting, setIsRequesting] = useState(true); const [nextPageToken, setNextPageToken] = useState(""); - const { tag: tagQuery, text: textQuery } = useFilterWithUrlParams(); const sortedMemos = memoList.value.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)); useEffect(() => { memoList.reset(); fetchMemos(""); - }, [tagQuery, textQuery]); + }, [memoFilterStore.filters]); const fetchMemos = async (nextPageToken: string) => { setIsRequesting(true); const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`]; const contentSearch: string[] = []; - if (textQuery) { - contentSearch.push(JSON.stringify(textQuery)); + for (const filter of memoFilterStore.filters) { + if (filter.factor === "contentSearch") { + contentSearch.push(`"${filter.value}"`); + } else if (filter.factor === "tag") { + filters.push(`tag == "${filter.value}"`); + } } if (contentSearch.length > 0) { filters.push(`content_search == [${contentSearch.join(", ")}]`); } - if (tagQuery) { - filters.push(`tag == "${tagQuery}"`); - } const response = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), @@ -61,6 +62,7 @@ const Explore = () => { )}
+
{sortedMemos.map((memo) => ( diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index a97ff4f7..a8253c2f 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -5,14 +5,14 @@ import Empty from "@/components/Empty"; import { HomeSidebar, HomeSidebarDrawer } from "@/components/HomeSidebar"; import Icon from "@/components/Icon"; import MemoEditor from "@/components/MemoEditor"; +import MemoFilters from "@/components/MemoFilters"; import MemoView from "@/components/MemoView"; import MobileHeader from "@/components/MobileHeader"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import { getTimeStampByDate } from "@/helpers/datetime"; import useCurrentUser from "@/hooks/useCurrentUser"; -import useFilterWithUrlParams from "@/hooks/useFilterWithUrlParams"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; -import { useMemoList, useMemoStore } from "@/store/v1"; +import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1"; import { RowStatus } from "@/types/proto/api/v1/common"; import { useTranslate } from "@/utils/i18n"; @@ -22,9 +22,9 @@ const Home = () => { const user = useCurrentUser(); const memoStore = useMemoStore(); const memoList = useMemoList(); + const memoFilterStore = useMemoFilterStore(); const [isRequesting, setIsRequesting] = useState(true); const [nextPageToken, setNextPageToken] = useState(""); - const filter = useFilterWithUrlParams(); const sortedMemos = memoList.value .filter((memo) => memo.rowStatus === RowStatus.ACTIVE) .sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)) @@ -33,32 +33,28 @@ const Home = () => { useEffect(() => { memoList.reset(); fetchMemos(""); - }, [filter.tag, filter.text, filter.memoPropertyFilter]); + }, [memoFilterStore.filters]); const fetchMemos = async (nextPageToken: string) => { setIsRequesting(true); const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`]; const contentSearch: string[] = []; - if (filter.text) { - contentSearch.push(JSON.stringify(filter.text)); + for (const filter of memoFilterStore.filters) { + if (filter.factor === "contentSearch") { + contentSearch.push(`"${filter.value}"`); + } else if (filter.factor === "tag") { + filters.push(`tag == "${filter.value}"`); + } else if (filter.factor === "property.hasLink") { + filters.push(`has_link == true`); + } else if (filter.factor === "property.hasTaskList") { + filters.push(`has_task_list == true`); + } else if (filter.factor === "property.hasCode") { + filters.push(`has_code == true`); + } } if (contentSearch.length > 0) { filters.push(`content_search == [${contentSearch.join(", ")}]`); } - if (filter.tag) { - filters.push(`tag == "${filter.tag}"`); - } - if (filter.memoPropertyFilter) { - if (filter.memoPropertyFilter.hasLink) { - filters.push(`has_link == true`); - } - if (filter.memoPropertyFilter.hasTaskList) { - filters.push(`has_task_list == true`); - } - if (filter.memoPropertyFilter.hasCode) { - filters.push(`has_code == true`); - } - } const response = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), @@ -78,6 +74,7 @@ const Home = () => {
+
{sortedMemos.map((memo) => ( diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index 564ed782..e8a4ce41 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -5,14 +5,14 @@ import { toast } from "react-hot-toast"; import { useParams } from "react-router-dom"; import Empty from "@/components/Empty"; import Icon from "@/components/Icon"; +import MemoFilters from "@/components/MemoFilters"; import MemoView from "@/components/MemoView"; import MobileHeader from "@/components/MobileHeader"; import UserAvatar from "@/components/UserAvatar"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import { getTimeStampByDate } from "@/helpers/datetime"; -import useFilterWithUrlParams from "@/hooks/useFilterWithUrlParams"; import useLoading from "@/hooks/useLoading"; -import { useMemoList, useMemoStore, useUserStore } from "@/store/v1"; +import { useMemoFilterStore, useMemoList, useMemoStore, useUserStore } from "@/store/v1"; import { User } from "@/types/proto/api/v1/user_service"; import { useTranslate } from "@/utils/i18n"; @@ -24,9 +24,9 @@ const UserProfile = () => { const [user, setUser] = useState(); const memoStore = useMemoStore(); const memoList = useMemoList(); + const memoFilterStore = useMemoFilterStore(); const [isRequesting, setIsRequesting] = useState(true); const [nextPageToken, setNextPageToken] = useState(""); - const { tag: tagQuery, text: textQuery } = useFilterWithUrlParams(); const sortedMemos = memoList.value .sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime)) .sort((a, b) => Number(b.pinned) - Number(a.pinned)); @@ -60,7 +60,7 @@ const UserProfile = () => { memoList.reset(); fetchMemos(""); - }, [user, tagQuery, textQuery]); + }, [user, memoFilterStore.filters]); const fetchMemos = async (nextPageToken: string) => { if (!user) { @@ -70,15 +70,16 @@ const UserProfile = () => { setIsRequesting(true); const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`]; const contentSearch: string[] = []; - if (textQuery) { - contentSearch.push(JSON.stringify(textQuery)); + for (const filter of memoFilterStore.filters) { + if (filter.factor === "contentSearch") { + contentSearch.push(`"${filter.value}"`); + } else if (filter.factor === "tag") { + filters.push(`tag == "${filter.value}"`); + } } if (contentSearch.length > 0) { filters.push(`content_search == [${contentSearch.join(", ")}]`); } - if (tagQuery) { - filters.push(`tag == "${tagQuery}"`); - } const response = await memoStore.fetchMemos({ pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, filter: filters.join(" && "), @@ -125,6 +126,7 @@ const UserProfile = () => {

+ {sortedMemos.map((memo) => ( ))} diff --git a/web/src/store/v1/index.ts b/web/src/store/v1/index.ts index 3b6a4707..58215f43 100644 --- a/web/src/store/v1/index.ts +++ b/web/src/store/v1/index.ts @@ -5,3 +5,4 @@ export * from "./resourceName"; export * from "./resource"; export * from "./workspaceSetting"; export * from "./tag"; +export * from "./memoFilter"; diff --git a/web/src/store/v1/memoFilter.ts b/web/src/store/v1/memoFilter.ts new file mode 100644 index 00000000..1d1d4f38 --- /dev/null +++ b/web/src/store/v1/memoFilter.ts @@ -0,0 +1,40 @@ +import { uniq } from "lodash-es"; +import { create } from "zustand"; +import { combine, persist } from "zustand/middleware"; + +type FilterFactor = + | "tag" + | "visibility" + | "contentSearch" + | "displayTime" + | "property.hasLink" + | "property.hasTaskList" + | "property.hasCode"; + +export interface MemoFilter { + factor: FilterFactor; + value: string; +} + +interface State { + filters: MemoFilter[]; +} + +const getDefaultState = (): State => ({ + filters: [], +}); + +export const useMemoFilterStore = create( + persist( + combine(getDefaultState(), (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: uniq([...state.filters, filter]) })), + removeFilter: (filterFn: (f: MemoFilter) => boolean) => set((state) => ({ filters: state.filters.filter((f) => !filterFn(f)) })), + })), + { + name: "memo-filter", + }, + ), +);