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";
|
||||
|
||||
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 }) {
|
||||
|
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;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
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 {
|
||||
const response = await axios.get(`${this.apiBaseUrl}/tag/search`, {
|
||||
params: {
|
||||
keyword
|
||||
keyword,
|
||||
mainTag
|
||||
}
|
||||
})
|
||||
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 {
|
||||
const response = await axios.putForm(`${this.apiBaseUrl}/tag/${tagId}/description`, {
|
||||
description
|
||||
const response = await axios.putForm(`${this.apiBaseUrl}/tag/${tagId}/info`, {
|
||||
description,
|
||||
alias_of: aliasOf,
|
||||
type,
|
||||
})
|
||||
return response.data
|
||||
} catch (e: any) {
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {MdAdd, MdClose, MdDelete, MdOutlineInfo, MdSearch} from "react-icons/md";
|
||||
import { useEffect, useState } from "react";
|
||||
import {MdAdd, MdClose, MdDelete, MdOutlineInfo} from "react-icons/md";
|
||||
import { Tag } from "../network/models.ts";
|
||||
import { network } from "../network/network.ts";
|
||||
import { LuInfo } from "react-icons/lu";
|
||||
import {useNavigate, useParams} from "react-router";
|
||||
import showToast from "../components/toast.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { app } from "../app.ts";
|
||||
import { ErrorAlert } from "../components/alert.tsx";
|
||||
import Loading from "../components/loading.tsx";
|
||||
import TagInput from "../components/tag_input.tsx";
|
||||
|
||||
export default function EditResourcePage() {
|
||||
const [title, setTitle] = useState<string>("")
|
||||
@@ -256,139 +256,3 @@ export default function EditResourcePage() {
|
||||
</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 {MdAdd, MdClose, MdDelete, MdOutlineInfo, MdSearch} from "react-icons/md";
|
||||
import { useEffect, useState } from "react";
|
||||
import {MdAdd, MdClose, MdDelete, MdOutlineInfo} from "react-icons/md";
|
||||
import { Tag } from "../network/models.ts";
|
||||
import { network } from "../network/network.ts";
|
||||
import { LuInfo } from "react-icons/lu";
|
||||
import { useNavigate } from "react-router";
|
||||
import showToast from "../components/toast.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { app } from "../app.ts";
|
||||
import { ErrorAlert } from "../components/alert.tsx";
|
||||
import {useAppContext} from "../components/AppContext.tsx";
|
||||
import TagInput from "../components/tag_input.tsx";
|
||||
|
||||
export default function PublishPage() {
|
||||
const [title, setTitle] = useState<string>("")
|
||||
@@ -231,140 +231,4 @@ export default function PublishPage() {
|
||||
</button>
|
||||
</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 Markdown from "react-markdown";
|
||||
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() {
|
||||
const { tag: tagName } = useParams()
|
||||
@@ -40,7 +43,7 @@ export default function TaggedResourcesPage() {
|
||||
return <div>
|
||||
<div className={"flex items-center"}>
|
||||
<h1 className={"text-2xl pt-6 pb-2 px-4 font-bold flex-1"}>
|
||||
{tagName}
|
||||
{tag?.name ?? tagName}
|
||||
</h1>
|
||||
{
|
||||
tag && <EditTagButton tag={tag} onEdited={(t) => {
|
||||
@@ -48,6 +51,14 @@ export default function TaggedResourcesPage() {
|
||||
}} />
|
||||
}
|
||||
</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"}>
|
||||
<Markdown>
|
||||
@@ -63,6 +74,9 @@ export default function TaggedResourcesPage() {
|
||||
|
||||
function EditTagButton({tag, onEdited}: { tag: Tag, onEdited: (t: Tag) => void }) {
|
||||
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 [error, setError] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
@@ -72,16 +86,13 @@ function EditTagButton({tag, onEdited}: { tag: Tag, onEdited: (t: Tag) => void }
|
||||
}, [tag.description]);
|
||||
|
||||
const submit = async () => {
|
||||
if (description === tag.description) {
|
||||
return;
|
||||
}
|
||||
if (description && description.length > 256) {
|
||||
setError(t("Description is too long"));
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const res = await network.setTagDescription(tag.id, description);
|
||||
const res = await network.setTagInfo(tag.id, description, aliasOf?.id ?? null, type);
|
||||
setIsLoading(false);
|
||||
if (res.success) {
|
||||
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();
|
||||
}}>{t("Edit")}</Button>
|
||||
<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>
|
||||
<p className="py-2 text-sm">{t("Set the description of the tag.")}</p>
|
||||
<p className="pb-3 text-sm">{t("Use markdown format.")}</p>
|
||||
<textarea className="textarea h-24 w-full resize-none" value={description} onChange={(e) => setDescription(e.target.value)}/>
|
||||
<div className={"flex py-3"}>
|
||||
<p className={"flex-1"}>The tag is an alias of another tag</p>
|
||||
<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} />}
|
||||
<div className="modal-action">
|
||||
<form method="dialog">
|
||||
|
Reference in New Issue
Block a user