mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 04:17:23 +00:00
Add tags page.
This commit is contained in:
@@ -11,6 +11,7 @@ import TaggedResourcesPage from "./pages/tagged_resources_page.tsx";
|
|||||||
import UserPage from "./pages/user_page.tsx";
|
import UserPage from "./pages/user_page.tsx";
|
||||||
import EditResourcePage from "./pages/edit_resource_page.tsx";
|
import EditResourcePage from "./pages/edit_resource_page.tsx";
|
||||||
import AboutPage from "./pages/about_page.tsx";
|
import AboutPage from "./pages/about_page.tsx";
|
||||||
|
import TagsPage from "./pages/tags_page.tsx";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -28,6 +29,7 @@ export default function App() {
|
|||||||
<Route path={"/user/:username"} element={<UserPage/>}/>
|
<Route path={"/user/:username"} element={<UserPage/>}/>
|
||||||
<Route path={"/resource/edit/:rid"} element={<EditResourcePage/>}/>
|
<Route path={"/resource/edit/:rid"} element={<EditResourcePage/>}/>
|
||||||
<Route path={"/about"} element={<AboutPage/>}/>
|
<Route path={"/about"} element={<AboutPage/>}/>
|
||||||
|
<Route path={"/tags"} element={<TagsPage/>}/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
@@ -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 ${!className?.includes("badge-") && "badge-primary"} ${className}`} onClick={onClick}>{children}</span>
|
return <span className={`badge ${!className?.includes("badge-") && "badge-primary"} select-none ${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 }) {
|
||||||
|
@@ -23,36 +23,77 @@ export default function Navigator() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className="navbar bg-base-100 shadow-sm fixed top-0 z-1 lg:z-10" key={key}>
|
<div className="navbar bg-base-100 shadow-sm fixed top-0 z-1 lg:z-10" key={key}>
|
||||||
<div className={"flex-1 max-w-7xl mx-auto flex"}>
|
<div className={"flex-1 max-w-7xl mx-auto flex items-center"}>
|
||||||
<div className="flex-1">
|
<div className="dropdown">
|
||||||
|
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle lg:hidden">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h7" /> </svg>
|
||||||
|
</div>
|
||||||
|
<ul id={"navi_menu"}
|
||||||
|
tabIndex={0}
|
||||||
|
className="menu menu-md dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
|
||||||
|
<li onClick={() => {
|
||||||
|
const menu = document.getElementById("navi_menu") as HTMLElement;
|
||||||
|
menu.blur();
|
||||||
|
navigate("/");
|
||||||
|
}}><a>{t("Home")}</a></li>
|
||||||
|
<li onClick={() => {
|
||||||
|
const menu = document.getElementById("navi_menu") as HTMLElement;
|
||||||
|
menu.blur();
|
||||||
|
navigate("/tags")
|
||||||
|
}}><a>{t("Tags")}</a></li>
|
||||||
|
<li onClick={() => {
|
||||||
|
const menu = document.getElementById("navi_menu") as HTMLElement;
|
||||||
|
menu.blur();
|
||||||
|
navigate("/about")
|
||||||
|
}}><a>{t("About")}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<button className="btn btn-ghost text-xl" onClick={() => {
|
<button className="btn btn-ghost text-xl" onClick={() => {
|
||||||
appContext.clear()
|
appContext.clear()
|
||||||
navigate(`/`);
|
navigate(`/`, { replace: true});
|
||||||
}}>{app.appName}</button>
|
}}>{app.appName}</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hidden lg:flex">
|
||||||
|
<ul className="menu menu-horizontal px-1">
|
||||||
|
<li onClick={() => {
|
||||||
|
navigate("/");
|
||||||
|
}}><a>{t("Home")}</a></li>
|
||||||
|
<li onClick={() => {
|
||||||
|
navigate("/tags")
|
||||||
|
}}><a>{t("Tags")}</a></li>
|
||||||
|
<li onClick={() => {
|
||||||
|
navigate("/about")
|
||||||
|
}}><a>{t("About")}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className={"flex-1"}></div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<SearchBar />
|
<SearchBar/>
|
||||||
<UploadingSideBar />
|
<UploadingSideBar/>
|
||||||
{
|
{
|
||||||
app.isLoggedIn() && <button className={"btn btn-circle btn-ghost"} onClick={() => {
|
app.isLoggedIn() && <button className={"btn btn-circle btn-ghost"} onClick={() => {
|
||||||
navigate("/manage");
|
navigate("/manage");
|
||||||
}}>
|
}}>
|
||||||
<MdSettings size={24} />
|
<MdSettings size={24}/>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button className={"btn btn-circle btn-ghost"} onClick={() => {
|
<button className={"btn btn-circle btn-ghost"} onClick={() => {
|
||||||
window.open("https://github.com/wgh136/nysoure", "_blank");
|
window.open("https://github.com/wgh136/nysoure", "_blank");
|
||||||
}}>
|
}}>
|
||||||
<IoLogoGithub size={24} />
|
<IoLogoGithub size={24}/>
|
||||||
</button>
|
</button>
|
||||||
{
|
{
|
||||||
app.isLoggedIn() ? <UserButton /> : <button className={"btn btn-primary btn-square btn-soft"} onClick={() => {
|
app.isLoggedIn() ? <UserButton/> :
|
||||||
navigate("/login");
|
<button className={"btn btn-primary btn-square btn-soft"} onClick={() => {
|
||||||
}}>
|
navigate("/login");
|
||||||
<MdOutlinePerson size={24}></MdOutlinePerson>
|
}}>
|
||||||
</button>
|
<MdOutlinePerson size={24}></MdOutlinePerson>
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -169,6 +169,8 @@ export const i18nData = {
|
|||||||
"Provide a file url for the server to download, and the file will be moved to the selected storage.": "Provide a file url for the server to download, and the file will be moved to the selected storage.",
|
"Provide a file url for the server to download, and the file will be moved to the selected storage.": "Provide a file url for the server to download, and the file will be moved to the selected storage.",
|
||||||
"Verifying your request": "Verifying your request",
|
"Verifying your request": "Verifying your request",
|
||||||
"Please check your network if the verification takes too long or the captcha does not appear.": "Please check your network if the verification takes too long or the captcha does not appear.",
|
"Please check your network if the verification takes too long or the captcha does not appear.": "Please check your network if the verification takes too long or the captcha does not appear.",
|
||||||
|
"About": "About",
|
||||||
|
"Home": "Home",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
@@ -341,6 +343,8 @@ export const i18nData = {
|
|||||||
"Provide a file url for the server to download, and the file will be moved to the selected storage.": "提供一个文件链接供服务器下载,文件将被移动到选定的存储中。",
|
"Provide a file url for the server to download, and the file will be moved to the selected storage.": "提供一个文件链接供服务器下载,文件将被移动到选定的存储中。",
|
||||||
"Verifying your request": "正在验证您的请求",
|
"Verifying your request": "正在验证您的请求",
|
||||||
"Please check your network if the verification takes too long or the captcha does not appear.": "如果验证时间过长或验证码未出现, 请检查您的网络连接",
|
"Please check your network if the verification takes too long or the captcha does not appear.": "如果验证时间过长或验证码未出现, 请检查您的网络连接",
|
||||||
|
"About": "关于",
|
||||||
|
"Home": "首页",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-TW": {
|
"zh-TW": {
|
||||||
@@ -513,6 +517,8 @@ export const i18nData = {
|
|||||||
"Provide a file url for the server to download, and the file will be moved to the selected storage.": "提供一個檔案連結供伺服器下載,檔案將被移動到選定的儲存中。",
|
"Provide a file url for the server to download, and the file will be moved to the selected storage.": "提供一個檔案連結供伺服器下載,檔案將被移動到選定的儲存中。",
|
||||||
"Verifying your request": "正在驗證您的請求",
|
"Verifying your request": "正在驗證您的請求",
|
||||||
"Please check your network if the verification takes too long or the captcha does not appear.": "如果驗證時間過長或驗證碼未出現,請檢查您的網絡連接。",
|
"Please check your network if the verification takes too long or the captcha does not appear.": "如果驗證時間過長或驗證碼未出現,請檢查您的網絡連接。",
|
||||||
|
"About": "關於",
|
||||||
|
"Home": "首頁",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,10 @@ export interface Tag {
|
|||||||
aliases: string[];
|
aliases: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TagWithCount extends Tag {
|
||||||
|
resources_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateResourceParams {
|
export interface CreateResourceParams {
|
||||||
title: string;
|
title: string;
|
||||||
alternative_titles: string[];
|
alternative_titles: string[];
|
||||||
|
@@ -14,7 +14,7 @@ import {
|
|||||||
UserWithToken,
|
UserWithToken,
|
||||||
Comment,
|
Comment,
|
||||||
CommentWithResource,
|
CommentWithResource,
|
||||||
ServerConfig, RSort
|
ServerConfig, RSort, TagWithCount
|
||||||
} from "./models.ts";
|
} from "./models.ts";
|
||||||
|
|
||||||
class Network {
|
class Network {
|
||||||
@@ -284,6 +284,19 @@ class Network {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllTags(): Promise<Response<TagWithCount[]>> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.apiBaseUrl}/tag`)
|
||||||
|
return response.data
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: e.toString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async searchTags(keyword: string, mainTag?: boolean): 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`, {
|
||||||
|
@@ -3,10 +3,7 @@ import ResourcesView from "../components/resources_view.tsx";
|
|||||||
import {network} from "../network/network.ts";
|
import {network} from "../network/network.ts";
|
||||||
import { app } from "../app.ts";
|
import { app } from "../app.ts";
|
||||||
import {RSort} from "../network/models.ts";
|
import {RSort} from "../network/models.ts";
|
||||||
import Button from "../components/button.tsx";
|
|
||||||
import {MdInfoOutline} from "react-icons/md";
|
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {useNavigate} from "react-router";
|
|
||||||
import {useAppContext} from "../components/AppContext.tsx";
|
import {useAppContext} from "../components/AppContext.tsx";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
@@ -15,8 +12,6 @@ export default function HomePage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const appContext = useAppContext()
|
const appContext = useAppContext()
|
||||||
|
|
||||||
@@ -59,15 +54,6 @@ export default function HomePage() {
|
|||||||
<option value="4">{t("Downloads Ascending")}</option>
|
<option value="4">{t("Downloads Ascending")}</option>
|
||||||
<option value="5">{t("Downloads Descending")}</option>
|
<option value="5">{t("Downloads Descending")}</option>
|
||||||
</select>
|
</select>
|
||||||
<span className={"flex-1"}/>
|
|
||||||
<Button onClick={() => {
|
|
||||||
navigate("/about");
|
|
||||||
}}>
|
|
||||||
<div className={"flex items-center"}>
|
|
||||||
<MdInfoOutline size={24} className={"inline-block mr-2"}/>
|
|
||||||
<span>{t("About this site")}</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<ResourcesView
|
<ResourcesView
|
||||||
key={`home_page_${order}`}
|
key={`home_page_${order}`}
|
||||||
|
@@ -55,7 +55,7 @@ export default function TaggedResourcesPage() {
|
|||||||
<div className={"px-3"}>
|
<div className={"px-3"}>
|
||||||
{
|
{
|
||||||
(tag?.aliases ?? []).map((e) => {
|
(tag?.aliases ?? []).map((e) => {
|
||||||
return <Badge className={"m-1 badge-outline badge-soft"}>{e}</Badge>
|
return <Badge className={"m-1 badge-primary badge-soft"}>{e}</Badge>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
62
frontend/src/pages/tags_page.tsx
Normal file
62
frontend/src/pages/tags_page.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {TagWithCount} from "../network/models.ts";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {network} from "../network/network.ts";
|
||||||
|
import showToast from "../components/toast.ts";
|
||||||
|
import Loading from "../components/loading.tsx";
|
||||||
|
import Badge from "../components/badge.tsx";
|
||||||
|
import {useNavigate} from "react-router";
|
||||||
|
|
||||||
|
export default function TagsPage() {
|
||||||
|
const [tags, setTags] = useState<TagWithCount[] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
network.getAllTags().then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
setTags(res.data!);
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
type: "error",
|
||||||
|
message: res.message || "Failed to load tags"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
if (!tags) {
|
||||||
|
return <Loading/>
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsMap = new Map<string, TagWithCount[]>();
|
||||||
|
|
||||||
|
for (const tag of tags || []) {
|
||||||
|
const type = tag.type
|
||||||
|
if (!tagsMap.has(type)) {
|
||||||
|
tagsMap.set(type, []);
|
||||||
|
}
|
||||||
|
tagsMap.get(type)?.push(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [_, tags] of tagsMap) {
|
||||||
|
tags.sort((a, b) => b.resources_count - a.resources_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="flex flex-col gap-4 p-4">
|
||||||
|
<h1 className={"text-2xl font-bold py-2"}>Tags</h1>
|
||||||
|
{Array.from(tagsMap.entries()).map(([type, tags]) => (
|
||||||
|
<div key={type} className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-lg font-bold pl-1">{type == "" ? "Other" : type}</h2>
|
||||||
|
<p>
|
||||||
|
{tags.map(tag => (
|
||||||
|
<Badge onClick={() => {
|
||||||
|
navigate(`/tag/${tag.name}`);
|
||||||
|
}} key={tag.name} className={"m-1 cursor-pointer badge-soft badge-primary"}>
|
||||||
|
{tag.name + (tag.resources_count > 0 ? ` (${tag.resources_count})` : "")}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
@@ -122,6 +122,21 @@ func handleGetTagByName(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAllTags(c fiber.Ctx) error {
|
||||||
|
tags, err := service.GetTagList()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tags == nil {
|
||||||
|
tags = []model.TagViewWithCount{}
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[*[]model.TagViewWithCount]{
|
||||||
|
Success: true,
|
||||||
|
Data: &tags,
|
||||||
|
Message: "All tags retrieved successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AddTagRoutes(api fiber.Router) {
|
func AddTagRoutes(api fiber.Router) {
|
||||||
tag := api.Group("/tag")
|
tag := api.Group("/tag")
|
||||||
{
|
{
|
||||||
@@ -130,5 +145,6 @@ func AddTagRoutes(api fiber.Router) {
|
|||||||
tag.Delete("/:id", handleDeleteTag)
|
tag.Delete("/:id", handleDeleteTag)
|
||||||
tag.Put("/:id/info", handleSetTagInfo)
|
tag.Put("/:id/info", handleSetTagInfo)
|
||||||
tag.Get("/:name", handleGetTagByName)
|
tag.Get("/:name", handleGetTagByName)
|
||||||
|
tag.Get("/", getAllTags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -268,6 +268,36 @@ func GetResourceByTag(tagID uint, page int, pageSize int) ([]model.Resource, int
|
|||||||
return resources, int(totalPages), nil
|
return resources, int(totalPages), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountResourcesByTag counts the number of resources associated with a specific tag.
|
||||||
|
func CountResourcesByTag(tagID uint) (int64, error) {
|
||||||
|
tag, err := GetTagByID(tagID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if tag.AliasOf != nil {
|
||||||
|
tag, err = GetTagByID(*tag.AliasOf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var tagIds []uint
|
||||||
|
tagIds = append(tagIds, tag.ID)
|
||||||
|
for _, alias := range tag.Aliases {
|
||||||
|
tagIds = append(tagIds, alias.ID)
|
||||||
|
}
|
||||||
|
var count 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(&count).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ExistsResource(id uint) (bool, error) {
|
func ExistsResource(id uint) (bool, error) {
|
||||||
var r model.Resource
|
var r model.Resource
|
||||||
if err := db.Model(&model.Resource{}).Where("id = ?", id).First(&r).Error; err != nil {
|
if err := db.Model(&model.Resource{}).Where("id = ?", id).First(&r).Error; err != nil {
|
||||||
|
@@ -82,3 +82,13 @@ func SetTagInfo(id uint, description string, aliasOf *uint, tagType string) erro
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListTags retrieves all tags from the database.
|
||||||
|
// Only returns the ID, name, and type of each tag.
|
||||||
|
func ListTags() ([]model.Tag, error) {
|
||||||
|
var tags []model.Tag
|
||||||
|
if err := db.Select("id", "name", "type").Where("alias_of is null").Find(&tags).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
@@ -33,3 +33,15 @@ func (t *Tag) ToView() *TagView {
|
|||||||
Aliases: aliases,
|
Aliases: aliases,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TagViewWithCount struct {
|
||||||
|
TagView
|
||||||
|
ResourceCount int `json:"resources_count"` // Count of resources associated with the tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TagView) WithCount(count int) *TagViewWithCount {
|
||||||
|
return &TagViewWithCount{
|
||||||
|
TagView: *t,
|
||||||
|
ResourceCount: count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -56,6 +56,10 @@ func CreateResource(uid uint, params *ResourceCreateParams) (uint, error) {
|
|||||||
if r, err = dao.CreateResource(r); err != nil {
|
if r, err = dao.CreateResource(r); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
err = updateCachedTagList()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error updating cached tag list:", err)
|
||||||
|
}
|
||||||
return r.ID, nil
|
return r.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +173,10 @@ func DeleteResource(uid, id uint) error {
|
|||||||
if err := dao.DeleteResource(id); err != nil {
|
if err := dao.DeleteResource(id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = updateCachedTagList()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error updating cached tag list:", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,5 +249,9 @@ func EditResource(uid, rid uint, params *ResourceCreateParams) error {
|
|||||||
log.Error("UpdateResource error: ", err)
|
log.Error("UpdateResource error: ", err)
|
||||||
return model.NewInternalServerError("Failed to update resource")
|
return model.NewInternalServerError("Failed to update resource")
|
||||||
}
|
}
|
||||||
|
err = updateCachedTagList()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error updating cached tag list:", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,10 @@ func CreateTag(uid uint, name string) (*model.TagView, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = updateCachedTagList()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error updating cached tag list:", err)
|
||||||
|
}
|
||||||
return t.ToView(), nil
|
return t.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +67,10 @@ func SearchTag(name string, mainTag bool) ([]model.TagView, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTag(id uint) error {
|
func DeleteTag(id uint) error {
|
||||||
|
err := updateCachedTagList()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error updating cached tag list:", err)
|
||||||
|
}
|
||||||
return dao.DeleteTag(id)
|
return dao.DeleteTag(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,5 +96,36 @@ func SetTagInfo(uid uint, id uint, description string, aliasOf *uint, tagType st
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
err = updateCachedTagList()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error updating cached tag list:", err)
|
||||||
|
}
|
||||||
return t.ToView(), nil
|
return t.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cachedTagList []model.TagViewWithCount
|
||||||
|
|
||||||
|
func updateCachedTagList() error {
|
||||||
|
tags, err := dao.ListTags()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cachedTagList = make([]model.TagViewWithCount, 0, len(tags))
|
||||||
|
for _, tag := range tags {
|
||||||
|
count, err := dao.CountResourcesByTag(tag.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cachedTagList = append(cachedTagList, *tag.ToView().WithCount(int(count)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTagList() ([]model.TagViewWithCount, error) {
|
||||||
|
if cachedTagList == nil {
|
||||||
|
if err := updateCachedTagList(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedTagList, nil
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user