mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Add Quick Add Tag functionality and improve tag management in Edit and Publish pages
This commit is contained in:
@@ -4,6 +4,9 @@ import {useTranslation} from "react-i18next";
|
|||||||
import {network} from "../network/network.ts";
|
import {network} from "../network/network.ts";
|
||||||
import {LuInfo} from "react-icons/lu";
|
import {LuInfo} from "react-icons/lu";
|
||||||
import {MdSearch} from "react-icons/md";
|
import {MdSearch} from "react-icons/md";
|
||||||
|
import Button from "./button.tsx";
|
||||||
|
import Input, {TextArea} from "./input.tsx";
|
||||||
|
import {ErrorAlert} from "./alert.tsx";
|
||||||
|
|
||||||
export default function TagInput({ onAdd, mainTag }: { onAdd: (tag: Tag) => void, mainTag?: boolean }) {
|
export default function TagInput({ onAdd, mainTag }: { onAdd: (tag: Tag) => void, mainTag?: boolean }) {
|
||||||
const [keyword, setKeyword] = useState<string>("")
|
const [keyword, setKeyword] = useState<string>("")
|
||||||
@@ -143,3 +146,60 @@ class Debounce {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function QuickAddTagDialog({ onAdded }: { onAdded: (tags: Tag[]) => void }) {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
const [text, setText] = useState<string>("")
|
||||||
|
|
||||||
|
const [type, setType] = useState<string>("")
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (text.trim().length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError(null)
|
||||||
|
const names = text.split(",").filter((n) => n.length > 0)
|
||||||
|
const res = await network.getOrCreateTags(names, type)
|
||||||
|
if (!res.success) {
|
||||||
|
setError(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const tags = res.data!
|
||||||
|
onAdded(tags)
|
||||||
|
setText("")
|
||||||
|
setType("")
|
||||||
|
const dialog = document.getElementById("quick_add_tag_dialog") as HTMLDialogElement
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Button className={"btn-soft btn-primary"} onClick={() => {
|
||||||
|
const dialog = document.getElementById("quick_add_tag_dialog") as HTMLDialogElement
|
||||||
|
dialog.showModal()
|
||||||
|
}}>{t("Quick Add")}</Button>
|
||||||
|
<dialog id="quick_add_tag_dialog" className="modal">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">{t('Add Tags')}</h3>
|
||||||
|
<p className="py-2 text-sm">
|
||||||
|
{t("Input tags separated by commas.")}
|
||||||
|
<br/>
|
||||||
|
{t("If the tag does not exist, it will be created automatically.")}
|
||||||
|
<br/>
|
||||||
|
{t("Optionally, you can specify a type for the new tags.")}
|
||||||
|
</p>
|
||||||
|
<TextArea value={text} onChange={(e) => setText(e.target.value)} label={"Tags"}/>
|
||||||
|
<Input value={type} onChange={(e) => setType(e.target.value)} label={"Type"}/>
|
||||||
|
{error && <ErrorAlert className={"mt-2"} message={error}/>}
|
||||||
|
<div className="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<Button className="btn">{t("Cancel")}</Button>
|
||||||
|
</form>
|
||||||
|
<Button className={"btn-primary"} disabled={text === ""} onClick={handleSubmit}>{t("Submit")}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
}
|
@@ -172,6 +172,11 @@ export const i18nData = {
|
|||||||
"About": "About",
|
"About": "About",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"Other": "Other",
|
"Other": "Other",
|
||||||
|
"Quick Add": "Quick Add",
|
||||||
|
"Add Tags": "Add Tags",
|
||||||
|
"Input tags separated by commas.": "Input tags separated by commas.",
|
||||||
|
"If the tag does not exist, it will be created automatically.": "If the tag does not exist, it will be created automatically.",
|
||||||
|
"Optionally, you can specify a type for the new tags.": "Optionally, you can specify a type for the new tags.",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
@@ -347,6 +352,11 @@ export const i18nData = {
|
|||||||
"About": "关于",
|
"About": "关于",
|
||||||
"Home": "首页",
|
"Home": "首页",
|
||||||
"Other": "其他",
|
"Other": "其他",
|
||||||
|
"Quick Add": "快速添加",
|
||||||
|
"Add Tags": "添加标签",
|
||||||
|
"Input tags separated by commas.": "输入标签, 用逗号分隔。",
|
||||||
|
"If the tag does not exist, it will be created automatically.": "如果标签不存在, 将自动创建。",
|
||||||
|
"Optionally, you can specify a type for the new tags.": "您可以选择为新标签指定一个类型。",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-TW": {
|
"zh-TW": {
|
||||||
@@ -522,6 +532,11 @@ export const i18nData = {
|
|||||||
"About": "關於",
|
"About": "關於",
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
"Other": "其他",
|
"Other": "其他",
|
||||||
|
"Quick Add": "快速添加",
|
||||||
|
"Add Tags": "添加標籤",
|
||||||
|
"Input tags separated by commas.": "輸入標籤, 用逗號分隔。",
|
||||||
|
"If the tag does not exist, it will be created automatically.": "如果標籤不存在, 將自動創建。",
|
||||||
|
"Optionally, you can specify a type for the new tags.": "您可以選擇為新標籤指定一個類型。",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -330,6 +330,22 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrCreateTags(names: string[], tagType: string): Promise<Response<Tag[]>> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${this.apiBaseUrl}/tag/batch`, {
|
||||||
|
names,
|
||||||
|
type: tagType
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: e.toString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getTagByName(name: string): Promise<Response<Tag>> {
|
async getTagByName(name: string): Promise<Response<Tag>> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.apiBaseUrl}/tag/${name}`)
|
const response = await axios.get(`${this.apiBaseUrl}/tag/${name}`)
|
||||||
|
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { app } from "../app.ts";
|
import { app } from "../app.ts";
|
||||||
import { ErrorAlert } from "../components/alert.tsx";
|
import { ErrorAlert } from "../components/alert.tsx";
|
||||||
import Loading from "../components/loading.tsx";
|
import Loading from "../components/loading.tsx";
|
||||||
import TagInput from "../components/tag_input.tsx";
|
import TagInput, {QuickAddTagDialog} from "../components/tag_input.tsx";
|
||||||
|
|
||||||
export default function EditResourcePage() {
|
export default function EditResourcePage() {
|
||||||
const [title, setTitle] = useState<string>("")
|
const [title, setTitle] = useState<string>("")
|
||||||
@@ -178,9 +178,30 @@ export default function EditResourcePage() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
<div className={"flex items-center"}>
|
||||||
<TagInput onAdd={(tag) => {
|
<TagInput onAdd={(tag) => {
|
||||||
setTags([...tags, tag])
|
setTags((prev) => {
|
||||||
|
const existingTag = prev.find(t => t.id === tag.id);
|
||||||
|
if (existingTag) {
|
||||||
|
return prev; // If the tag already exists, do not add it again
|
||||||
|
}
|
||||||
|
return [...prev, tag];
|
||||||
|
})
|
||||||
}} />
|
}} />
|
||||||
|
<span className={"w-4"}/>
|
||||||
|
<QuickAddTagDialog onAdded={(tags) => {
|
||||||
|
setTags((prev) => {
|
||||||
|
const newTags = [...prev];
|
||||||
|
for (const tag of tags) {
|
||||||
|
const existingTag = newTags.find(t => t.id === tag.id);
|
||||||
|
if (!existingTag) {
|
||||||
|
newTags.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newTags;
|
||||||
|
})
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
<div className={"h-4"}></div>
|
<div className={"h-4"}></div>
|
||||||
<p className={"my-1"}>{t("Description")}</p>
|
<p className={"my-1"}>{t("Description")}</p>
|
||||||
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
|
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
|
||||||
|
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { app } from "../app.ts";
|
import { app } from "../app.ts";
|
||||||
import { ErrorAlert } from "../components/alert.tsx";
|
import { ErrorAlert } from "../components/alert.tsx";
|
||||||
import {useAppContext} from "../components/AppContext.tsx";
|
import {useAppContext} from "../components/AppContext.tsx";
|
||||||
import TagInput from "../components/tag_input.tsx";
|
import TagInput, {QuickAddTagDialog} from "../components/tag_input.tsx";
|
||||||
|
|
||||||
export default function PublishPage() {
|
export default function PublishPage() {
|
||||||
const [title, setTitle] = useState<string>("")
|
const [title, setTitle] = useState<string>("")
|
||||||
@@ -154,9 +154,30 @@ export default function PublishPage() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
<div className={"flex items-center"}>
|
||||||
<TagInput onAdd={(tag) => {
|
<TagInput onAdd={(tag) => {
|
||||||
setTags([...tags, tag])
|
setTags((prev) => {
|
||||||
|
const existingTag = prev.find(t => t.id === tag.id);
|
||||||
|
if (existingTag) {
|
||||||
|
return prev; // If the tag already exists, do not add it again
|
||||||
|
}
|
||||||
|
return [...prev, tag];
|
||||||
|
})
|
||||||
}} />
|
}} />
|
||||||
|
<span className={"w-4"}/>
|
||||||
|
<QuickAddTagDialog onAdded={(tags) => {
|
||||||
|
setTags((prev) => {
|
||||||
|
const newTags = [...prev];
|
||||||
|
for (const tag of tags) {
|
||||||
|
const existingTag = newTags.find(t => t.id === tag.id);
|
||||||
|
if (!existingTag) {
|
||||||
|
newTags.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newTags;
|
||||||
|
})
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
<div className={"h-4"}></div>
|
<div className={"h-4"}></div>
|
||||||
<p className={"my-1"}>{t("Description")}</p>
|
<p className={"my-1"}>{t("Description")}</p>
|
||||||
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
|
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
|
||||||
|
@@ -137,6 +137,49 @@ func getAllTags(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOrCreateTags(c fiber.Ctx) error {
|
||||||
|
uid, ok := c.Locals("uid").(uint)
|
||||||
|
if !ok {
|
||||||
|
return model.NewUnAuthorizedError("You must be logged in to get or create tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetOrCreateTagsRequest struct {
|
||||||
|
Names []string `json:"names"`
|
||||||
|
TagType string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var req GetOrCreateTagsRequest
|
||||||
|
if err := c.Bind().JSON(&req); err != nil {
|
||||||
|
return model.NewRequestError("Invalid request format")
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(req.Names))
|
||||||
|
for _, name := range req.Names {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
return model.NewRequestError("At least one tag name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
tagType := strings.TrimSpace(req.TagType)
|
||||||
|
|
||||||
|
tags, err := service.GetOrCreateTags(uid, names, tagType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[*[]model.TagView]{
|
||||||
|
Success: true,
|
||||||
|
Data: &tags,
|
||||||
|
Message: "Tags retrieved or created successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AddTagRoutes(api fiber.Router) {
|
func AddTagRoutes(api fiber.Router) {
|
||||||
tag := api.Group("/tag")
|
tag := api.Group("/tag")
|
||||||
{
|
{
|
||||||
@@ -146,5 +189,6 @@ func AddTagRoutes(api fiber.Router) {
|
|||||||
tag.Put("/:id/info", handleSetTagInfo)
|
tag.Put("/:id/info", handleSetTagInfo)
|
||||||
tag.Get("/:name", handleGetTagByName)
|
tag.Get("/:name", handleGetTagByName)
|
||||||
tag.Get("/", getAllTags)
|
tag.Get("/", getAllTags)
|
||||||
|
tag.Post("/batch", getOrCreateTags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,15 @@ func CreateTag(tag string) (model.Tag, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateTagWithType(tag string, tagType string) (model.Tag, error) {
|
||||||
|
// Create a new tag with a specific type in the database
|
||||||
|
t := model.Tag{Name: tag, Type: tagType}
|
||||||
|
if err := db.Create(&t).Error; err != nil {
|
||||||
|
return model.Tag{}, err
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
func SearchTag(keyword string, mainTag bool) ([]model.Tag, error) {
|
func SearchTag(keyword string, mainTag bool) ([]model.Tag, error) {
|
||||||
// Search for a tag by its name in the database
|
// Search for a tag by its name in the database
|
||||||
var t []model.Tag
|
var t []model.Tag
|
||||||
|
@@ -134,3 +134,30 @@ func GetTagList() ([]model.TagViewWithCount, error) {
|
|||||||
}
|
}
|
||||||
return cachedTagList, nil
|
return cachedTagList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetOrCreateTags(uid uint, names []string, tagType string) ([]model.TagView, error) {
|
||||||
|
canUpload, err := checkUserCanUpload(uid)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error checking user permissions:", err)
|
||||||
|
return nil, model.NewInternalServerError("Error checking user permissions")
|
||||||
|
}
|
||||||
|
if !canUpload {
|
||||||
|
return nil, model.NewUnAuthorizedError("User cannot create tags")
|
||||||
|
}
|
||||||
|
tags := make([]model.TagView, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
t, err := dao.GetTagByName(name)
|
||||||
|
if err != nil {
|
||||||
|
if model.IsNotFoundError(err) {
|
||||||
|
t, err = dao.CreateTagWithType(name, tagType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags = append(tags, *t.ToView())
|
||||||
|
}
|
||||||
|
return tags, updateCachedTagList()
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user