Commit 1f0bfd21 authored by Steven's avatar Steven

chore: update tag store

parent 4aa72306
......@@ -55,11 +55,7 @@ message BatchUpsertTagRequest {
message BatchUpsertTagResponse {}
message ListTagsRequest {
// The creator of tags.
// Format: users/{id}
string user = 1;
}
message ListTagsRequest {}
message ListTagsResponse {
repeated Tag tags = 1;
......
......@@ -2772,11 +2772,6 @@ Used internally for obfuscating the page token.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| user | [string](#string) | | The creator of tags. Format: users/{id} |
......
This diff is collapsed.
......@@ -103,21 +103,10 @@ func local_request_TagService_BatchUpsertTag_0(ctx context.Context, marshaler ru
}
var (
filter_TagService_ListTags_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_TagService_ListTags_0(ctx context.Context, marshaler runtime.Marshaler, client TagServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListTagsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_TagService_ListTags_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.ListTags(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
......@@ -127,13 +116,6 @@ func local_request_TagService_ListTags_0(ctx context.Context, marshaler runtime.
var protoReq ListTagsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_TagService_ListTags_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.ListTags(ctx, &protoReq)
return msg, metadata, err
......
......@@ -638,14 +638,6 @@ paths:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: user
description: |-
The creator of tags.
Format: users/{id}
in: query
required: false
type: string
tags:
- TagService
delete:
......
......@@ -51,12 +51,16 @@ func (s *APIV2Service) BatchUpsertTag(ctx context.Context, request *apiv2pb.Batc
}
func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) {
tagFind := &store.FindTag{}
userID, err := ExtractUserIDFromName(request.User)
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
tagFind := &store.FindTag{
CreatorID: user.ID,
}
tagFind.CreatorID = userID
tags, err := s.Store.ListTags(ctx, tagFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
......
......@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { tagServiceClient } from "@/grpcweb";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useTagStore } from "@/store/module";
import { useTagStore } from "@/store/v1";
import { useTranslate } from "@/utils/i18n";
import { TAG_REG } from "@/utils/tag";
import { generateDialog } from "./Dialog";
......@@ -28,8 +28,8 @@ const CreateTagDialog: React.FC<Props> = (props: Props) => {
const [tagName, setTagName] = useState<string>("");
const [suggestTagNameList, setSuggestTagNameList] = useState<string[]>([]);
const [showTagSuggestions, setShowTagSuggestions] = useState<boolean>(false);
const tagNameList = tagStore.state.tags;
const shownSuggestTagNameList = suggestTagNameList.filter((tag) => !tagNameList.includes(tag));
const tagNameList = tagStore.getState().tags;
const shownSuggestTagNameList = suggestTagNameList.filter((tag) => !tagNameList.has(tag));
useEffect(() => {
tagServiceClient
......@@ -107,7 +107,7 @@ const CreateTagDialog: React.FC<Props> = (props: Props) => {
startDecorator={<Icon.Hash className="w-4 h-auto" />}
endDecorator={<Icon.Check onClick={handleSaveBtnClick} className="w-4 h-auto cursor-pointer hover:opacity-80" />}
/>
{tagNameList.length > 0 && (
{tagNameList.size > 0 && (
<>
<p className="w-full mt-2 mb-1 text-sm text-gray-400">{t("tag.all-tags")}</p>
<div className="w-full flex flex-row justify-start items-start flex-wrap">
......
import { Dropdown, Menu, MenuButton, MenuItem } from "@mui/joy";
import { useEffect, useState } from "react";
import useDebounce from "react-use/lib/useDebounce";
import useToggle from "react-use/lib/useToggle";
import { useFilterStore, useTagStore } from "@/store/module";
import { useMemoList } from "@/store/v1";
import { useFilterStore } from "@/store/module";
import { useMemoList, useTagStore } from "@/store/v1";
import { useTranslate } from "@/utils/i18n";
import showCreateTagDialog from "../CreateTagDialog";
import { showCommonDialog } from "../Dialog/CommonDialog";
......@@ -24,13 +25,17 @@ const TagsSection = () => {
const filterStore = useFilterStore();
const tagStore = useTagStore();
const memoList = useMemoList();
const tagsText = tagStore.state.tags;
const tagsText = tagStore.getState().tags;
const filter = filterStore.state;
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
tagStore.fetchTags();
}, [memoList.size()]);
useDebounce(
() => {
tagStore.fetchTags();
},
300,
[memoList.size()],
);
useEffect(() => {
const sortedTags = Array.from(tagsText).sort();
......
......@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
import useClickAway from "react-use/lib/useClickAway";
import Icon from "@/components/Icon";
import OverflowTip from "@/components/kit/OverflowTip";
import { useTagStore } from "@/store/module";
import { useTagStore } from "@/store/v1";
import { useTranslate } from "@/utils/i18n";
import { EditorRefActions } from "../Editor";
......@@ -17,7 +17,7 @@ const TagSelector = (props: Props) => {
const tagStore = useTagStore();
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const tags = tagStore.state.tags;
const tags = tagStore.getState().tags;
useEffect(() => {
(async () => {
......@@ -60,9 +60,9 @@ const TagSelector = (props: Props) => {
</MenuButton>
<Menu className="relative text-sm" component="div" size="sm" placement="bottom-start">
<div ref={containerRef}>
{tags.length > 0 ? (
{tags.size > 0 ? (
<div className="flex-row justify-start items-start flex-wrap px-1 max-w-[12rem] h-auto max-h-48 overflow-y-auto font-mono">
{tags.map((tag) => {
{Array.from(tags).map((tag) => {
return (
<div
key={tag}
......
......@@ -3,7 +3,7 @@ import Fuse from "fuse.js";
import { useEffect, useRef, useState } from "react";
import getCaretCoordinates from "textarea-caret";
import OverflowTip from "@/components/kit/OverflowTip";
import { useTagStore } from "@/store/module";
import { useTagStore } from "@/store/v1";
import { EditorRefActions } from ".";
type Props = {
......@@ -15,16 +15,16 @@ type Position = { left: number; top: number; height: number };
const TagSuggestions = ({ editorRef, editorActions }: Props) => {
const [position, setPosition] = useState<Position | null>(null);
const hide = () => setPosition(null);
const { state } = useTagStore();
const tagsRef = useRef(state.tags);
tagsRef.current = state.tags;
const tagStore = useTagStore();
const tagsRef = useRef(Array.from(tagStore.getState().tags));
tagsRef.current = Array.from(tagStore.getState().tags);
const [selected, select] = useState(0);
const selectedRef = useRef(selected);
selectedRef.current = selected;
const hide = () => setPosition(null);
const getCurrentWord = (): [word: string, startIndex: number] => {
const editor = editorRef.current;
if (!editor) return ["", 0];
......
import { IconButton } from "@mui/joy";
import { useEffect } from "react";
import { useTagStore } from "@/store/module";
import { useTagStore } from "@/store/v1";
import { MemoRelation } from "@/types/proto/api/v2/memo_relation_service";
import MemoEditor from ".";
import { generateDialog } from "../Dialog";
......
......@@ -7,8 +7,7 @@ import { memoServiceClient } from "@/grpcweb";
import { TAB_SPACE_WIDTH } from "@/helpers/consts";
import { isValidUrl } from "@/helpers/utils";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useTagStore } from "@/store/module";
import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore } from "@/store/v1";
import { useMemoStore, useResourceStore, useUserStore, useWorkspaceSettingStore, useTagStore } from "@/store/v1";
import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service";
import { Memo, Visibility } from "@/types/proto/api/v2/memo_service";
import { Resource } from "@/types/proto/api/v2/resource_service";
......
import { useEffect, useState } from "react";
import { memoServiceClient } from "@/grpcweb";
import { useTagStore } from "@/store/module";
import { useMemoStore } from "@/store/v1";
import { useMemoStore, useTagStore } from "@/store/v1";
import { User } from "@/types/proto/api/v2/user_service";
import { useTranslate } from "@/utils/i18n";
import Icon from "./Icon";
......@@ -19,7 +18,7 @@ const UserStatisticsView = (props: Props) => {
const [isRequesting, setIsRequesting] = useState(false);
const days = Math.ceil((Date.now() - user.createTime!.getTime()) / 86400000);
const memos = Object.values(memoStore.getState().memoMapByName);
const tags = tagStore.state.tags.length;
const tags = tagStore.getState().tags.size;
useEffect(() => {
if (memos.length === 0) {
......
......@@ -3,11 +3,9 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import dialogReducer from "./reducer/dialog";
import filterReducer from "./reducer/filter";
import resourceReducer from "./reducer/resource";
import tagReducer from "./reducer/tag";
const store = configureStore({
reducer: {
tag: tagReducer,
filter: filterReducer,
resource: resourceReducer,
dialog: dialogReducer,
......
export * from "./filter";
export * from "./tag";
export * from "./dialog";
import { tagServiceClient } from "@/grpcweb";
import useCurrentUser from "@/hooks/useCurrentUser";
import store, { useAppSelector } from "..";
import { deleteTag as deleteTagAction, setTags, upsertTag as upsertTagAction } from "../reducer/tag";
export const useTagStore = () => {
const state = useAppSelector((state) => state.tag);
const currentUser = useCurrentUser();
const getState = () => {
return store.getState().tag;
};
const fetchTags = async () => {
const { tags } = await tagServiceClient.listTags({
user: currentUser.name,
});
store.dispatch(setTags(tags.map((tag) => tag.name)));
};
const upsertTag = async (tagName: string) => {
await tagServiceClient.upsertTag({
name: tagName,
});
store.dispatch(upsertTagAction(tagName));
};
const batchUpsertTag = async (tagNames: string[]) => {
await tagServiceClient.batchUpsertTag({
requests: tagNames.map((name) => ({
name,
})),
});
for (const tagName of tagNames) {
store.dispatch(upsertTagAction(tagName));
}
};
const deleteTag = async (tagName: string) => {
await tagServiceClient.deleteTag({
tag: {
name: tagName,
creator: currentUser.name,
},
});
store.dispatch(deleteTagAction(tagName));
};
return {
state,
getState,
fetchTags,
upsertTag,
batchUpsertTag,
deleteTag,
};
};
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface State {
tags: string[];
}
const tagSlice = createSlice({
name: "tag",
initialState: {
tags: [],
} as State,
reducers: {
setTags: (state, action: PayloadAction<string[]>) => {
return {
...state,
tags: action.payload,
};
},
upsertTag: (state, action: PayloadAction<string>) => {
if (state.tags.includes(action.payload)) {
return state;
}
return {
...state,
tags: state.tags.concat(action.payload),
};
},
deleteTag: (state, action: PayloadAction<string>) => {
return {
...state,
tags: state.tags.filter((tag) => {
return tag !== action.payload;
}),
};
},
},
});
export const { setTags, upsertTag, deleteTag } = tagSlice.actions;
export default tagSlice.reducer;
......@@ -4,3 +4,4 @@ export * from "./inbox";
export * from "./resourceName";
export * from "./resource";
export * from "./workspaceSetting";
export * from "./tag";
import { create } from "zustand";
import { combine } from "zustand/middleware";
import { tagServiceClient } from "@/grpcweb";
interface State {
tags: Set<string>;
}
const getDefaultState = (): State => ({
tags: new Set(),
});
export const useTagStore = create(
combine(getDefaultState(), (set, get) => ({
setState: (state: State) => set(state),
getState: () => get(),
fetchTags: async () => {
const { tags } = await tagServiceClient.listTags({});
set({ tags: new Set(tags.map((tag) => tag.name)) });
return tags;
},
upsertTag: async (tagName: string) => {
await tagServiceClient.upsertTag({
name: tagName,
});
const { tags } = get();
set({ tags: new Set([...tags, tagName]) });
},
batchUpsertTag: async (tagNames: string[]) => {
await tagServiceClient.batchUpsertTag({
requests: tagNames.map((name) => ({
name,
})),
});
const { tags } = get();
set({ tags: new Set([...tags, ...tagNames]) });
},
deleteTag: async (tagName: string) => {
await tagServiceClient.deleteTag({
tag: {
name: tagName,
},
});
const { tags } = get();
tags.delete(tagName);
set({ tags });
},
})),
);
......@@ -29,6 +29,7 @@ const requestCache = new Map<string, Promise<any>>();
export const useUserStore = create(
combine(getDefaultState(), (set, get) => ({
getState: () => get(),
fetchUsers: async () => {
const { users } = await userServiceClient.listUsers({});
const userMap = get().userMapByName;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment