diff --git a/frontend/src/network/network.ts b/frontend/src/network/network.ts index a627c10..638ceac 100644 --- a/frontend/src/network/network.ts +++ b/frontend/src/network/network.ts @@ -435,6 +435,24 @@ class Network { } } + async setTagAlias(tagID: number, aliases: string[]): Promise> { + try { + const response = await axios.put( + `${this.apiBaseUrl}/tag/${tagID}/alias`, + { + aliases, + }, + ); + return response.data; + } catch (e: any) { + console.error(e); + return { + success: false, + message: e.toString(), + }; + } + } + /** * Upload image and return the image id */ diff --git a/frontend/src/pages/tagged_resources_page.tsx b/frontend/src/pages/tagged_resources_page.tsx index 0b4a0d9..2c1f4b8 100644 --- a/frontend/src/pages/tagged_resources_page.tsx +++ b/frontend/src/pages/tagged_resources_page.tsx @@ -12,6 +12,7 @@ import Input, { TextArea } from "../components/input.tsx"; import TagInput from "../components/tag_input.tsx"; import Badge from "../components/badge.tsx"; import { useAppContext } from "../components/AppContext.tsx"; +import { MdAdd, MdClose, MdEdit } from "react-icons/md"; export default function TaggedResourcesPage() { const { tag: tagName } = useParams(); @@ -45,8 +46,8 @@ export default function TaggedResourcesPage() { return (
-
-

+
+

{tag?.name ?? tagName}

{tag && app.canUpload() && ( @@ -67,6 +68,9 @@ export default function TaggedResourcesPage() { {(tag?.aliases ?? []).map((e) => { return {e}; })} + {app.canUpload() && tag && ( + + )}
{tag?.description && (
@@ -216,3 +220,138 @@ function EditTagButton({ ); } + +function EditAliasDialog({ + tag, + onEdited, +}: { + tag: Tag; + onEdited?: (t: Tag) => void; +}) { + const { t } = useTranslation(); + const [alias, setAlias] = useState(() => { + return tag.aliases ?? []; + }); + const [isLoading, setIsLoading] = useState(false); + const [content, setContent] = useState(""); + const [error, setError] = useState(null); + + const submit = async () => { + if (isLoading) { + return; + } + setError(null); + + // compare alias and tag.aliases + let isModified = false; + if (alias.length !== tag.aliases?.length) { + isModified = true; + } else { + for (let i = 0; i < alias.length; i++) { + if (alias[i] !== tag.aliases![i]) { + isModified = true; + break; + } + } + } + if (!isModified) { + setError(t("No changes made")); + return; + } + + setIsLoading(true); + const res = await network.setTagAlias(tag.id, alias); + setIsLoading(false); + if (res.success) { + const dialog = document.getElementById( + "edit_alias_dialog", + ) as HTMLDialogElement; + dialog.close(); + if (onEdited) { + onEdited(res.data!); + } + } else { + setError(res.message || t("Unknown error")); + } + }; + + return ( + <> + { + setError(null); + setAlias(tag.aliases ?? []); + const dialog = document.getElementById( + "edit_alias_dialog", + ) as HTMLDialogElement; + dialog.showModal(); + }} + > + + {t("Edit")} + + +
+

{t("Edit Alias")}

+

+ {alias.map((e) => { + return ( + + {e} + { + setAlias((prev) => prev.filter((x) => x !== e)); + }} + > + + + + ); + })} +

+
+ { + setContent(e.currentTarget.value); + }} + /> + + +
+ {error && } +
+
+ +
+ +
+
+
+ + ); +} diff --git a/server/api/tag.go b/server/api/tag.go index 550be42..eb709f9 100644 --- a/server/api/tag.go +++ b/server/api/tag.go @@ -180,12 +180,44 @@ func getOrCreateTags(c fiber.Ctx) error { }) } +func editTagAlias(c fiber.Ctx) error { + uid, ok := c.Locals("uid").(uint) + if !ok { + return model.NewUnAuthorizedError("You must be logged in to edit tag aliases") + } + + idStr := c.Params("id") + id, err := strconv.Atoi(idStr) + if err != nil { + return model.NewRequestError("Invalid tag ID") + } + + var req struct { + Aliases []string `json:"aliases"` + } + if err := c.Bind().JSON(&req); err != nil { + return model.NewRequestError("Invalid request format") + } + + tag, err := service.EditTagAlias(uid, uint(id), req.Aliases) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(model.Response[model.TagView]{ + Success: true, + Data: *tag, + Message: "Tag aliases updated successfully", + }) +} + func AddTagRoutes(api fiber.Router) { tag := api.Group("/tag") { tag.Post("/", handleCreateTag) tag.Get("/search", handleSearchTag) tag.Delete("/:id", handleDeleteTag) + tag.Put("/:id/alias", editTagAlias) tag.Put("/:id/info", handleSetTagInfo) tag.Get("/:name", handleGetTagByName) tag.Get("/", getAllTags) diff --git a/server/dao/tag.go b/server/dao/tag.go index 05af143..5dc8292 100644 --- a/server/dao/tag.go +++ b/server/dao/tag.go @@ -3,12 +3,16 @@ package dao import ( "errors" "nysoure/server/model" + "strings" "gorm.io/gorm" ) func CreateTag(tag string) (model.Tag, error) { // Create a new tag in the database + if strings.Contains(tag, "%") { + return model.Tag{}, model.NewRequestError("Tag name cannot contain '%' character") + } t := model.Tag{Name: tag} if err := db.Create(&t).Error; err != nil { return model.Tag{}, err @@ -18,6 +22,9 @@ func CreateTag(tag string) (model.Tag, error) { func CreateTagWithType(tag string, tagType string) (model.Tag, error) { // Create a new tag with a specific type in the database + if strings.Contains(tag, "%") { + return model.Tag{}, model.NewRequestError("Tag name cannot contain '%' character") + } t := model.Tag{Name: tag, Type: tagType} if err := db.Create(&t).Error; err != nil { return model.Tag{}, err @@ -101,3 +108,35 @@ func ListTags() ([]model.Tag, error) { } return tags, nil } + +// SetTagAlias sets a tag with the given ID having the given alias. +func SetTagAlias(tagID uint, alias string) error { + // Set a tag as an alias of another tag + var t model.Tag + if err := db.Where("name = ?", alias).First(&t).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // create + newTag, err := CreateTag(alias) + if err != nil { + return err + } + t = newTag + } else { + return err + } + } + if t.ID == tagID { + return model.NewRequestError("Tag cannot be an alias of itself") + } + return db.Model(&t).Update("alias_of", tagID).Error +} + +// RemoveTagAliasOf sets a tag is an independent tag, removing its alias relationship. +func RemoveTagAliasOf(tagID uint) error { + // Remove the alias of a tag + return db.Model(&model.Tag{ + Model: gorm.Model{ + ID: tagID, + }, + }).Update("alias_of", nil).Error +} diff --git a/server/service/tag.go b/server/service/tag.go index 00db2a9..41c2921 100644 --- a/server/service/tag.go +++ b/server/service/tag.go @@ -4,6 +4,8 @@ import ( "github.com/gofiber/fiber/v3/log" "nysoure/server/dao" "nysoure/server/model" + "slices" + "strings" ) func CreateTag(uid uint, name string) (*model.TagView, error) { @@ -161,3 +163,68 @@ func GetOrCreateTags(uid uint, names []string, tagType string) ([]model.TagView, } return tags, updateCachedTagList() } + +func EditTagAlias(uid uint, tagID uint, aliases []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") + } + + tag, err := dao.GetTagByID(tagID) + if err != nil { + return nil, err + } + + if tag.AliasOf != nil { + return nil, model.NewRequestError("Cannot edit aliases of a tag that is an alias of another tag") + } + + // trim params + for i, alias := range aliases { + aliases[i] = strings.TrimSpace(alias) + if aliases[i] == "" { + return nil, model.NewRequestError("Alias cannot be empty") + } + } + + // new aliases + for _, name := range aliases { + if name == "" { + continue + } + exists := false + for _, alias := range tag.Aliases { + if alias.Name == name { + exists = true + break + } + } + if !exists { + err := dao.SetTagAlias(tagID, name) + if err != nil { + return nil, err + } + } + } + + // remove old aliases + for _, alias := range tag.Aliases { + if !slices.Contains(aliases, alias.Name) { + err := dao.RemoveTagAliasOf(alias.ID) + if err != nil { + return nil, err + } + } + } + + t, err := dao.GetTagByID(tagID) + if err != nil { + return nil, err + } + + return t.ToView(), updateCachedTagList() +}