Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: updated tags list UI #3310

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
131 changes: 57 additions & 74 deletions web/src/components/HomeSidebar/TagsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dropdown, Menu, MenuButton, MenuItem } from "@mui/joy";
import { Autocomplete, AutocompleteOption, Dropdown, Menu, MenuButton, MenuItem } from "@mui/joy";
import { useState } from "react";
import useDebounce from "react-use/lib/useDebounce";
import { useFilterStore } from "@/store/module";
import { useMemoList, useTagStore } from "@/store/v1";
Expand All @@ -13,7 +14,6 @@ const TagsSection = () => {
const filterStore = useFilterStore();
const tagStore = useTagStore();
const memoList = useMemoList();
const filter = filterStore.state;
const tags = tagStore.tags;

useDebounce(
Expand All @@ -24,6 +24,22 @@ const TagsSection = () => {
[memoList.size()],
);

const [search, setSearch] = useState<string | null>();

const handleDeleteTag = async (tagValue: string) => {
showCommonDialog({
title: t("tag.delete-tag"),
content: t("tag.delete-confirm"),
style: "danger",
dialogName: "delete-tag-dialog",
onConfirm: async () => {
await tagStore.deleteTag(tagValue);
tagStore.fetchTags({ skipCache: true });
setSearch("");
},
});
};

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">
Expand All @@ -32,10 +48,45 @@ const TagsSection = () => {
</span>
</div>
{tags.size > 0 ? (
<div className="flex flex-col justify-start items-start relative w-full h-auto flex-nowrap gap-2 mt-1">
{Array.from(tags).map((tag) => (
<TagItemContainer key={tag} tag={tag} tagQuery={filter.tag} />
))}
<div className="w-full flex items-center justify-between">
<Autocomplete
key={search}
options={Array.from(tags)}
autoHighlight
renderOption={(props, option) => (
<AutocompleteOption {...props}>
<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">
<Icon.Hash className="group-hover:hidden w-4 h-auto shrink-0 opacity-60 mr-1" />
<span className="truncate cursor-pointer hover:opacity-80">{option}</span>
</div>
</div>
</AutocompleteOption>
)}
className="w-full"
value={search || null}
onChange={(event, searchValue) => {
setSearch(searchValue);
filterStore.setTagFilter(searchValue || "");
}}
/>
{search && (
<Dropdown>
<MenuButton variant="plain">
<Icon.MoreVertical />
</MenuButton>
<Menu size="sm" placement="bottom">
<MenuItem onClick={() => showRenameTagDialog({ tag: search })}>
<Icon.Edit3 className="w-4 h-auto" />
{t("common.rename")}
</MenuItem>
<MenuItem color="danger" onClick={() => handleDeleteTag(search)}>
<Icon.Trash className="w-4 h-auto" />
{t("common.delete")}
</MenuItem>
</Menu>
</Dropdown>
)}
</div>
) : (
<div className="p-2 border rounded-md flex flex-row justify-start items-start gap-1 text-gray-400 dark:text-gray-500">
Expand All @@ -47,72 +98,4 @@ const TagsSection = () => {
);
};

interface TagItemContainerProps {
tag: string;
tagQuery?: string;
}

const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => {
const t = useTranslate();
const filterStore = useFilterStore();
const tagStore = useTagStore();
const { tag, tagQuery } = props;
const isActive = tagQuery === tag;

const handleTagClick = () => {
if (isActive) {
filterStore.setTagFilter(undefined);
} else {
filterStore.setTagFilter(tag);
}
};

const handleDeleteTag = async () => {
showCommonDialog({
title: t("tag.delete-tag"),
content: t("tag.delete-confirm"),
style: "danger",
dialogName: "delete-tag-dialog",
onConfirm: async () => {
await tagStore.deleteTag(tag);
tagStore.fetchTags({ skipCache: true });
},
});
};

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"
}`}
>
<Dropdown>
<MenuButton slots={{ root: "div" }}>
<div className="shrink-0 group">
<Icon.Hash className="group-hover:hidden w-4 h-auto shrink-0 opacity-60 mr-1" />
<Icon.MoreVertical className="hidden group-hover:block w-4 h-auto shrink-0 opacity-60 mr-1" />
</div>
</MenuButton>
<Menu size="sm" placement="bottom">
<MenuItem onClick={() => showRenameTagDialog({ tag: tag })}>
<Icon.Edit3 className="w-4 h-auto" />
{t("common.rename")}
</MenuItem>
<MenuItem color="danger" onClick={handleDeleteTag}>
<Icon.Trash className="w-4 h-auto" />
{t("common.delete")}
</MenuItem>
</Menu>
</Dropdown>
<span className="truncate cursor-pointer hover:opacity-80" onClick={handleTagClick}>
{tag}
</span>
</div>
</div>
</>
);
};

export default TagsSection;