mirror of
https://github.com/usememos/memos.git
synced 2025-03-20 12:40:09 +01:00
feat: create tag dialog (#814)
This commit is contained in:
parent
e4a8a4d708
commit
68a77b6e1f
106
server/tag.go
106
server/tag.go
@ -2,20 +2,85 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
metric "github.com/usememos/memos/plugin/metrics"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var tagRegexpList = []*regexp.Regexp{regexp.MustCompile(`^#([^\s#]+?) `), regexp.MustCompile(`^#([^\s#]+?)\s`), regexp.MustCompile(`[^\S]#([^\s#]+?)$`), regexp.MustCompile(`[^\S]#([^\s#]+?) `), regexp.MustCompile(` #([^\s#]+?) `)}
|
||||
|
||||
func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
g.POST("/tag", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
tagUpsert := &api.TagUpsert{
|
||||
CreatorID: userID,
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(tagUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
|
||||
}
|
||||
if tagUpsert.Name == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
|
||||
}
|
||||
|
||||
tag, err := s.Store.UpsertTag(ctx, tagUpsert)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
|
||||
}
|
||||
s.Collector.Collect(ctx, &metric.Metric{
|
||||
Name: "tag created",
|
||||
})
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tag.Name)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tag response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/tag", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
tagFind := &api.TagFind{}
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
tagFind.CreatorID = userID
|
||||
}
|
||||
|
||||
if tagFind.CreatorID == 0 {
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
|
||||
}
|
||||
tagFind.CreatorID = currentUserID
|
||||
}
|
||||
|
||||
tagList, err := s.Store.FindTagList(ctx, tagFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
|
||||
}
|
||||
|
||||
tagNameList := []string{}
|
||||
for _, tag := range tagList {
|
||||
tagNameList = append(tagNameList, tag.Name)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tagNameList)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tags response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/tag/suggestion", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
contentSearch := "#"
|
||||
normalRowStatus := api.Normal
|
||||
@ -65,15 +130,42 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.DELETE("/tag/:tagName", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
tagName := c.Param("tagName")
|
||||
if tagName == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Tag name cannot be empty")
|
||||
}
|
||||
|
||||
tagDelete := &api.TagDelete{
|
||||
Name: tagName,
|
||||
CreatorID: userID,
|
||||
}
|
||||
if err := s.Store.DeleteTag(ctx, tagDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Tag name not found: %s", tagName))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagName)).SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
var tagRegexp = regexp.MustCompile(`#([^\s#]+)`)
|
||||
|
||||
func findTagListFromMemoContent(memoContent string) []string {
|
||||
tagMapSet := make(map[string]bool)
|
||||
for _, tagRegexp := range tagRegexpList {
|
||||
for _, rawTag := range tagRegexp.FindAllString(memoContent, -1) {
|
||||
tag := tagRegexp.ReplaceAllString(rawTag, "$1")
|
||||
tagMapSet[tag] = true
|
||||
}
|
||||
matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
|
||||
for _, v := range matches {
|
||||
tagName := v[1]
|
||||
tagMapSet[tagName] = true
|
||||
}
|
||||
|
||||
tagList := []string{}
|
||||
|
@ -31,11 +31,11 @@ func TestFindTagListFromMemoContent(t *testing.T) {
|
||||
},
|
||||
{
|
||||
memoContent: "#tag1 123123#tag2 \n#tag3 #tag4 ",
|
||||
want: []string{"tag1", "tag3", "tag4"},
|
||||
want: []string{"tag1", "tag2", "tag3", "tag4"},
|
||||
},
|
||||
{
|
||||
memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3 #tag4 http://123123.com?123123#tag2) ",
|
||||
want: []string{"tag1", "tag3", "tag4"},
|
||||
want: []string{"tag1", "tag2", "tag2)", "tag3", "tag4"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
@ -87,7 +87,9 @@ func upsertTag(ctx context.Context, tx *sql.Tx, upsert *api.TagUpsert) (*tagRaw,
|
||||
name, creator_id
|
||||
)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(name, creator_id) DO NOTHING
|
||||
ON CONFLICT(name, creator_id) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name
|
||||
RETURNING name, creator_id
|
||||
`
|
||||
var tagRaw tagRaw
|
||||
|
@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemoStore, useShortcutStore } from "../store/module";
|
||||
import { useShortcutStore, useTagStore } from "../store/module";
|
||||
import { filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import Icon from "./Icon";
|
||||
@ -162,9 +162,9 @@ interface MemoFilterInputerProps {
|
||||
const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInputerProps) => {
|
||||
const { index, filter, handleFilterChange, handleFilterRemove } = props;
|
||||
const { t } = useTranslation();
|
||||
const memoStore = useMemoStore();
|
||||
const tagStore = useTagStore();
|
||||
const [value, setValue] = useState<string>(filter.value.value);
|
||||
const tags = Array.from(memoStore.getState().tags);
|
||||
const tags = Array.from(tagStore.getState().tags);
|
||||
const { type } = filter;
|
||||
|
||||
const typeDataSource = Object.values(filterConsts).map(({ text, value }) => ({ text: t(text), value }));
|
||||
|
140
web/src/components/CreateTagDialog.tsx
Normal file
140
web/src/components/CreateTagDialog.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import { TextField } from "@mui/joy";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTagStore } from "../store/module";
|
||||
import { getTagSuggestionList } from "../helpers/api";
|
||||
import Tag from "../labs/marked/parser/Tag";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import { generateDialog } from "./Dialog";
|
||||
|
||||
type Props = DialogProps;
|
||||
|
||||
const validateTagName = (tagName: string): boolean => {
|
||||
const matchResult = Tag.matcher(`#${tagName}`);
|
||||
if (!matchResult || matchResult[1] !== tagName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const CreateTagDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy } = props;
|
||||
const tagStore = useTagStore();
|
||||
const [tagName, setTagName] = useState<string>("");
|
||||
const [suggestTagNameList, setSuggestTagNameList] = useState<string[]>([]);
|
||||
const tagNameList = tagStore.state.tags;
|
||||
|
||||
useEffect(() => {
|
||||
getTagSuggestionList().then(({ data }) => {
|
||||
setSuggestTagNameList(data.data.filter((tag) => !tagNameList.includes(tag) && validateTagName(tag)));
|
||||
});
|
||||
}, [tagNameList]);
|
||||
|
||||
const handleTagNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const tagName = e.target.value as string;
|
||||
setTagName(tagName.trim());
|
||||
};
|
||||
|
||||
const handleRemoveSuggestTag = (tag: string) => {
|
||||
setSuggestTagNameList(suggestTagNameList.filter((item) => item !== tag));
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!validateTagName(tagName)) {
|
||||
toastHelper.error("Invalid tag name");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await tagStore.upsertTag(tagName);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = async (tag: string) => {
|
||||
await tagStore.deleteTag(tag);
|
||||
};
|
||||
|
||||
const handleSaveSuggestTagList = async () => {
|
||||
for (const tagName of suggestTagNameList) {
|
||||
if (validateTagName(tagName)) {
|
||||
await tagStore.upsertTag(tagName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Create Tag</p>
|
||||
<button className="btn close-btn" onClick={() => destroy()}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container !w-80">
|
||||
<TextField
|
||||
className="mb-2"
|
||||
placeholder="TAG_NAME"
|
||||
value={tagName}
|
||||
onChange={handleTagNameChanged}
|
||||
fullWidth
|
||||
startDecorator={<Icon.Hash className="w-4 h-auto" />}
|
||||
endDecorator={<Icon.CheckCircle onClick={handleSaveBtnClick} className="w-4 h-auto" />}
|
||||
/>
|
||||
{tagNameList.length > 0 && (
|
||||
<>
|
||||
<p className="w-full mt-2 mb-1 text-sm text-gray-400">All tags</p>
|
||||
<div className="w-full flex flex-row justify-start items-start flex-wrap">
|
||||
{tagNameList.map((tag) => (
|
||||
<span
|
||||
className="text-sm mr-2 mt-1 font-mono cursor-pointer truncate hover:opacity-60 hover:line-through"
|
||||
key={tag}
|
||||
onClick={() => handleDeleteTag(tag)}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{suggestTagNameList.length > 0 && (
|
||||
<>
|
||||
<p className="w-full mt-2 mb-1 text-sm text-gray-400">Tag suggestions</p>
|
||||
<div className="w-full flex flex-row justify-start items-start flex-wrap">
|
||||
{suggestTagNameList.map((tag) => (
|
||||
<span
|
||||
className="text-sm mr-2 mt-1 font-mono cursor-pointer truncate hover:opacity-60 hover:line-through"
|
||||
key={tag}
|
||||
onClick={() => handleRemoveSuggestTag(tag)}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 text-sm border px-2 leading-6 rounded cursor-pointer hover:opacity-80 hover:shadow"
|
||||
onClick={handleSaveSuggestTagList}
|
||||
>
|
||||
Save all
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function showCreateTagDialog() {
|
||||
generateDialog(
|
||||
{
|
||||
className: "create-tag-dialog",
|
||||
dialogName: "create-tag-dialog",
|
||||
},
|
||||
CreateTagDialog
|
||||
);
|
||||
}
|
||||
|
||||
export default showCreateTagDialog;
|
@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
|
||||
import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
||||
import { useEditorStore, useLocationStore, useMemoStore, useResourceStore, useUserStore } from "../store/module";
|
||||
import { useEditorStore, useLocationStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "../store/module";
|
||||
import * as storage from "../helpers/storage";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
@ -44,6 +44,7 @@ const MemoEditor = () => {
|
||||
const editorStore = useEditorStore();
|
||||
const locationStore = useLocationStore();
|
||||
const memoStore = useMemoStore();
|
||||
const tagStore = useTagStore();
|
||||
const resourceStore = useResourceStore();
|
||||
|
||||
const [state, setState] = useState<State>({
|
||||
@ -57,7 +58,7 @@ const MemoEditor = () => {
|
||||
const tagSelectorRef = useRef<HTMLDivElement>(null);
|
||||
const user = userStore.state.user as User;
|
||||
const setting = user.setting;
|
||||
const tags = memoStore.state.tags;
|
||||
const tags = tagStore.state.tags;
|
||||
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
||||
return {
|
||||
value: item.value,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||
import { useLocationStore, useTagStore, useUserStore } from "../store/module";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import Icon from "./Icon";
|
||||
import showCreateTagDialog from "./CreateTagDialog";
|
||||
import "../less/tag-list.less";
|
||||
|
||||
interface Tag {
|
||||
@ -15,16 +16,14 @@ const TagList = () => {
|
||||
const { t } = useTranslation();
|
||||
const locationStore = useLocationStore();
|
||||
const userStore = useUserStore();
|
||||
const memoStore = useMemoStore();
|
||||
const { memos, tags: tagsText } = memoStore.state;
|
||||
const tagStore = useTagStore();
|
||||
const tagsText = tagStore.state.tags;
|
||||
const query = locationStore.state.query;
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (memos.length > 0) {
|
||||
memoStore.updateTagsState();
|
||||
}
|
||||
}, [memos]);
|
||||
tagStore.fetchTags();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const sortedTags = Array.from(tagsText).sort();
|
||||
@ -72,7 +71,15 @@ const TagList = () => {
|
||||
|
||||
return (
|
||||
<div className="tags-wrapper">
|
||||
<p className="title-text">{t("common.tags")}</p>
|
||||
<div className="w-full flex flex-row justify-start items-center px-4 mb-1">
|
||||
<span className="text-sm leading-6 font-mono text-gray-400">{t("common.tags")}</span>
|
||||
<button
|
||||
onClick={() => showCreateTagDialog()}
|
||||
className="flex flex-col justify-center items-center w-5 h-5 bg-gray-200 dark:bg-zinc-700 rounded ml-2 hover:shadow"
|
||||
>
|
||||
<Icon.Plus className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="tags-container">
|
||||
{tags.map((t, idx) => (
|
||||
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={query?.tag} />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||
import { useLocationStore, useMemoStore, useTagStore, useUserStore } from "../store/module";
|
||||
import { getMemoStats } from "../helpers/api";
|
||||
import * as utils from "../helpers/utils";
|
||||
import Icon from "./Icon";
|
||||
@ -17,8 +17,10 @@ const UserBanner = () => {
|
||||
const locationStore = useLocationStore();
|
||||
const userStore = useUserStore();
|
||||
const memoStore = useMemoStore();
|
||||
const tagStore = useTagStore();
|
||||
const { user, owner } = userStore.state;
|
||||
const { memos, tags } = memoStore.state;
|
||||
const { memos } = memoStore.state;
|
||||
const tags = tagStore.state.tags;
|
||||
const [username, setUsername] = useState("Memos");
|
||||
const [memoAmount, setMemoAmount] = useState(0);
|
||||
const [createdDays, setCreatedDays] = useState(0);
|
||||
|
@ -187,6 +187,20 @@ export function getTagList(tagFind?: TagFind) {
|
||||
return axios.get<ResponseObject<string[]>>(`/api/tag?${queryList.join("&")}`);
|
||||
}
|
||||
|
||||
export function getTagSuggestionList() {
|
||||
return axios.get<ResponseObject<string[]>>(`/api/tag/suggestion`);
|
||||
}
|
||||
|
||||
export function upsertTag(tagName: string) {
|
||||
return axios.post<ResponseObject<string>>(`/api/tag`, {
|
||||
name: tagName,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteTag(tagName: string) {
|
||||
return axios.delete<ResponseObject<string>>(`/api/tag/${tagName}`);
|
||||
}
|
||||
|
||||
export async function getRepoStarCount() {
|
||||
const { data } = await axios.get(`https://api.github.com/repos/usememos/memos`, {
|
||||
headers: {
|
||||
|
@ -1,10 +1,6 @@
|
||||
.tags-wrapper {
|
||||
@apply flex flex-col justify-start items-start px-2 w-full h-auto flex-nowrap pb-4 mt-2 grow hide-scrollbar;
|
||||
|
||||
> .title-text {
|
||||
@apply w-full px-4 text-sm leading-6 font-mono text-gray-400;
|
||||
}
|
||||
|
||||
> .tags-container {
|
||||
@apply flex flex-col justify-start items-start relative w-full h-auto flex-nowrap mb-2 mt-1;
|
||||
|
||||
|
@ -8,12 +8,14 @@ import shortcutReducer from "./reducer/shortcut";
|
||||
import locationReducer from "./reducer/location";
|
||||
import resourceReducer from "./reducer/resource";
|
||||
import dialogReducer from "./reducer/dialog";
|
||||
import tagReducer from "./reducer/tag";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
global: globalReducer,
|
||||
user: userReducer,
|
||||
memo: memoReducer,
|
||||
tag: tagReducer,
|
||||
editor: editorReducer,
|
||||
shortcut: shortcutReducer,
|
||||
location: locationReducer,
|
||||
|
@ -2,6 +2,7 @@ export * from "./editor";
|
||||
export * from "./global";
|
||||
export * from "./location";
|
||||
export * from "./memo";
|
||||
export * from "./tag";
|
||||
export * from "./resource";
|
||||
export * from "./shortcut";
|
||||
export * from "./user";
|
||||
|
@ -3,7 +3,7 @@ import * as api from "../../helpers/api";
|
||||
import { DEFAULT_MEMO_LIMIT } from "../../helpers/consts";
|
||||
import { useUserStore } from "./";
|
||||
import store, { useAppSelector } from "../";
|
||||
import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos, setTags } from "../reducer/memo";
|
||||
import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos } from "../reducer/memo";
|
||||
|
||||
const convertResponseModelMemo = (memo: Memo): Memo => {
|
||||
return {
|
||||
@ -85,14 +85,6 @@ export const useMemoStore = () => {
|
||||
|
||||
return await fetchMemoById(memoId);
|
||||
},
|
||||
updateTagsState: async () => {
|
||||
const tagFind: TagFind = {};
|
||||
if (userStore.isVisitorMode()) {
|
||||
tagFind.creatorId = userStore.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getTagList(tagFind)).data;
|
||||
store.dispatch(setTags(data));
|
||||
},
|
||||
getLinkedMemos: async (memoId: MemoId): Promise<Memo[]> => {
|
||||
const regex = new RegExp(`[@(.+?)](${memoId})`);
|
||||
return state.memos.filter((m) => m.content.match(regex));
|
||||
|
31
web/src/store/module/tag.ts
Normal file
31
web/src/store/module/tag.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import store, { useAppSelector } from "..";
|
||||
import * as api from "../../helpers/api";
|
||||
import { deleteTag, setTags, upsertTag } from "../reducer/tag";
|
||||
import { useUserStore } from "./";
|
||||
|
||||
export const useTagStore = () => {
|
||||
const state = useAppSelector((state) => state.tag);
|
||||
const userStore = useUserStore();
|
||||
return {
|
||||
state,
|
||||
getState: () => {
|
||||
return store.getState().tag;
|
||||
},
|
||||
fetchTags: async () => {
|
||||
const tagFind: TagFind = {};
|
||||
if (userStore.isVisitorMode()) {
|
||||
tagFind.creatorId = userStore.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getTagList(tagFind)).data;
|
||||
store.dispatch(setTags(data));
|
||||
},
|
||||
upsertTag: async (tagName: string) => {
|
||||
await api.upsertTag(tagName);
|
||||
store.dispatch(upsertTag(tagName));
|
||||
},
|
||||
deleteTag: async (tagName: string) => {
|
||||
await api.deleteTag(tagName);
|
||||
store.dispatch(deleteTag(tagName));
|
||||
},
|
||||
};
|
||||
};
|
@ -2,7 +2,6 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface State {
|
||||
memos: Memo[];
|
||||
tags: string[];
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
@ -10,7 +9,6 @@ const memoSlice = createSlice({
|
||||
name: "memo",
|
||||
initialState: {
|
||||
memos: [],
|
||||
tags: [],
|
||||
// isFetching flag should starts with true.
|
||||
isFetching: true,
|
||||
} as State,
|
||||
@ -52,12 +50,6 @@ const memoSlice = createSlice({
|
||||
}),
|
||||
};
|
||||
},
|
||||
setTags: (state, action: PayloadAction<string[]>) => {
|
||||
return {
|
||||
...state,
|
||||
tags: action.payload,
|
||||
};
|
||||
},
|
||||
setIsFetching: (state, action: PayloadAction<boolean>) => {
|
||||
return {
|
||||
...state,
|
||||
@ -67,6 +59,6 @@ const memoSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMemos, createMemo, patchMemo, deleteMemo, setTags, setIsFetching } = memoSlice.actions;
|
||||
export const { setMemos, createMemo, patchMemo, deleteMemo, setIsFetching } = memoSlice.actions;
|
||||
|
||||
export default memoSlice.reducer;
|
||||
|
42
web/src/store/reducer/tag.ts
Normal file
42
web/src/store/reducer/tag.ts
Normal file
@ -0,0 +1,42 @@
|
||||
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;
|
Loading…
x
Reference in New Issue
Block a user