mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 20:27:23 +00:00
Add tag type and tag alias.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
|
|
||||||
export default function Badge({children, className, onClick }: { children: ReactNode, className?: string, onClick?: () => void }) {
|
export default function Badge({children, className, onClick }: { children: ReactNode, className?: string, onClick?: () => void }) {
|
||||||
return <span className={`badge badge-primary text-sm ${className}`} onClick={onClick}>{children}</span>
|
return <span className={`badge ${!className?.includes("badge-") && "badge-primary"} ${className}`} onClick={onClick}>{children}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BadgeAccent({children, className, onClick }: { children: ReactNode, className?: string, onClick?: () => void }) {
|
export function BadgeAccent({children, className, onClick }: { children: ReactNode, className?: string, onClick?: () => void }) {
|
||||||
|
142
frontend/src/components/tag_input.tsx
Normal file
142
frontend/src/components/tag_input.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import {Tag} from "../network/models.ts";
|
||||||
|
import {useRef, useState} from "react";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {network} from "../network/network.ts";
|
||||||
|
import {LuInfo} from "react-icons/lu";
|
||||||
|
import {MdSearch} from "react-icons/md";
|
||||||
|
|
||||||
|
export default function TagInput({ onAdd, mainTag }: { onAdd: (tag: Tag) => void, mainTag?: boolean }) {
|
||||||
|
const [keyword, setKeyword] = useState<string>("")
|
||||||
|
const [tags, setTags] = useState<Tag[]>([])
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isLoading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const debounce = useRef(new Debounce(500))
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const searchTags = async (keyword: string) => {
|
||||||
|
if (keyword.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
setTags([])
|
||||||
|
setError(null)
|
||||||
|
const res = await network.searchTags(keyword, mainTag)
|
||||||
|
if (!res.success) {
|
||||||
|
setError(res.message)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTags(res.data!)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = async (v: string) => {
|
||||||
|
setKeyword(v)
|
||||||
|
setTags([])
|
||||||
|
setError(null)
|
||||||
|
if (v.length !== 0) {
|
||||||
|
setLoading(true)
|
||||||
|
debounce.current.run(() => searchTags(v))
|
||||||
|
} else {
|
||||||
|
setLoading(false)
|
||||||
|
debounce.current.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateTag = async (name: string) => {
|
||||||
|
setLoading(true)
|
||||||
|
const res = await network.createTag(name)
|
||||||
|
if (!res.success) {
|
||||||
|
setError(res.message)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onAdd(res.data!)
|
||||||
|
setKeyword("")
|
||||||
|
setTags([])
|
||||||
|
setLoading(false)
|
||||||
|
const input = document.getElementById("search_tags_input") as HTMLInputElement
|
||||||
|
input.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
let dropdownContent
|
||||||
|
if (error) {
|
||||||
|
dropdownContent = <div className="alert alert-error my-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
} else if (!keyword) {
|
||||||
|
dropdownContent = <div className="flex flex-row py-2 px-4">
|
||||||
|
<LuInfo size={20} />
|
||||||
|
<span className={"w-2"} />
|
||||||
|
<span className={"flex-1"}>{t("Please enter a search keyword")}</span>
|
||||||
|
</div>
|
||||||
|
} else if (isLoading) {
|
||||||
|
dropdownContent = <div className="flex flex-row py-2 px-4">
|
||||||
|
<span className={"loading loading-spinner loading-sm"}></span>
|
||||||
|
<span className={"w-2"} />
|
||||||
|
<span className={"flex-1"}>{t("Searching...")}</span>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
const haveExactMatch = tags.find((t) => t.name === keyword) !== undefined
|
||||||
|
dropdownContent = <>
|
||||||
|
{
|
||||||
|
tags.map((t) => {
|
||||||
|
return <li key={t.id} onClick={() => {
|
||||||
|
onAdd(t);
|
||||||
|
setKeyword("")
|
||||||
|
setTags([])
|
||||||
|
const input = document.getElementById("search_tags_input") as HTMLInputElement
|
||||||
|
input.blur()
|
||||||
|
}}><a>{t.name}</a></li>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!haveExactMatch && <li onClick={() => {
|
||||||
|
handleCreateTag(keyword)
|
||||||
|
}}><a>{t("Create Tag")}: {keyword}</a></li>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={"dropdown dropdown-end"}>
|
||||||
|
<label className="input">
|
||||||
|
<MdSearch size={18}/>
|
||||||
|
<input autoComplete={"off"} id={"search_tags_input"} tabIndex={0} type="text" className="grow" placeholder={t("Search Tags")} value={keyword} onChange={(e) => handleChange(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow mt-2 border border-base-300">
|
||||||
|
{dropdownContent}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
class Debounce {
|
||||||
|
private timer: number | null = null
|
||||||
|
private readonly delay: number
|
||||||
|
|
||||||
|
constructor(delay: number) {
|
||||||
|
this.delay = delay
|
||||||
|
}
|
||||||
|
|
||||||
|
run(callback: () => void) {
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
}
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
callback()
|
||||||
|
}, this.delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -31,6 +31,8 @@ export interface Tag {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
type: string;
|
||||||
|
aliases: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateResourceParams {
|
export interface CreateResourceParams {
|
||||||
|
@@ -284,11 +284,12 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchTags(keyword: string): Promise<Response<Tag[]>> {
|
async searchTags(keyword: string, mainTag?: boolean): Promise<Response<Tag[]>> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.apiBaseUrl}/tag/search`, {
|
const response = await axios.get(`${this.apiBaseUrl}/tag/search`, {
|
||||||
params: {
|
params: {
|
||||||
keyword
|
keyword,
|
||||||
|
mainTag
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
@@ -329,10 +330,12 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setTagDescription(tagId: number, description: string): Promise<Response<Tag>> {
|
async setTagInfo(tagId: number, description: string, aliasOf: number | null, type: string): Promise<Response<Tag>> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.putForm(`${this.apiBaseUrl}/tag/${tagId}/description`, {
|
const response = await axios.putForm(`${this.apiBaseUrl}/tag/${tagId}/info`, {
|
||||||
description
|
description,
|
||||||
|
alias_of: aliasOf,
|
||||||
|
type,
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {MdAdd, MdClose, MdDelete, MdOutlineInfo, MdSearch} from "react-icons/md";
|
import {MdAdd, MdClose, MdDelete, MdOutlineInfo} from "react-icons/md";
|
||||||
import { Tag } from "../network/models.ts";
|
import { Tag } from "../network/models.ts";
|
||||||
import { network } from "../network/network.ts";
|
import { network } from "../network/network.ts";
|
||||||
import { LuInfo } from "react-icons/lu";
|
|
||||||
import {useNavigate, useParams} from "react-router";
|
import {useNavigate, useParams} from "react-router";
|
||||||
import showToast from "../components/toast.ts";
|
import showToast from "../components/toast.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
||||||
|
|
||||||
export default function EditResourcePage() {
|
export default function EditResourcePage() {
|
||||||
const [title, setTitle] = useState<string>("")
|
const [title, setTitle] = useState<string>("")
|
||||||
@@ -256,139 +256,3 @@ export default function EditResourcePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function TagInput({ onAdd }: { onAdd: (tag: Tag) => void }) {
|
|
||||||
const [keyword, setKeyword] = useState<string>("")
|
|
||||||
const [tags, setTags] = useState<Tag[]>([])
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [isLoading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
const debounce = useRef(new Debounce(500))
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const searchTags = async (keyword: string) => {
|
|
||||||
if (keyword.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLoading(true)
|
|
||||||
setTags([])
|
|
||||||
setError(null)
|
|
||||||
const res = await network.searchTags(keyword)
|
|
||||||
if (!res.success) {
|
|
||||||
setError(res.message)
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setTags(res.data!)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = async (v: string) => {
|
|
||||||
setKeyword(v)
|
|
||||||
setTags([])
|
|
||||||
setError(null)
|
|
||||||
if (v.length !== 0) {
|
|
||||||
setLoading(true)
|
|
||||||
debounce.current.run(() => searchTags(v))
|
|
||||||
} else {
|
|
||||||
setLoading(false)
|
|
||||||
debounce.current.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreateTag = async (name: string) => {
|
|
||||||
setLoading(true)
|
|
||||||
const res = await network.createTag(name)
|
|
||||||
if (!res.success) {
|
|
||||||
setError(res.message)
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onAdd(res.data!)
|
|
||||||
setKeyword("")
|
|
||||||
setTags([])
|
|
||||||
setLoading(false)
|
|
||||||
const input = document.getElementById("search_tags_input") as HTMLInputElement
|
|
||||||
input.blur()
|
|
||||||
}
|
|
||||||
|
|
||||||
let dropdownContent
|
|
||||||
if (error) {
|
|
||||||
dropdownContent = <div className="alert alert-error my-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
} else if (!keyword) {
|
|
||||||
dropdownContent = <div className="flex flex-row py-2 px-4">
|
|
||||||
<LuInfo size={20} />
|
|
||||||
<span className={"w-2"} />
|
|
||||||
<span className={"flex-1"}>{t("Please enter a search keyword")}</span>
|
|
||||||
</div>
|
|
||||||
} else if (isLoading) {
|
|
||||||
dropdownContent = <div className="flex flex-row py-2 px-4">
|
|
||||||
<span className={"loading loading-spinner loading-sm"}></span>
|
|
||||||
<span className={"w-2"} />
|
|
||||||
<span className={"flex-1"}>{t("Searching...")}</span>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
const haveExactMatch = tags.find((t) => t.name === keyword) !== undefined
|
|
||||||
dropdownContent = <>
|
|
||||||
{
|
|
||||||
tags.map((t) => {
|
|
||||||
return <li key={t.id} onClick={() => {
|
|
||||||
onAdd(t);
|
|
||||||
setKeyword("")
|
|
||||||
setTags([])
|
|
||||||
const input = document.getElementById("search_tags_input") as HTMLInputElement
|
|
||||||
input.blur()
|
|
||||||
}}><a>{t.name}</a></li>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!haveExactMatch && <li onClick={() => {
|
|
||||||
handleCreateTag(keyword)
|
|
||||||
}}><a>{t("Create Tag")}: {keyword}</a></li>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={"dropdown dropdown-end"}>
|
|
||||||
<label className="input">
|
|
||||||
<MdSearch size={18}/>
|
|
||||||
<input autoComplete={"off"} id={"search_tags_input"} tabIndex={0} type="text" className="grow" placeholder={t("Search Tags")} value={keyword} onChange={(e) => handleChange(e.target.value)} />
|
|
||||||
</label>
|
|
||||||
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow mt-2 border border-base-300">
|
|
||||||
{dropdownContent}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
class Debounce {
|
|
||||||
private timer: number | null = null
|
|
||||||
private readonly delay: number
|
|
||||||
|
|
||||||
constructor(delay: number) {
|
|
||||||
this.delay = delay
|
|
||||||
}
|
|
||||||
|
|
||||||
run(callback: () => void) {
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer)
|
|
||||||
}
|
|
||||||
this.timer = setTimeout(() => {
|
|
||||||
callback()
|
|
||||||
}, this.delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer)
|
|
||||||
this.timer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {MdAdd, MdClose, MdDelete, MdOutlineInfo, MdSearch} from "react-icons/md";
|
import {MdAdd, MdClose, MdDelete, MdOutlineInfo} from "react-icons/md";
|
||||||
import { Tag } from "../network/models.ts";
|
import { Tag } from "../network/models.ts";
|
||||||
import { network } from "../network/network.ts";
|
import { network } from "../network/network.ts";
|
||||||
import { LuInfo } from "react-icons/lu";
|
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import showToast from "../components/toast.ts";
|
import showToast from "../components/toast.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
||||||
|
|
||||||
export default function PublishPage() {
|
export default function PublishPage() {
|
||||||
const [title, setTitle] = useState<string>("")
|
const [title, setTitle] = useState<string>("")
|
||||||
@@ -232,139 +232,3 @@ export default function PublishPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function TagInput({ onAdd }: { onAdd: (tag: Tag) => void }) {
|
|
||||||
const [keyword, setKeyword] = useState<string>("")
|
|
||||||
const [tags, setTags] = useState<Tag[]>([])
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [isLoading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
const debounce = useRef(new Debounce(500))
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const searchTags = async (keyword: string) => {
|
|
||||||
if (keyword.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLoading(true)
|
|
||||||
setTags([])
|
|
||||||
setError(null)
|
|
||||||
const res = await network.searchTags(keyword)
|
|
||||||
if (!res.success) {
|
|
||||||
setError(res.message)
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setTags(res.data!)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = async (v: string) => {
|
|
||||||
setKeyword(v)
|
|
||||||
setTags([])
|
|
||||||
setError(null)
|
|
||||||
if (v.length !== 0) {
|
|
||||||
setLoading(true)
|
|
||||||
debounce.current.run(() => searchTags(v))
|
|
||||||
} else {
|
|
||||||
setLoading(false)
|
|
||||||
debounce.current.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreateTag = async (name: string) => {
|
|
||||||
setLoading(true)
|
|
||||||
const res = await network.createTag(name)
|
|
||||||
if (!res.success) {
|
|
||||||
setError(res.message)
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onAdd(res.data!)
|
|
||||||
setKeyword("")
|
|
||||||
setTags([])
|
|
||||||
setLoading(false)
|
|
||||||
const input = document.getElementById("search_tags_input") as HTMLInputElement
|
|
||||||
input.blur()
|
|
||||||
}
|
|
||||||
|
|
||||||
let dropdownContent
|
|
||||||
if (error) {
|
|
||||||
dropdownContent = <div className="alert alert-error my-2">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
} else if (!keyword) {
|
|
||||||
dropdownContent = <div className="flex flex-row py-2 px-4">
|
|
||||||
<LuInfo size={20} />
|
|
||||||
<span className={"w-2"} />
|
|
||||||
<span className={"flex-1"}>{t("Please enter a search keyword")}</span>
|
|
||||||
</div>
|
|
||||||
} else if (isLoading) {
|
|
||||||
dropdownContent = <div className="flex flex-row py-2 px-4">
|
|
||||||
<span className={"loading loading-spinner loading-sm"}></span>
|
|
||||||
<span className={"w-2"} />
|
|
||||||
<span className={"flex-1"}>{t("Searching...")}</span>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
const haveExactMatch = tags.find((t) => t.name === keyword) !== undefined
|
|
||||||
dropdownContent = <>
|
|
||||||
{
|
|
||||||
tags.map((t) => {
|
|
||||||
return <li key={t.id} onClick={() => {
|
|
||||||
onAdd(t);
|
|
||||||
setKeyword("")
|
|
||||||
setTags([])
|
|
||||||
const input = document.getElementById("search_tags_input") as HTMLInputElement
|
|
||||||
input.blur()
|
|
||||||
}}><a>{t.name}</a></li>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!haveExactMatch && <li onClick={() => {
|
|
||||||
handleCreateTag(keyword)
|
|
||||||
}}><a>{t("Create Tag")}: {keyword}</a></li>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={"dropdown dropdown-end"}>
|
|
||||||
<label className="input">
|
|
||||||
<MdSearch size={18}/>
|
|
||||||
<input autoComplete={"off"} id={"search_tags_input"} tabIndex={0} type="text" className="grow" placeholder={t("Search Tags")} value={keyword} onChange={(e) => handleChange(e.target.value)} />
|
|
||||||
</label>
|
|
||||||
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow mt-2 border border-base-300">
|
|
||||||
{dropdownContent}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
class Debounce {
|
|
||||||
private timer: number | null = null
|
|
||||||
private readonly delay: number
|
|
||||||
|
|
||||||
constructor(delay: number) {
|
|
||||||
this.delay = delay
|
|
||||||
}
|
|
||||||
|
|
||||||
run(callback: () => void) {
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer)
|
|
||||||
}
|
|
||||||
this.timer = setTimeout(() => {
|
|
||||||
callback()
|
|
||||||
}, this.delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer)
|
|
||||||
this.timer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -8,6 +8,9 @@ import {Tag} from "../network/models.ts";
|
|||||||
import Button from "../components/button.tsx";
|
import Button from "../components/button.tsx";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import {app} from "../app.ts";
|
import {app} from "../app.ts";
|
||||||
|
import Input, {TextArea} from "../components/input.tsx";
|
||||||
|
import TagInput from "../components/tag_input.tsx";
|
||||||
|
import Badge from "../components/badge.tsx";
|
||||||
|
|
||||||
export default function TaggedResourcesPage() {
|
export default function TaggedResourcesPage() {
|
||||||
const { tag: tagName } = useParams()
|
const { tag: tagName } = useParams()
|
||||||
@@ -40,7 +43,7 @@ export default function TaggedResourcesPage() {
|
|||||||
return <div>
|
return <div>
|
||||||
<div className={"flex items-center"}>
|
<div className={"flex items-center"}>
|
||||||
<h1 className={"text-2xl pt-6 pb-2 px-4 font-bold flex-1"}>
|
<h1 className={"text-2xl pt-6 pb-2 px-4 font-bold flex-1"}>
|
||||||
{tagName}
|
{tag?.name ?? tagName}
|
||||||
</h1>
|
</h1>
|
||||||
{
|
{
|
||||||
tag && <EditTagButton tag={tag} onEdited={(t) => {
|
tag && <EditTagButton tag={tag} onEdited={(t) => {
|
||||||
@@ -48,6 +51,14 @@ export default function TaggedResourcesPage() {
|
|||||||
}} />
|
}} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{tag?.type && <h2 className={"text-base-content/60 ml-2 text-xl pl-2 mb-2"}>{tag.type}</h2>}
|
||||||
|
<div className={"px-3"}>
|
||||||
|
{
|
||||||
|
(tag?.aliases ?? []).map((e) => {
|
||||||
|
return <Badge className={"m-1 badge-outline badge-soft"}>{e}</Badge>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
(tag?.description && app.canUpload()) && <article className={"px-4 py-2"}>
|
(tag?.description && app.canUpload()) && <article className={"px-4 py-2"}>
|
||||||
<Markdown>
|
<Markdown>
|
||||||
@@ -63,6 +74,9 @@ export default function TaggedResourcesPage() {
|
|||||||
|
|
||||||
function EditTagButton({tag, onEdited}: { tag: Tag, onEdited: (t: Tag) => void }) {
|
function EditTagButton({tag, onEdited}: { tag: Tag, onEdited: (t: Tag) => void }) {
|
||||||
const [description, setDescription] = useState(tag.description);
|
const [description, setDescription] = useState(tag.description);
|
||||||
|
const [isAlias, setIsAlias] = useState(false);
|
||||||
|
const [aliasOf, setAliasOf] = useState<Tag | null>(null);
|
||||||
|
const [type, setType] = useState(tag.type);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -72,16 +86,13 @@ function EditTagButton({tag, onEdited}: { tag: Tag, onEdited: (t: Tag) => void }
|
|||||||
}, [tag.description]);
|
}, [tag.description]);
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (description === tag.description) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (description && description.length > 256) {
|
if (description && description.length > 256) {
|
||||||
setError(t("Description is too long"));
|
setError(t("Description is too long"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const res = await network.setTagDescription(tag.id, description);
|
const res = await network.setTagInfo(tag.id, description, aliasOf?.id ?? null, type);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
const dialog = document.getElementById("edit_tag_dialog") as HTMLDialogElement;
|
const dialog = document.getElementById("edit_tag_dialog") as HTMLDialogElement;
|
||||||
@@ -98,11 +109,34 @@ function EditTagButton({tag, onEdited}: { tag: Tag, onEdited: (t: Tag) => void }
|
|||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
}}>{t("Edit")}</Button>
|
}}>{t("Edit")}</Button>
|
||||||
<dialog id="edit_tag_dialog" className="modal">
|
<dialog id="edit_tag_dialog" className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box" style={{
|
||||||
|
overflowY: "initial"
|
||||||
|
}}>
|
||||||
<h3 className="font-bold text-lg">{t("Edit Tag")}</h3>
|
<h3 className="font-bold text-lg">{t("Edit Tag")}</h3>
|
||||||
<p className="py-2 text-sm">{t("Set the description of the tag.")}</p>
|
<div className={"flex py-3"}>
|
||||||
<p className="pb-3 text-sm">{t("Use markdown format.")}</p>
|
<p className={"flex-1"}>The tag is an alias of another tag</p>
|
||||||
<textarea className="textarea h-24 w-full resize-none" value={description} onChange={(e) => setDescription(e.target.value)}/>
|
<input type="checkbox" className="toggle toggle-primary" checked={isAlias} onChange={(e) => {
|
||||||
|
setIsAlias(e.target.checked);
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isAlias ? <>
|
||||||
|
{
|
||||||
|
aliasOf && <div className={"py-2 border border-base-300 rounded-3xl mt-2 px-4 flex mb-4"}>
|
||||||
|
<p className={"flex-1"}>Alias Of: </p>
|
||||||
|
<Badge>{aliasOf.name}</Badge>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<TagInput mainTag={true} onAdd={(tag: Tag) => {
|
||||||
|
setAliasOf(tag);
|
||||||
|
}}/>
|
||||||
|
</> : <>
|
||||||
|
<Input value={type} onChange={(e) => setType(e.target.value)} label={"Type"}/>
|
||||||
|
<TextArea label={"Description"} value={description} onChange={(e) => setDescription(e.target.value)}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
{error && <ErrorAlert className={"mt-2"} message={error} />}
|
{error && <ErrorAlert className={"mt-2"} message={error} />}
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
|
@@ -36,7 +36,9 @@ func handleSearchTag(c fiber.Ctx) error {
|
|||||||
return model.NewRequestError("Keyword is required")
|
return model.NewRequestError("Keyword is required")
|
||||||
}
|
}
|
||||||
keyword = strings.TrimSpace(keyword)
|
keyword = strings.TrimSpace(keyword)
|
||||||
tags, err := service.SearchTag(keyword)
|
mainTagStr := c.Query("main_tag")
|
||||||
|
mainTag := mainTagStr == "true" || mainTagStr == "1"
|
||||||
|
tags, err := service.SearchTag(keyword, mainTag)
|
||||||
if tags == nil {
|
if tags == nil {
|
||||||
tags = []model.TagView{}
|
tags = []model.TagView{}
|
||||||
}
|
}
|
||||||
@@ -66,7 +68,7 @@ func handleDeleteTag(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSetTagDescription(c fiber.Ctx) error {
|
func handleSetTagInfo(c fiber.Ctx) error {
|
||||||
uid, ok := c.Locals("uid").(uint)
|
uid, ok := c.Locals("uid").(uint)
|
||||||
if !ok {
|
if !ok {
|
||||||
return model.NewUnAuthorizedError("You must be logged in to set tag description")
|
return model.NewUnAuthorizedError("You must be logged in to set tag description")
|
||||||
@@ -76,11 +78,19 @@ func handleSetTagDescription(c fiber.Ctx) error {
|
|||||||
return model.NewRequestError("Invalid tag ID")
|
return model.NewRequestError("Invalid tag ID")
|
||||||
}
|
}
|
||||||
description := c.FormValue("description")
|
description := c.FormValue("description")
|
||||||
if description == "" {
|
|
||||||
return model.NewRequestError("Description is required")
|
|
||||||
}
|
|
||||||
description = strings.TrimSpace(description)
|
description = strings.TrimSpace(description)
|
||||||
t, err := service.SetTagDescription(uid, uint(id), description)
|
aliasOfStr := c.FormValue("alias_of")
|
||||||
|
var aliasOf *uint
|
||||||
|
if aliasOfStr != "" {
|
||||||
|
aliasID, err := strconv.Atoi(aliasOfStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewRequestError("Invalid alias ID")
|
||||||
|
}
|
||||||
|
aliasUint := uint(aliasID)
|
||||||
|
aliasOf = &aliasUint
|
||||||
|
}
|
||||||
|
tagType := c.FormValue("type")
|
||||||
|
t, err := service.SetTagInfo(uid, uint(id), description, aliasOf, tagType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -118,7 +128,7 @@ func AddTagRoutes(api fiber.Router) {
|
|||||||
tag.Post("/", handleCreateTag)
|
tag.Post("/", handleCreateTag)
|
||||||
tag.Get("/search", handleSearchTag)
|
tag.Get("/search", handleSearchTag)
|
||||||
tag.Delete("/:id", handleDeleteTag)
|
tag.Delete("/:id", handleDeleteTag)
|
||||||
tag.Put("/:id/description", handleSetTagDescription)
|
tag.Put("/:id/info", handleSetTagInfo)
|
||||||
tag.Get("/:name", handleGetTagByName)
|
tag.Get("/:name", handleGetTagByName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -180,20 +180,37 @@ func searchWithKeyword(keyword string) ([]model.Resource, error) {
|
|||||||
}
|
}
|
||||||
if len([]rune(keyword)) < 20 {
|
if len([]rune(keyword)) < 20 {
|
||||||
var tag model.Tag
|
var tag model.Tag
|
||||||
if err := db.Where("name = ?", keyword).First(&tag).Error; err != nil {
|
var err error
|
||||||
|
if tag, err = GetTagByName(keyword); err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := db.Model(&tag).Preload("Resources").Find(&tag).Error; err != nil {
|
if tag.AliasOf != nil {
|
||||||
|
tag, err = GetTagByID(*tag.AliasOf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var tagIds []uint
|
||||||
|
tagIds = append(tagIds, tag.ID)
|
||||||
|
for _, alias := range tag.Aliases {
|
||||||
|
tagIds = append(tagIds, alias.ID)
|
||||||
|
}
|
||||||
|
var resources []model.Resource
|
||||||
|
subQuery := db.Table("resource_tags").
|
||||||
|
Select("resource_id").
|
||||||
|
Where("tag_id IN ?", tagIds).
|
||||||
|
Group("resource_id")
|
||||||
|
if err := db.Where("id IN (?)", subQuery).Select("id", "title", "alternative_titles").Preload("Tags").Find(&resources).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return tag.Resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len([]rune(keyword)) < 80 {
|
if len([]rune(keyword)) < 80 {
|
||||||
var resources []model.Resource
|
var resources []model.Resource
|
||||||
if err := db.Where("title LIKE ?", "%"+keyword+"%").Or("alternative_titles LIKE ?", "%"+keyword+"%").Find(&resources).Error; err != nil {
|
if err := db.Where("title LIKE ?", "%"+keyword+"%").Or("alternative_titles LIKE ?", "%"+keyword+"%").Select("id", "title", "alternative_titles").Preload("Tags").Find(&resources).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return resources, nil
|
return resources, nil
|
||||||
@@ -202,23 +219,53 @@ func searchWithKeyword(keyword string) ([]model.Resource, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetResourceByTag(tagID uint, page int, pageSize int) ([]model.Resource, int, error) {
|
func GetResourceByTag(tagID uint, page int, pageSize int) ([]model.Resource, int, error) {
|
||||||
var tag model.Tag
|
tag, err := GetTagByID(tagID)
|
||||||
|
if err != nil {
|
||||||
total := db.Model(&model.Tag{
|
|
||||||
Model: gorm.Model{
|
|
||||||
ID: tagID,
|
|
||||||
},
|
|
||||||
}).Association("Resources").Count()
|
|
||||||
|
|
||||||
if err := db.Model(&model.Tag{}).Where("id = ?", tagID).Preload("Resources", func(tx *gorm.DB) *gorm.DB {
|
|
||||||
return tx.Offset((page - 1) * pageSize).Limit(pageSize).Preload("Tags").Preload("User").Preload("Images").Order("created_at DESC")
|
|
||||||
}).First(&tag).Error; err != nil {
|
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := (int(total) + pageSize - 1) / pageSize
|
if tag.AliasOf != nil {
|
||||||
|
tag, err = GetTagByID(*tag.AliasOf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tag.Resources, totalPages, nil
|
var tagIds []uint
|
||||||
|
tagIds = append(tagIds, tag.ID)
|
||||||
|
for _, alias := range tag.Aliases {
|
||||||
|
tagIds = append(tagIds, alias.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resources []model.Resource
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
subQuery := db.Table("resource_tags").
|
||||||
|
Select("resource_id").
|
||||||
|
Where("tag_id IN ?", tagIds).
|
||||||
|
Group("resource_id")
|
||||||
|
|
||||||
|
if err := db.Model(&model.Resource{}).
|
||||||
|
Where("id IN (?)", subQuery).
|
||||||
|
Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Where("id IN (?)", subQuery).
|
||||||
|
Offset((page - 1) * pageSize).
|
||||||
|
Limit(pageSize).
|
||||||
|
Preload("User").
|
||||||
|
Preload("Images").
|
||||||
|
Preload("Tags").
|
||||||
|
Preload("Files").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&resources).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
|
||||||
|
|
||||||
|
return resources, int(totalPages), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExistsResource(id uint) (bool, error) {
|
func ExistsResource(id uint) (bool, error) {
|
||||||
|
@@ -16,10 +16,14 @@ func CreateTag(tag string) (model.Tag, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchTag(keyword string) ([]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
|
||||||
if err := db.Model(&model.Tag{}).Where("name Like ?", "%"+keyword+"%").Limit(10).Find(&t).Error; err != nil {
|
query := db.Model(&model.Tag{}).Where("name Like ?", "%"+keyword+"%")
|
||||||
|
if mainTag {
|
||||||
|
query = query.Where("alias_of IS NULL")
|
||||||
|
}
|
||||||
|
if err := query.Limit(10).Find(&t).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
@@ -38,7 +42,7 @@ func DeleteTag(id uint) error {
|
|||||||
func GetTagByID(id uint) (model.Tag, error) {
|
func GetTagByID(id uint) (model.Tag, error) {
|
||||||
// Retrieve a tag by its ID from the database
|
// Retrieve a tag by its ID from the database
|
||||||
var t model.Tag
|
var t model.Tag
|
||||||
if err := db.First(&t, id).Error; err != nil {
|
if err := db.Preload("Aliases").First(&t, id).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return model.Tag{}, model.NewNotFoundError("Tag not found")
|
return model.Tag{}, model.NewNotFoundError("Tag not found")
|
||||||
}
|
}
|
||||||
@@ -50,7 +54,7 @@ func GetTagByID(id uint) (model.Tag, error) {
|
|||||||
func GetTagByName(name string) (model.Tag, error) {
|
func GetTagByName(name string) (model.Tag, error) {
|
||||||
// Retrieve a tag by its name from the database
|
// Retrieve a tag by its name from the database
|
||||||
var t model.Tag
|
var t model.Tag
|
||||||
if err := db.Where("name = ?", name).First(&t).Error; err != nil {
|
if err := db.Preload("Aliases").Where("name = ?", name).First(&t).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return model.Tag{}, model.NewNotFoundError("Tag not found")
|
return model.Tag{}, model.NewNotFoundError("Tag not found")
|
||||||
}
|
}
|
||||||
@@ -59,8 +63,14 @@ func GetTagByName(name string) (model.Tag, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetTagDescription(id uint, description string) error {
|
func SetTagInfo(id uint, description string, aliasOf *uint, tagType string) error {
|
||||||
if err := db.Model(model.Tag{}).Where("id = ?", id).Update("description", description).Error; err != nil {
|
t := model.Tag{Model: gorm.Model{
|
||||||
|
ID: id,
|
||||||
|
}, Description: description, Type: tagType, AliasOf: aliasOf}
|
||||||
|
if err := db.Model(&t).Updates(t).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return model.NewNotFoundError("Tag not found")
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@@ -6,19 +6,30 @@ type Tag struct {
|
|||||||
gorm.Model
|
gorm.Model
|
||||||
Name string `gorm:"unique"`
|
Name string `gorm:"unique"`
|
||||||
Description string
|
Description string
|
||||||
|
AliasOf *uint `gorm:"default:NULL"` // Foreign key for aliasing, can be NULL
|
||||||
|
Type string
|
||||||
Resources []Resource `gorm:"many2many:resource_tags;"`
|
Resources []Resource `gorm:"many2many:resource_tags;"`
|
||||||
|
Aliases []Tag `gorm:"foreignKey:AliasOf;references:ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TagView struct {
|
type TagView struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Aliases []string `json:"aliases"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tag) ToView() *TagView {
|
func (t *Tag) ToView() *TagView {
|
||||||
|
aliases := make([]string, 0, len(t.Aliases))
|
||||||
|
for _, alias := range t.Aliases {
|
||||||
|
aliases = append(aliases, alias.Name)
|
||||||
|
}
|
||||||
return &TagView{
|
return &TagView{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Description: t.Description,
|
Description: t.Description,
|
||||||
|
Type: t.Type,
|
||||||
|
Aliases: aliases,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,12 @@ func GetTag(id uint) (*model.TagView, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if t.AliasOf != nil {
|
||||||
|
t, err = dao.GetTagByID(*t.AliasOf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return t.ToView(), nil
|
return t.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,11 +41,17 @@ func GetTagByName(name string) (*model.TagView, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if t.AliasOf != nil {
|
||||||
|
t, err = dao.GetTagByID(*t.AliasOf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return t.ToView(), nil
|
return t.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchTag(name string) ([]model.TagView, error) {
|
func SearchTag(name string, mainTag bool) ([]model.TagView, error) {
|
||||||
tags, err := dao.SearchTag(name)
|
tags, err := dao.SearchTag(name, mainTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -54,7 +66,7 @@ func DeleteTag(id uint) error {
|
|||||||
return dao.DeleteTag(id)
|
return dao.DeleteTag(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetTagDescription(uid uint, id uint, description string) (*model.TagView, error) {
|
func SetTagInfo(uid uint, id uint, description string, aliasOf *uint, tagType string) (*model.TagView, error) {
|
||||||
canUpload, err := checkUserCanUpload(uid)
|
canUpload, err := checkUserCanUpload(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error checking user permissions:", err)
|
log.Error("Error checking user permissions:", err)
|
||||||
@@ -63,13 +75,18 @@ func SetTagDescription(uid uint, id uint, description string) (*model.TagView, e
|
|||||||
if !canUpload {
|
if !canUpload {
|
||||||
return nil, model.NewUnAuthorizedError("User cannot set tag description")
|
return nil, model.NewUnAuthorizedError("User cannot set tag description")
|
||||||
}
|
}
|
||||||
|
if err := dao.SetTagInfo(id, description, aliasOf, tagType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
t, err := dao.GetTagByID(id)
|
t, err := dao.GetTagByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.Description = description
|
if t.AliasOf != nil {
|
||||||
if err := dao.SetTagDescription(id, description); err != nil {
|
t, err = dao.GetTagByID(*t.AliasOf)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return t.ToView(), nil
|
return t.ToView(), nil
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user