diff --git a/web/src/components/HomeSidebar/TagsSection.tsx b/web/src/components/HomeSidebar/TagsSection.tsx index fafb2c79..73f67b14 100644 --- a/web/src/components/HomeSidebar/TagsSection.tsx +++ b/web/src/components/HomeSidebar/TagsSection.tsx @@ -1,8 +1,9 @@ -import { Dropdown, Menu, MenuButton, MenuItem } from "@mui/joy"; +import { Dropdown, Menu, MenuButton, MenuItem, Switch } from "@mui/joy"; import clsx from "clsx"; import toast from "react-hot-toast"; import { useLocation } from "react-router-dom"; import useDebounce from "react-use/lib/useDebounce"; +import useLocalStorage from "react-use/lib/useLocalStorage"; import { memoServiceClient } from "@/grpcweb"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useFilterStore } from "@/store/module"; @@ -10,6 +11,7 @@ import { useMemoList, useTagStore } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; import Icon from "../Icon"; import showRenameTagDialog from "../RenameTagDialog"; +import TagTree from "../TagTree"; interface Props { readonly?: boolean; @@ -22,6 +24,7 @@ const TagsSection = (props: Props) => { const filterStore = useFilterStore(); const tagStore = useTagStore(); const memoList = useMemoList(); + const [treeMode, setTreeMode] = useLocalStorage("tag-view-as-tree", false); const tagAmounts = Object.entries(tagStore.getState().tagAmounts) .sort((a, b) => a[0].localeCompare(b[0])) .sort((a, b) => b[1] - a[1]); @@ -54,48 +57,62 @@ const TagsSection = (props: Props) => { return (
-
+
{t("common.tags")} - {tagAmounts.length > 0 && ({tagAmounts.length})} + + + + + + + Tree mode + setTreeMode(event.target.checked)} /> + + +
{tagAmounts.length > 0 ? ( -
- {tagAmounts.map(([tag, amount]) => ( -
- - -
- - -
-
- - showRenameTagDialog({ tag: tag })}> - - {t("common.rename")} - - handleDeleteTag(tag)}> - - {t("common.delete")} - - -
+ treeMode ? ( + t[0])} /> + ) : ( +
+ {tagAmounts.map(([tag, amount]) => (
handleTagClick(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" > - {tag} - {amount > 1 && ({amount})} + + +
+ + +
+
+ + showRenameTagDialog({ tag: tag })}> + + {t("common.rename")} + + handleDeleteTag(tag)}> + + {t("common.delete")} + + +
+
handleTagClick(tag)} + > + {tag} + {amount > 1 && ({amount})} +
-
- ))} -
+ ))} +
+ ) ) : ( !props.readonly && (
diff --git a/web/src/components/TagTree.tsx b/web/src/components/TagTree.tsx new file mode 100644 index 00000000..d21abe4d --- /dev/null +++ b/web/src/components/TagTree.tsx @@ -0,0 +1,142 @@ +import { useEffect, useState } from "react"; +import useToggle from "react-use/lib/useToggle"; +import { useFilterStore } from "@/store/module"; +import Icon from "./Icon"; + +interface Tag { + key: string; + text: string; + subTags: Tag[]; +} + +interface Props { + tags: string[]; +} + +const TagTree = ({ tags: rawTags }: Props) => { + const filterStore = useFilterStore(); + const filter = filterStore.state; + const [tags, setTags] = useState([]); + + useEffect(() => { + const sortedTags = Array.from(rawTags).sort(); + const root: Tag = { + key: "", + text: "", + subTags: [], + }; + + for (const tag of sortedTags) { + const subtags = tag.split("/"); + let tempObj = root; + let tagText = ""; + + for (let i = 0; i < subtags.length; i++) { + const key = subtags[i]; + if (i === 0) { + tagText += key; + } else { + tagText += "/" + key; + } + + let obj = null; + + for (const t of tempObj.subTags) { + if (t.text === tagText) { + obj = t; + break; + } + } + + if (!obj) { + obj = { + key, + text: tagText, + subTags: [], + }; + tempObj.subTags.push(obj); + } + + tempObj = obj; + } + } + + setTags(root.subTags as Tag[]); + }, [rawTags]); + + return ( +
+ {tags.map((t, idx) => ( + + ))} +
+ ); +}; + +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 hasSubTags = tag.subTags.length > 0; + const [showSubTags, toggleSubTags] = useToggle(false); + + const handleTagClick = () => { + if (isActive) { + filterStore.setTagFilter(undefined); + } else { + filterStore.setTagFilter(tag.text); + } + }; + + const handleToggleBtnClick = (event: React.MouseEvent) => { + event.stopPropagation(); + toggleSubTags(); + }; + + return ( + <> +
+
+
+ +
+ + {tag.key} + +
+
+ {hasSubTags ? ( + + + + ) : null} +
+
+ {hasSubTags ? ( +
+ {tag.subTags.map((st, idx) => ( + + ))} +
+ ) : null} + + ); +}; + +export default TagTree;