chore: introduce tag view option

pull/3692/head
Steven 9 months ago
parent 260a4f89fc
commit f2cfc528a6

@ -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<boolean>("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 (
<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-start 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>
{tagAmounts.length > 0 && <span className="shrink-0">({tagAmounts.length})</span>}
<Dropdown>
<MenuButton slots={{ root: "div" }}>
<Icon.MoreHorizontal className="w-4 h-auto shrink-0 opacity-60" />
</MenuButton>
<Menu size="sm" placement="bottom-end">
<MenuItem>
<span className="text-sm shrink-0 mr-2">Tree mode</span>
<Switch size="sm" checked={treeMode} onChange={(event) => setTreeMode(event.target.checked)} />
</MenuItem>
</Menu>
</Dropdown>
</div>
{tagAmounts.length > 0 ? (
<div className="w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1">
{tagAmounts.map(([tag, amount]) => (
<div
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"
>
<Dropdown>
<MenuButton slots={{ root: "div" }}>
<div className="shrink-0 group">
<Icon.Hash className="group-hover:hidden w-4 h-auto shrink-0 opacity-40" />
<Icon.MoreVertical className="hidden group-hover:block w-4 h-auto shrink-0 opacity-60" />
</div>
</MenuButton>
<Menu size="sm" placement="bottom-start">
<MenuItem onClick={() => showRenameTagDialog({ tag: tag })}>
<Icon.Edit3 className="w-4 h-auto" />
{t("common.rename")}
</MenuItem>
<MenuItem color="danger" onClick={() => handleDeleteTag(tag)}>
<Icon.Trash className="w-4 h-auto" />
{t("common.delete")}
</MenuItem>
</Menu>
</Dropdown>
treeMode ? (
<TagTree tags={tagAmounts.map((t) => t[0])} />
) : (
<div className="w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1">
{tagAmounts.map(([tag, amount]) => (
<div
className={clsx(
"inline-flex flex-nowrap ml-0.5 gap-0.5 cursor-pointer max-w-[calc(100%-16px)]",
filterStore.state.tag === tag && "text-blue-600 dark:text-blue-400",
)}
onClick={() => 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"
>
<span className="truncate dark:opacity-80">{tag}</span>
{amount > 1 && <span className="opacity-60 shrink-0">({amount})</span>}
<Dropdown>
<MenuButton slots={{ root: "div" }}>
<div className="shrink-0 group">
<Icon.Hash className="group-hover:hidden w-4 h-auto shrink-0 opacity-40" />
<Icon.MoreVertical className="hidden group-hover:block w-4 h-auto shrink-0 opacity-60" />
</div>
</MenuButton>
<Menu size="sm" placement="bottom-start">
<MenuItem onClick={() => showRenameTagDialog({ tag: tag })}>
<Icon.Edit3 className="w-4 h-auto" />
{t("common.rename")}
</MenuItem>
<MenuItem color="danger" onClick={() => handleDeleteTag(tag)}>
<Icon.Trash className="w-4 h-auto" />
{t("common.delete")}
</MenuItem>
</Menu>
</Dropdown>
<div
className={clsx(
"inline-flex flex-nowrap ml-0.5 gap-0.5 cursor-pointer max-w-[calc(100%-16px)]",
filterStore.state.tag === tag && "text-blue-600 dark:text-blue-400",
)}
onClick={() => handleTagClick(tag)}
>
<span className="truncate dark:opacity-80">{tag}</span>
{amount > 1 && <span className="opacity-60 shrink-0">({amount})</span>}
</div>
</div>
</div>
))}
</div>
))}
</div>
)
) : (
!props.readonly && (
<div className="p-2 border border-dashed dark:border-zinc-800 rounded-md flex flex-row justify-start items-start gap-1 text-gray-400 dark:text-gray-500">

@ -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<Tag[]>([]);
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 (
<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} />
))}
</div>
);
};
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 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 (
<>
<div className="relative flex flex-row justify-between items-center w-full leading-6 py-0 mt-px rounded-lg text-sm select-none shrink-0">
<div
className={`flex flex-row justify-start items-center truncate shrink leading-5 mr-1 text-gray-600 dark:text-gray-400 ${
isActive && "!text-blue-600"
}`}
>
<div className="shrink-0">
<Icon.Hash className="w-4 h-auto shrink-0 mr-1 text-gray-400 dark:text-gray-500" />
</div>
<span className="truncate cursor-pointer hover:opacity-80" onClick={handleTagClick}>
{tag.key}
</span>
</div>
<div className="flex flex-row justify-end items-center">
{hasSubTags ? (
<span
className={`flex flex-row justify-center items-center w-6 h-6 shrink-0 transition-all rotate-0 ${showSubTags && "rotate-90"}`}
onClick={handleToggleBtnClick}
>
<Icon.ChevronRight className="w-5 h-5 cursor-pointer text-gray-400 dark:text-gray-500" />
</span>
) : null}
</div>
</div>
{hasSubTags ? (
<div
className={`w-[calc(100%-0.5rem)] flex flex-col justify-start items-start h-auto ml-2 pl-2 border-l-2 border-l-gray-200 dark:border-l-zinc-800 ${
!showSubTags && "!hidden"
}`}
>
{tag.subTags.map((st, idx) => (
<TagItemContainer key={st.text + "-" + idx} tag={st} tagQuery={tagQuery} />
))}
</div>
) : null}
</>
);
};
export default TagTree;
Loading…
Cancel
Save