mirror of
https://github.com/usememos/memos.git
synced 2025-02-21 05:40:57 +01:00
feat: implement memo filters
This commit is contained in:
parent
b3b4aa9ddb
commit
cd38ec93ed
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
40
web/src/components/MemoFilters.tsx
Normal file
40
web/src/components/MemoFilters.tsx
Normal 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;
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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" />
|
||||
|
@ -2,5 +2,4 @@ export * from "./useLoading";
|
||||
export * from "./useCurrentUser";
|
||||
export * from "./useNavigateTo";
|
||||
export * from "./useAsyncEffect";
|
||||
export * from "./useFilterWithUrlParams";
|
||||
export * from "./useResponsiveWidth";
|
||||
|
@ -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;
|
@ -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}
|
||||
|
@ -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 />
|
||||
|
@ -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 />
|
||||
|
@ -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 />
|
||||
))}
|
||||
|
@ -5,3 +5,4 @@ export * from "./resourceName";
|
||||
export * from "./resource";
|
||||
export * from "./workspaceSetting";
|
||||
export * from "./tag";
|
||||
export * from "./memoFilter";
|
||||
|
40
web/src/store/v1/memoFilter.ts
Normal file
40
web/src/store/v1/memoFilter.ts
Normal 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",
|
||||
},
|
||||
),
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user