feat: implement memo filters

This commit is contained in:
Steven 2024-07-26 00:46:48 +08:00
parent b3b4aa9ddb
commit cd38ec93ed
14 changed files with 176 additions and 142 deletions

View File

@ -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<Props> = ({ 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,
});
}
};

View File

@ -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 (
<div className="w-full mb-2 flex flex-row justify-start items-start gap-2">
<span className="flex flex-row items-center gap-0.5 text-gray-500 text-sm leading-6">
<Icon.Filter className="w-4 h-auto opacity-60 inline" />
Filters
</span>
<div className="flex flex-row justify-start items-center flex-wrap gap-2 leading-6">
{filters.map((filter) => (
<div
key={filter.factor}
className="flex flex-row items-center gap-1 bg-gray-100 dark:bg-zinc-800 border dark:border-zinc-700 px-1 rounded-md"
>
<span className="text-gray-600 dark:text-gray-500 text-sm">{filter.factor}</span>
{filter.value && <span className="text-gray-500 dark:text-gray-400 text-sm max-w-12 truncate">{filter.value}</span>}
<button
onClick={() => memoFilterStore.removeFilter((f) => isEqual(f, filter))}
className="text-gray-500 dark:text-gray-300 opacity-60 hover:opacity-100"
>
<Icon.X className="w-3 h-auto" />
</button>
</div>
))}
</div>
</div>
);
};
export default MemoFilters;

View File

@ -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: 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: Props) => {
newTag: newName,
});
toast.success("Rename tag successfully");
filterStore.setTagFilter(newName);
tagStore.fetchTags({ user }, { skipCache: true });
} catch (error: any) {
console.error(error);

View File

@ -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<HTMLInputElement>) => {
const onTextChange = (event: React.FormEvent<HTMLInputElement>) => {
setQueryText(event.currentTarget.value);
};
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
if (queryText !== "") {
memoFilterStore.removeFilter((f) => f.factor === "contentSearch");
memoFilterStore.addFilter({
factor: "contentSearch",
value: queryText,
});
}
}
};
return (
<div className="relative w-full h-auto flex flex-row justify-start items-center">
<Icon.Search className="absolute left-3 w-4 h-auto opacity-30" />
@ -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}
/>
</div>
);

View File

@ -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<Tag[]>([]);
useEffect(() => {
@ -67,7 +65,7 @@ const TagTree = ({ tags: rawTags }: Props) => {
return (
<div className="flex flex-col justify-start items-start relative w-full h-auto flex-nowrap gap-2 mt-1">
{tags.map((t, idx) => (
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={filter.tag} />
<TagItemContainer key={t.text + "-" + idx} tag={t} />
))}
</div>
);
@ -75,21 +73,24 @@ const TagTree = ({ tags: rawTags }: Props) => {
interface TagItemContainerProps {
tag: Tag;
tagQuery?: string;
}
const TagItemContainer: React.FC<TagItemContainerProps> = (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<TagItemContainerProps> = (props: TagItemContain
}`}
>
{tag.subTags.map((st, idx) => (
<TagItemContainer key={st.text + "-" + idx} tag={st} tagQuery={tagQuery} />
<TagItemContainer key={st.text + "-" + idx} tag={st} />
))}
</div>
) : null}

View File

@ -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<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 });
const [activityStats, setActivityStats] = useState<Record<string, number>>({});
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 = () => {
<div
className={clsx(
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
filter.memoPropertyFilter?.hasLink ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
)}
onClick={() => filterStore.setMemoPropertyFilter({ hasLink: !filter.memoPropertyFilter?.hasLink })}
onClick={() => memoFilterStore.addFilter({ factor: "property.hasLink", value: "" })}
>
<div className="w-auto flex justify-start items-center mr-1">
<Icon.Link className="w-4 h-auto mr-1" />
@ -133,9 +130,8 @@ const UserStatisticsView = () => {
<div
className={clsx(
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
filter.memoPropertyFilter?.hasTaskList ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
)}
onClick={() => filterStore.setMemoPropertyFilter({ hasTaskList: !filter.memoPropertyFilter?.hasTaskList })}
onClick={() => memoFilterStore.addFilter({ factor: "property.hasTaskList", value: "" })}
>
<div className="w-auto flex justify-start items-center mr-1">
{memoStats.incompleteTasks > 0 ? (
@ -160,9 +156,8 @@ const UserStatisticsView = () => {
<div
className={clsx(
"w-auto border dark:border-zinc-800 pl-1 pr-1.5 rounded-md flex justify-between items-center cursor-pointer hover:shadow",
filter.memoPropertyFilter?.hasCode ? "bg-blue-50 dark:bg-blue-900 shadow" : "",
)}
onClick={() => filterStore.setMemoPropertyFilter({ hasCode: !filter.memoPropertyFilter?.hasCode })}
onClick={() => memoFilterStore.addFilter({ factor: "property.hasCode", value: "" })}
>
<div className="w-auto flex justify-start items-center mr-1">
<Icon.Code2 className="w-4 h-auto mr-1" />

View File

@ -2,5 +2,4 @@ export * from "./useLoading";
export * from "./useCurrentUser";
export * from "./useNavigateTo";
export * from "./useAsyncEffect";
export * from "./useFilterWithUrlParams";
export * from "./useResponsiveWidth";

View File

@ -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;

View File

@ -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<string>("");
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 = () => {
<SearchBar />
</div>
</div>
<MemoFilters />
{sortedMemos.map((memo) => (
<div
key={memo.name}

View File

@ -4,14 +4,14 @@ import { useEffect, useState } from "react";
import Empty from "@/components/Empty";
import { ExploreSidebar, ExploreSidebarDrawer } from "@/components/ExploreSidebar";
import Icon from "@/components/Icon";
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 { useTranslate } from "@/utils/i18n";
const Explore = () => {
@ -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<string>("");
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 = () => {
)}
<div className={clsx("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
<div className={clsx(md ? "w-[calc(100%-15rem)]" : "w-full")}>
<MemoFilters />
<div className="flex flex-col justify-start items-start w-full max-w-full">
{sortedMemos.map((memo) => (
<MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility showPinned compact />

View File

@ -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<string>("");
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 = () => {
<div className={clsx("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
<div className={clsx(md ? "w-[calc(100%-15rem)]" : "w-full")}>
<MemoEditor className="mb-2" cacheKey="home-memo-editor" />
<MemoFilters />
<div className="flex flex-col justify-start items-start w-full max-w-full">
{sortedMemos.map((memo) => (
<MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility showPinned compact />

View File

@ -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<User>();
const memoStore = useMemoStore();
const memoList = useMemoList();
const memoFilterStore = useMemoFilterStore();
const [isRequesting, setIsRequesting] = useState(true);
const [nextPageToken, setNextPageToken] = useState<string>("");
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 = () => {
</p>
</div>
</div>
<MemoFilters />
{sortedMemos.map((memo) => (
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />
))}

View File

@ -5,3 +5,4 @@ export * from "./resourceName";
export * from "./resource";
export * from "./workspaceSetting";
export * from "./tag";
export * from "./memoFilter";

View File

@ -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",
},
),
);