mirror of https://github.com/usememos/memos
feat: create tag dialog (#814)
parent
e4a8a4d708
commit
68a77b6e1f
@ -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;
|
@ -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));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -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…
Reference in New Issue