mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: implement memo metadata store
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import useDebounce from "react-use/lib/useDebounce";
|
||||||
import SearchBar from "@/components/SearchBar";
|
import SearchBar from "@/components/SearchBar";
|
||||||
|
import { useMemoList, useMemoMetadataStore } from "@/store/v1";
|
||||||
import TagsSection from "../HomeSidebar/TagsSection";
|
import TagsSection from "../HomeSidebar/TagsSection";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -7,6 +10,19 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ExploreSidebar = (props: Props) => {
|
const ExploreSidebar = (props: Props) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const memoList = useMemoList();
|
||||||
|
const memoMetadataStore = useMemoMetadataStore();
|
||||||
|
|
||||||
|
useDebounce(
|
||||||
|
async () => {
|
||||||
|
if (memoList.size() === 0) return;
|
||||||
|
await memoMetadataStore.fetchMemoMetadata({ location });
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
[memoList.size(), location.pathname],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import useDebounce from "react-use/lib/useDebounce";
|
||||||
import SearchBar from "@/components/SearchBar";
|
import SearchBar from "@/components/SearchBar";
|
||||||
import UserStatisticsView from "@/components/UserStatisticsView";
|
import UserStatisticsView from "@/components/UserStatisticsView";
|
||||||
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
|
import { useMemoList, useMemoMetadataStore } from "@/store/v1";
|
||||||
import TagsSection from "./TagsSection";
|
import TagsSection from "./TagsSection";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -8,6 +12,19 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HomeSidebar = (props: Props) => {
|
const HomeSidebar = (props: Props) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const user = useCurrentUser();
|
||||||
|
const memoList = useMemoList();
|
||||||
|
const memoMetadataStore = useMemoMetadataStore();
|
||||||
|
|
||||||
|
useDebounce(
|
||||||
|
async () => {
|
||||||
|
await memoMetadataStore.fetchMemoMetadata({ user, location });
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
[memoList.size(), user, location.pathname],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@@ -3,11 +3,10 @@ import clsx from "clsx";
|
|||||||
import { Edit3Icon, HashIcon, MoreVerticalIcon, TagsIcon, TrashIcon } from "lucide-react";
|
import { Edit3Icon, HashIcon, MoreVerticalIcon, TagsIcon, TrashIcon } from "lucide-react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import useDebounce from "react-use/lib/useDebounce";
|
|
||||||
import useLocalStorage from "react-use/lib/useLocalStorage";
|
import useLocalStorage from "react-use/lib/useLocalStorage";
|
||||||
import { memoServiceClient } from "@/grpcweb";
|
import { memoServiceClient } from "@/grpcweb";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { useMemoFilterStore, useMemoList, useTagStore } from "@/store/v1";
|
import { useMemoFilterStore, useMemoMetadataStore, useMemoTagList } from "@/store/v1";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import showRenameTagDialog from "../RenameTagDialog";
|
import showRenameTagDialog from "../RenameTagDialog";
|
||||||
import TagTree from "../TagTree";
|
import TagTree from "../TagTree";
|
||||||
@@ -22,26 +21,12 @@ const TagsSection = (props: Props) => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const memoFilterStore = useMemoFilterStore();
|
const memoFilterStore = useMemoFilterStore();
|
||||||
const tagStore = useTagStore();
|
const memoMetadataStore = useMemoMetadataStore();
|
||||||
const memoList = useMemoList();
|
|
||||||
const [treeMode, setTreeMode] = useLocalStorage<boolean>("tag-view-as-tree", false);
|
const [treeMode, setTreeMode] = useLocalStorage<boolean>("tag-view-as-tree", false);
|
||||||
const tagAmounts = Object.entries(tagStore.getState().tagAmounts)
|
const tags = Object.entries(useMemoTagList())
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.sort((a, b) => b[1] - a[1]);
|
.sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
useDebounce(
|
|
||||||
() => {
|
|
||||||
if (memoList.size() === 0) return;
|
|
||||||
fetchTags();
|
|
||||||
},
|
|
||||||
300,
|
|
||||||
[memoList.size(), location.pathname],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchTags = async () => {
|
|
||||||
await tagStore.fetchTags({ user, location });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTagClick = (tag: string) => {
|
const handleTagClick = (tag: string) => {
|
||||||
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter) => filter.value === tag);
|
const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter) => filter.value === tag);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
@@ -61,7 +46,7 @@ const TagsSection = (props: Props) => {
|
|||||||
parent: "memos/-",
|
parent: "memos/-",
|
||||||
tag: tag,
|
tag: tag,
|
||||||
});
|
});
|
||||||
await tagStore.fetchTags({ location, user }, { skipCache: true });
|
await memoMetadataStore.fetchMemoMetadata({ user, location });
|
||||||
toast.success(t("message.deleted-successfully"));
|
toast.success(t("message.deleted-successfully"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -70,7 +55,7 @@ const TagsSection = (props: Props) => {
|
|||||||
<div className="flex flex-col justify-start items-start w-full mt-3 px-1 h-auto shrink-0 flex-nowrap hide-scrollbar">
|
<div className="flex flex-col justify-start items-start w-full mt-3 px-1 h-auto shrink-0 flex-nowrap hide-scrollbar">
|
||||||
<div className="flex flex-row justify-between items-center w-full gap-1 mb-1 text-sm leading-6 text-gray-400 select-none">
|
<div className="flex flex-row justify-between items-center w-full gap-1 mb-1 text-sm leading-6 text-gray-400 select-none">
|
||||||
<span>{t("common.tags")}</span>
|
<span>{t("common.tags")}</span>
|
||||||
{tagAmounts.length > 0 && (
|
{tags.length > 0 && (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<MoreVerticalIcon className="w-4 h-auto shrink-0 opacity-60" />
|
<MoreVerticalIcon className="w-4 h-auto shrink-0 opacity-60" />
|
||||||
@@ -84,12 +69,12 @@ const TagsSection = (props: Props) => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{tagAmounts.length > 0 ? (
|
{tags.length > 0 ? (
|
||||||
treeMode ? (
|
treeMode ? (
|
||||||
<TagTree tagAmounts={tagAmounts} />
|
<TagTree tagAmounts={tags} />
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1">
|
<div className="w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1">
|
||||||
{tagAmounts.map(([tag, amount]) => (
|
{tags.map(([tag, amount]) => (
|
||||||
<div
|
<div
|
||||||
key={tag}
|
key={tag}
|
||||||
className="shrink-0 w-auto max-w-full text-sm rounded-md leading-6 flex flex-row justify-start items-center select-none hover:opacity-80 text-gray-600 dark:text-gray-400 dark:border-zinc-800"
|
className="shrink-0 w-auto max-w-full text-sm rounded-md leading-6 flex flex-row justify-start items-center select-none hover:opacity-80 text-gray-600 dark:text-gray-400 dark:border-zinc-800"
|
||||||
|
@@ -3,9 +3,7 @@ import { HashIcon } from "lucide-react";
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import useClickAway from "react-use/lib/useClickAway";
|
import useClickAway from "react-use/lib/useClickAway";
|
||||||
import OverflowTip from "@/components/kit/OverflowTip";
|
import OverflowTip from "@/components/kit/OverflowTip";
|
||||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
import { useMemoTagList } from "@/store/v1";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
|
||||||
import { useTagStore } from "@/store/v1";
|
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { EditorRefActions } from "../Editor";
|
import { EditorRefActions } from "../Editor";
|
||||||
|
|
||||||
@@ -16,21 +14,12 @@ interface Props {
|
|||||||
const TagSelector = (props: Props) => {
|
const TagSelector = (props: Props) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const { editorRef } = props;
|
const { editorRef } = props;
|
||||||
const tagStore = useTagStore();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const tags = tagStore.sortedTags();
|
const tags = Object.entries(useMemoTagList())
|
||||||
const user = useCurrentUser();
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
useAsyncEffect(async () => {
|
.map(([tag]) => tag);
|
||||||
if (!open) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await tagStore.fetchTags({ user });
|
|
||||||
} catch (error) {
|
|
||||||
// do nothing.
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
useClickAway(containerRef, () => {
|
useClickAway(containerRef, () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
@@ -3,7 +3,7 @@ import Fuse from "fuse.js";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import getCaretCoordinates from "textarea-caret";
|
import getCaretCoordinates from "textarea-caret";
|
||||||
import OverflowTip from "@/components/kit/OverflowTip";
|
import OverflowTip from "@/components/kit/OverflowTip";
|
||||||
import { useTagStore } from "@/store/v1";
|
import { useMemoTagList } from "@/store/v1";
|
||||||
import { EditorRefActions } from ".";
|
import { EditorRefActions } from ".";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,12 +15,13 @@ type Position = { left: number; top: number; height: number };
|
|||||||
|
|
||||||
const TagSuggestions = ({ editorRef, editorActions }: Props) => {
|
const TagSuggestions = ({ editorRef, editorActions }: Props) => {
|
||||||
const [position, setPosition] = useState<Position | null>(null);
|
const [position, setPosition] = useState<Position | null>(null);
|
||||||
const tagStore = useTagStore();
|
|
||||||
const tags = tagStore.sortedTags();
|
|
||||||
|
|
||||||
const [selected, select] = useState(0);
|
const [selected, select] = useState(0);
|
||||||
const selectedRef = useRef(selected);
|
const selectedRef = useRef(selected);
|
||||||
selectedRef.current = selected;
|
selectedRef.current = selected;
|
||||||
|
const tags = Object.entries(useMemoTagList())
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([tag]) => tag);
|
||||||
|
|
||||||
const hide = () => setPosition(null);
|
const hide = () => setPosition(null);
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import { toast } from "react-hot-toast";
|
|||||||
import { memoServiceClient } from "@/grpcweb";
|
import { memoServiceClient } from "@/grpcweb";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import { useTagStore } from "@/store/v1";
|
import { useMemoMetadataStore } from "@/store/v1";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ interface Props extends DialogProps {
|
|||||||
const RenameTagDialog: React.FC<Props> = (props: Props) => {
|
const RenameTagDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { tag, destroy } = props;
|
const { tag, destroy } = props;
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const tagStore = useTagStore();
|
const memoMetadataStore = useMemoMetadataStore();
|
||||||
const [newName, setNewName] = useState(tag);
|
const [newName, setNewName] = useState(tag);
|
||||||
const requestState = useLoading(false);
|
const requestState = useLoading(false);
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
@@ -42,7 +42,7 @@ const RenameTagDialog: React.FC<Props> = (props: Props) => {
|
|||||||
newTag: newName,
|
newTag: newName,
|
||||||
});
|
});
|
||||||
toast.success("Rename tag successfully");
|
toast.success("Rename tag successfully");
|
||||||
tagStore.fetchTags({ user }, { skipCache: true });
|
memoMetadataStore.fetchMemoMetadata({ user });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(error.details);
|
toast.error(error.details);
|
||||||
|
@@ -19,15 +19,11 @@ import { memoServiceClient } from "@/grpcweb";
|
|||||||
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
import useAsyncEffect from "@/hooks/useAsyncEffect";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1";
|
import { useMemoFilterStore, useMemoMetadataStore } from "@/store/v1";
|
||||||
import { MemoView } from "@/types/proto/api/v1/memo_service";
|
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import ActivityCalendar from "./ActivityCalendar";
|
import ActivityCalendar from "./ActivityCalendar";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/Popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/Popover";
|
||||||
|
|
||||||
// Set the maximum number of memos to fetch.
|
|
||||||
const DEFAULT_MEMO_PAGE_SIZE = 1000000;
|
|
||||||
|
|
||||||
interface UserMemoStats {
|
interface UserMemoStats {
|
||||||
link: number;
|
link: number;
|
||||||
taskList: number;
|
taskList: number;
|
||||||
@@ -38,9 +34,9 @@ interface UserMemoStats {
|
|||||||
const UserStatisticsView = () => {
|
const UserStatisticsView = () => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const memoStore = useMemoStore();
|
|
||||||
const memoList = useMemoList();
|
|
||||||
const memoFilterStore = useMemoFilterStore();
|
const memoFilterStore = useMemoFilterStore();
|
||||||
|
const memoMetadataStore = useMemoMetadataStore();
|
||||||
|
const metadataList = Object.values(memoMetadataStore.getState().dataMapByName);
|
||||||
const [memoAmount, setMemoAmount] = useState(0);
|
const [memoAmount, setMemoAmount] = useState(0);
|
||||||
const [memoStats, setMemoStats] = useState<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 });
|
const [memoStats, setMemoStats] = useState<UserMemoStats>({ link: 0, taskList: 0, code: 0, incompleteTasks: 0 });
|
||||||
const [activityStats, setActivityStats] = useState<Record<string, number>>({});
|
const [activityStats, setActivityStats] = useState<Record<string, number>>({});
|
||||||
@@ -49,14 +45,8 @@ const UserStatisticsView = () => {
|
|||||||
const days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000);
|
const days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000);
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
if (memoList.size() === 0) return;
|
|
||||||
|
|
||||||
const { memos } = await memoServiceClient.listMemos({
|
|
||||||
pageSize: DEFAULT_MEMO_PAGE_SIZE,
|
|
||||||
view: MemoView.MEMO_VIEW_METADATA_ONLY,
|
|
||||||
});
|
|
||||||
const memoStats: UserMemoStats = { link: 0, taskList: 0, code: 0, incompleteTasks: 0 };
|
const memoStats: UserMemoStats = { link: 0, taskList: 0, code: 0, incompleteTasks: 0 };
|
||||||
memos.forEach((memo) => {
|
metadataList.forEach((memo) => {
|
||||||
const { property } = memo;
|
const { property } = memo;
|
||||||
if (property?.hasLink) {
|
if (property?.hasLink) {
|
||||||
memoStats.link += 1;
|
memoStats.link += 1;
|
||||||
@@ -72,9 +62,9 @@ const UserStatisticsView = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
setMemoStats(memoStats);
|
setMemoStats(memoStats);
|
||||||
setMemoAmount(memos.length);
|
setMemoAmount(metadataList.length);
|
||||||
setActivityStats(countBy(memos.map((memo) => dayjs(memo.displayTime).format("YYYY-MM-DD"))));
|
setActivityStats(countBy(metadataList.map((memo) => dayjs(memo.displayTime).format("YYYY-MM-DD"))));
|
||||||
}, [memoStore.stateId]);
|
}, [memoMetadataStore.stateId]);
|
||||||
|
|
||||||
const rebuildMemoProperty = async () => {
|
const rebuildMemoProperty = async () => {
|
||||||
await memoServiceClient.rebuildMemoProperty({
|
await memoServiceClient.rebuildMemoProperty({
|
||||||
|
@@ -4,5 +4,5 @@ export * from "./inbox";
|
|||||||
export * from "./resourceName";
|
export * from "./resourceName";
|
||||||
export * from "./resource";
|
export * from "./resource";
|
||||||
export * from "./workspaceSetting";
|
export * from "./workspaceSetting";
|
||||||
export * from "./tag";
|
|
||||||
export * from "./memoFilter";
|
export * from "./memoFilter";
|
||||||
|
export * from "./memoMetadata";
|
||||||
|
68
web/src/store/v1/memoMetadata.ts
Normal file
68
web/src/store/v1/memoMetadata.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { uniqueId } from "lodash-es";
|
||||||
|
import { Location } from "react-router-dom";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { combine } from "zustand/middleware";
|
||||||
|
import { memoServiceClient } from "@/grpcweb";
|
||||||
|
import { Routes } from "@/router";
|
||||||
|
import { Memo, MemoView } from "@/types/proto/api/v1/memo_service";
|
||||||
|
import { User } from "@/types/proto/api/v1/user_service";
|
||||||
|
|
||||||
|
// Set the maximum number of memos to fetch.
|
||||||
|
const DEFAULT_MEMO_PAGE_SIZE = 1000000;
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
// stateId is used to identify the store instance state.
|
||||||
|
// It should be update when any state change.
|
||||||
|
stateId: string;
|
||||||
|
dataMapByName: Record<string, Memo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefaultState = (): State => ({
|
||||||
|
stateId: uniqueId(),
|
||||||
|
dataMapByName: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useMemoMetadataStore = create(
|
||||||
|
combine(getDefaultState(), (set, get) => ({
|
||||||
|
setState: (state: State) => set(state),
|
||||||
|
getState: () => get(),
|
||||||
|
fetchMemoMetadata: async (params: { user?: User; location?: Location<any> }) => {
|
||||||
|
const filters = [`row_status == "NORMAL"`];
|
||||||
|
if (params.user) {
|
||||||
|
if (params.location?.pathname === Routes.EXPLORE) {
|
||||||
|
filters.push(`visibilities == ["PUBLIC", "PROTECTED"]`);
|
||||||
|
}
|
||||||
|
filters.push(`creator == "${params.user.name}"`);
|
||||||
|
} else {
|
||||||
|
filters.push(`visibilities == ["PUBLIC"]`);
|
||||||
|
}
|
||||||
|
const { memos, nextPageToken } = await memoServiceClient.listMemos({
|
||||||
|
filter: filters.join(" && "),
|
||||||
|
view: MemoView.MEMO_VIEW_METADATA_ONLY,
|
||||||
|
pageSize: DEFAULT_MEMO_PAGE_SIZE,
|
||||||
|
});
|
||||||
|
const memoMap = { ...get().dataMapByName };
|
||||||
|
for (const memo of memos) {
|
||||||
|
memoMap[memo.name] = memo;
|
||||||
|
}
|
||||||
|
set({ stateId: uniqueId(), dataMapByName: memoMap });
|
||||||
|
return { memos, nextPageToken };
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useMemoTagList = () => {
|
||||||
|
const memoStore = useMemoMetadataStore();
|
||||||
|
const data = Object.values(memoStore.getState().dataMapByName);
|
||||||
|
const tagAmounts: Record<string, number> = {};
|
||||||
|
data.forEach((memo) => {
|
||||||
|
memo.property?.tags.forEach((tag) => {
|
||||||
|
if (tagAmounts[tag]) {
|
||||||
|
tagAmounts[tag] += 1;
|
||||||
|
} else {
|
||||||
|
tagAmounts[tag] = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return tagAmounts;
|
||||||
|
};
|
@@ -1,62 +0,0 @@
|
|||||||
import { Location } from "react-router-dom";
|
|
||||||
import { create } from "zustand";
|
|
||||||
import { combine } from "zustand/middleware";
|
|
||||||
import { memoServiceClient } from "@/grpcweb";
|
|
||||||
import { Routes } from "@/router";
|
|
||||||
import { MemoView } from "@/types/proto/api/v1/memo_service";
|
|
||||||
import { User } from "@/types/proto/api/v1/user_service";
|
|
||||||
|
|
||||||
// Set the maximum number of memos to fetch.
|
|
||||||
const DEFAULT_MEMO_PAGE_SIZE = 1000000;
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
tagAmounts: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDefaultState = (): State => ({
|
|
||||||
tagAmounts: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useTagStore = create(
|
|
||||||
combine(getDefaultState(), (set, get) => ({
|
|
||||||
setState: (state: State) => set(state),
|
|
||||||
getState: () => get(),
|
|
||||||
sortedTags: () => {
|
|
||||||
return Object.entries(get().tagAmounts)
|
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.map(([tag]) => tag);
|
|
||||||
},
|
|
||||||
fetchTags: async (params: { user?: User; location?: Location<any> }, options?: { skipCache?: boolean }) => {
|
|
||||||
const { tagAmounts: cache } = get();
|
|
||||||
if (cache.length > 0 && !options?.skipCache) {
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
const filters = [`row_status == "NORMAL"`];
|
|
||||||
if (params.user) {
|
|
||||||
if (params.location?.pathname === Routes.EXPLORE) {
|
|
||||||
filters.push(`visibilities == ["PUBLIC", "PROTECTED"]`);
|
|
||||||
}
|
|
||||||
filters.push(`creator == "${params.user.name}"`);
|
|
||||||
} else {
|
|
||||||
filters.push(`visibilities == ["PUBLIC"]`);
|
|
||||||
}
|
|
||||||
const { memos } = await memoServiceClient.listMemos({
|
|
||||||
pageSize: DEFAULT_MEMO_PAGE_SIZE,
|
|
||||||
filter: filters.join(" && "),
|
|
||||||
view: MemoView.MEMO_VIEW_METADATA_ONLY,
|
|
||||||
});
|
|
||||||
const tagAmounts: Record<string, number> = {};
|
|
||||||
memos.forEach((memo) => {
|
|
||||||
memo.property?.tags.forEach((tag) => {
|
|
||||||
if (tagAmounts[tag]) {
|
|
||||||
tagAmounts[tag] += 1;
|
|
||||||
} else {
|
|
||||||
tagAmounts[tag] = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
set({ tagAmounts });
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
);
|
|
Reference in New Issue
Block a user