Moving the arttags table to tags table part 1
This commit is contained in:
140
prisma/migrations/20260202114116_tags_01/migration.sql
Normal file
140
prisma/migrations/20260202114116_tags_01/migration.sql
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Tag" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"sortIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"isVisible" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"description" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TagAlias" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"alias" TEXT NOT NULL,
|
||||||
|
"tagId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "TagAlias_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TagCategory" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"tagId" TEXT NOT NULL,
|
||||||
|
"categoryId" TEXT NOT NULL,
|
||||||
|
"isParent" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"showOnAnimalPage" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"parentTagId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "TagCategory_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Miniature" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Miniature_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ArtworkTagsV2" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_ArtworkTagsV2_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_MiniatureTags" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_MiniatureTags_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_CommissionTypeTags" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_CommissionTypeTags_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Tag_slug_key" ON "Tag"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TagAlias_alias_key" ON "TagAlias"("alias");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TagAlias_alias_idx" ON "TagAlias"("alias");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TagAlias_tagId_alias_key" ON "TagAlias"("tagId", "alias");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TagCategory_categoryId_idx" ON "TagCategory"("categoryId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TagCategory_tagId_idx" ON "TagCategory"("tagId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TagCategory_parentTagId_idx" ON "TagCategory"("parentTagId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TagCategory_categoryId_parentTagId_idx" ON "TagCategory"("categoryId", "parentTagId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TagCategory_tagId_categoryId_key" ON "TagCategory"("tagId", "categoryId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ArtworkTagsV2_B_index" ON "_ArtworkTagsV2"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_MiniatureTags_B_index" ON "_MiniatureTags"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_CommissionTypeTags_B_index" ON "_CommissionTypeTags"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TagAlias" ADD CONSTRAINT "TagAlias_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TagCategory" ADD CONSTRAINT "TagCategory_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TagCategory" ADD CONSTRAINT "TagCategory_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "ArtCategory"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TagCategory" ADD CONSTRAINT "TagCategory_parentTagId_fkey" FOREIGN KEY ("parentTagId") REFERENCES "Tag"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtworkTagsV2" ADD CONSTRAINT "_ArtworkTagsV2_A_fkey" FOREIGN KEY ("A") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArtworkTagsV2" ADD CONSTRAINT "_ArtworkTagsV2_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_MiniatureTags" ADD CONSTRAINT "_MiniatureTags_A_fkey" FOREIGN KEY ("A") REFERENCES "Miniature"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_MiniatureTags" ADD CONSTRAINT "_MiniatureTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_CommissionTypeTags" ADD CONSTRAINT "_CommissionTypeTags_A_fkey" FOREIGN KEY ("A") REFERENCES "CommissionType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_CommissionTypeTags" ADD CONSTRAINT "_CommissionTypeTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -54,6 +54,7 @@ model Artwork {
|
|||||||
categories ArtCategory[]
|
categories ArtCategory[]
|
||||||
colors ArtworkColor[]
|
colors ArtworkColor[]
|
||||||
tags ArtTag[]
|
tags ArtTag[]
|
||||||
|
tagsV2 Tag[] @relation("ArtworkTagsV2")
|
||||||
variants FileVariant[]
|
variants FileVariant[]
|
||||||
|
|
||||||
@@index([colorStatus])
|
@@index([colorStatus])
|
||||||
@ -102,6 +103,7 @@ model ArtCategory {
|
|||||||
|
|
||||||
artworks Artwork[]
|
artworks Artwork[]
|
||||||
tags ArtTag[]
|
tags ArtTag[]
|
||||||
|
tagLinks TagCategory[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model ArtTag {
|
model ArtTag {
|
||||||
@ -126,6 +128,63 @@ model ArtTag {
|
|||||||
children ArtTag[] @relation("TagHierarchy")
|
children ArtTag[] @relation("TagHierarchy")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Tag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
name String @unique
|
||||||
|
slug String @unique
|
||||||
|
isVisible Boolean @default(true)
|
||||||
|
|
||||||
|
description String?
|
||||||
|
|
||||||
|
aliases TagAlias[]
|
||||||
|
categoryLinks TagCategory[]
|
||||||
|
categoryParents TagCategory[] @relation("TagCategoryParent")
|
||||||
|
artworks Artwork[] @relation("ArtworkTagsV2")
|
||||||
|
commissionTypes CommissionType[] @relation("CommissionTypeTags")
|
||||||
|
miniatures Miniature[] @relation("MiniatureTags")
|
||||||
|
}
|
||||||
|
|
||||||
|
model TagAlias {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
alias String @unique
|
||||||
|
|
||||||
|
tagId String
|
||||||
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([tagId, alias])
|
||||||
|
@@index([alias])
|
||||||
|
}
|
||||||
|
|
||||||
|
model TagCategory {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
tagId String
|
||||||
|
categoryId String
|
||||||
|
|
||||||
|
isParent Boolean @default(false)
|
||||||
|
showOnAnimalPage Boolean @default(false)
|
||||||
|
parentTagId String?
|
||||||
|
|
||||||
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
|
category ArtCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||||
|
parentTag Tag? @relation("TagCategoryParent", fields: [parentTagId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@unique([tagId, categoryId])
|
||||||
|
@@index([categoryId])
|
||||||
|
@@index([tagId])
|
||||||
|
@@index([parentTagId])
|
||||||
|
@@index([categoryId, parentTagId])
|
||||||
|
}
|
||||||
|
|
||||||
model ArtTagAlias {
|
model ArtTagAlias {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -140,6 +199,14 @@ model ArtTagAlias {
|
|||||||
@@index([alias])
|
@@index([alias])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Miniature {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
tags Tag[] @relation("MiniatureTags")
|
||||||
|
}
|
||||||
|
|
||||||
model Color {
|
model Color {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -265,6 +332,8 @@ model CommissionType {
|
|||||||
|
|
||||||
description String?
|
description String?
|
||||||
|
|
||||||
|
tags Tag[] @relation("CommissionTypeTags")
|
||||||
|
|
||||||
options CommissionTypeOption[]
|
options CommissionTypeOption[]
|
||||||
extras CommissionTypeExtra[]
|
extras CommissionTypeExtra[]
|
||||||
customInputs CommissionTypeCustomInput[]
|
customInputs CommissionTypeCustomInput[]
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export async function deleteArtwork(artworkId: string) {
|
|||||||
colors: true,
|
colors: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
tags: true,
|
tags: true,
|
||||||
|
tagsV2: true,
|
||||||
categories: true,
|
categories: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -74,6 +75,7 @@ export async function deleteArtwork(artworkId: string) {
|
|||||||
where: { id: artworkId },
|
where: { id: artworkId },
|
||||||
data: {
|
data: {
|
||||||
tags: { set: [] },
|
tags: { set: [] },
|
||||||
|
tagsV2: { set: [] },
|
||||||
categories: { set: [] },
|
categories: { set: [] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -82,4 +84,4 @@ export async function deleteArtwork(artworkId: string) {
|
|||||||
await prisma.artwork.delete({ where: { id: artworkId } });
|
await prisma.artwork.delete({ where: { id: artworkId } });
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,9 +15,9 @@ export async function getSingleArtwork(id: string) {
|
|||||||
categories: true,
|
categories: true,
|
||||||
colors: { include: { color: true } },
|
colors: { include: { color: true } },
|
||||||
// sortContexts: true,
|
// sortContexts: true,
|
||||||
tags: true,
|
tagsV2: true,
|
||||||
variants: true,
|
variants: true,
|
||||||
timelapse: true
|
timelapse: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ function mapSortingToOrderBy(sorting: ArtworkTableInput["sorting"]) {
|
|||||||
// relation counts: Prisma supports ordering by _count
|
// relation counts: Prisma supports ordering by _count
|
||||||
albumsCount: (desc) => ({ albums: { _count: desc ? "desc" : "asc" } }),
|
albumsCount: (desc) => ({ albums: { _count: desc ? "desc" : "asc" } }),
|
||||||
categoriesCount: (desc) => ({ categories: { _count: desc ? "desc" : "asc" } }),
|
categoriesCount: (desc) => ({ categories: { _count: desc ? "desc" : "asc" } }),
|
||||||
tagsCount: (desc) => ({ tags: { _count: desc ? "desc" : "asc" } }),
|
tagsCount: (desc) => ({ tagsV2: { _count: desc ? "desc" : "asc" } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const orderBy = sorting
|
const orderBy = sorting
|
||||||
@ -89,7 +89,7 @@ export async function getArtworksTablePage(input: unknown) {
|
|||||||
gallery: { select: { id: true, name: true } },
|
gallery: { select: { id: true, name: true } },
|
||||||
albums: { select: { id: true, name: true } },
|
albums: { select: { id: true, name: true } },
|
||||||
categories: { select: { id: true, name: true } },
|
categories: { select: { id: true, name: true } },
|
||||||
_count: { select: { albums: true, categories: true, tags: true } },
|
_count: { select: { albums: true, categories: true, tagsV2: true } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@ -109,7 +109,7 @@ export async function getArtworksTablePage(input: unknown) {
|
|||||||
categories: a.categories,
|
categories: a.categories,
|
||||||
albumsCount: a._count.albums,
|
albumsCount: a._count.albums,
|
||||||
categoriesCount: a._count.categories,
|
categoriesCount: a._count.categories,
|
||||||
tagsCount: a._count.tags,
|
tagsCount: a._count.tagsV2,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const out = { rows, total, pageIndex, pageSize };
|
const out = { rows, total, pageIndex, pageSize };
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export async function updateArtwork(
|
|||||||
const tagsRelation =
|
const tagsRelation =
|
||||||
tagIds || tagsToCreate.length
|
tagIds || tagsToCreate.length
|
||||||
? {
|
? {
|
||||||
tags: {
|
tagsV2: {
|
||||||
set: [], // replace entire relation
|
set: [], // replace entire relation
|
||||||
connect: (tagIds ?? []).map((tagId) => ({ id: tagId })),
|
connect: (tagIds ?? []).map((tagId) => ({ id: tagId })),
|
||||||
connectOrCreate: tagsToCreate.map((tName) => ({
|
connectOrCreate: tagsToCreate.map((tName) => ({
|
||||||
@ -94,4 +94,4 @@ export async function updateArtwork(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return updatedArtwork;
|
return updatedArtwork;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export async function deleteCategory(catId: string) {
|
|||||||
id: true,
|
id: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
tags: true,
|
tagLinks: true,
|
||||||
artworks: true
|
artworks: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -25,7 +25,7 @@ export async function deleteCategory(catId: string) {
|
|||||||
throw new Error("Cannot delete category: it is used by artworks.");
|
throw new Error("Cannot delete category: it is used by artworks.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cat._count.tags > 0) {
|
if (cat._count.tagLinks > 0) {
|
||||||
throw new Error("Cannot delete category: it is used by tags.");
|
throw new Error("Cannot delete category: it is used by tags.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,4 +34,4 @@ export async function deleteCategory(catId: string) {
|
|||||||
revalidatePath("/categories");
|
revalidatePath("/categories");
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,14 +3,17 @@
|
|||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
|
||||||
export async function getCategoriesWithTags() {
|
export async function getCategoriesWithTags() {
|
||||||
return await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } })
|
return await prisma.artCategory.findMany({
|
||||||
|
include: { tagLinks: { include: { tag: true } } },
|
||||||
|
orderBy: { sortIndex: "asc" },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCategoriesWithCount() {
|
export async function getCategoriesWithCount() {
|
||||||
return await prisma.artCategory.findMany({
|
return await prisma.artCategory.findMany({
|
||||||
include: {
|
include: {
|
||||||
_count: { select: { artworks: true, tags: true } },
|
_count: { select: { artworks: true, tagLinks: true } },
|
||||||
},
|
},
|
||||||
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,30 +17,30 @@ export async function createTag(formData: TagFormInput) {
|
|||||||
const tagSlug = data.name.toLowerCase().replace(/\s+/g, "-");
|
const tagSlug = data.name.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
|
||||||
const created = await prisma.$transaction(async (tx) => {
|
const created = await prisma.$transaction(async (tx) => {
|
||||||
const tag = await tx.artTag.create({
|
const tag = await tx.tag.create({
|
||||||
data: {
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
slug: tagSlug,
|
slug: tagSlug,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
isParent: data.isParent,
|
isVisible: data.isVisible ?? true,
|
||||||
showOnAnimalPage: data.showOnAnimalPage,
|
|
||||||
parentId
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.categoryIds) {
|
if (data.categoryIds) {
|
||||||
await tx.artTag.update({
|
await tx.tagCategory.createMany({
|
||||||
where: { id: tag.id },
|
data: data.categoryIds.map((categoryId) => ({
|
||||||
data: {
|
tagId: tag.id,
|
||||||
categories: {
|
categoryId,
|
||||||
set: data.categoryIds.map(id => ({ id }))
|
isParent: data.isParent,
|
||||||
}
|
showOnAnimalPage: data.showOnAnimalPage,
|
||||||
}
|
parentTagId: parentId,
|
||||||
|
})),
|
||||||
|
skipDuplicates: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.aliases && data.aliases.length > 0) {
|
if (data.aliases && data.aliases.length > 0) {
|
||||||
await tx.artTagAlias.createMany({
|
await tx.tagAlias.createMany({
|
||||||
data: data.aliases.map((alias) => ({
|
data: data.aliases.map((alias) => ({
|
||||||
tagId: tag.id,
|
tagId: tag.id,
|
||||||
alias,
|
alias,
|
||||||
@ -53,4 +53,4 @@ export async function createTag(formData: TagFormInput) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return created
|
return created
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,14 +4,13 @@ import { prisma } from "@/lib/prisma";
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export async function deleteTag(tagId: string) {
|
export async function deleteTag(tagId: string) {
|
||||||
const tag = await prisma.artTag.findUnique({
|
const tag = await prisma.tag.findUnique({
|
||||||
where: { id: tagId },
|
where: { id: tagId },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
artworks: true,
|
artworks: true,
|
||||||
children: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -25,16 +24,21 @@ export async function deleteTag(tagId: string) {
|
|||||||
throw new Error("Cannot delete tag: it is used by artworks.");
|
throw new Error("Cannot delete tag: it is used by artworks.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag._count.children > 0) {
|
const parentUsage = await prisma.tagCategory.count({
|
||||||
|
where: { parentTagId: tagId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parentUsage > 0) {
|
||||||
throw new Error("Cannot delete tag: it has child tags.");
|
throw new Error("Cannot delete tag: it has child tags.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.artTagAlias.deleteMany({ where: { tagId } });
|
await tx.tagAlias.deleteMany({ where: { tagId } });
|
||||||
await tx.artTag.delete({ where: { id: tagId } });
|
await tx.tagCategory.deleteMany({ where: { tagId } });
|
||||||
|
await tx.tag.delete({ where: { id: tagId } });
|
||||||
});
|
});
|
||||||
|
|
||||||
revalidatePath("/tags");
|
revalidatePath("/tags");
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,5 +3,5 @@
|
|||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
|
||||||
export async function getTags() {
|
export async function getTags() {
|
||||||
return await prisma.artTag.findMany({ orderBy: { sortIndex: "asc" } })
|
return await prisma.tag.findMany({ orderBy: { sortIndex: "asc" } })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,20 +3,26 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export async function isDescendant(tagId: string, possibleAncestorId: string): Promise<boolean> {
|
export async function isDescendant(tagId: string, possibleAncestorId: string): Promise<boolean> {
|
||||||
// Walk upwards from possibleAncestorId; if we hit tagId, it's a cycle.
|
// Walk upwards across any category hierarchy; if we hit tagId, it's a cycle.
|
||||||
let current: string | null = possibleAncestorId;
|
const visited = new Set<string>();
|
||||||
|
const queue: string[] = [possibleAncestorId];
|
||||||
while (current) {
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift();
|
||||||
|
if (!current) continue;
|
||||||
if (current === tagId) return true;
|
if (current === tagId) return true;
|
||||||
|
if (visited.has(current)) continue;
|
||||||
|
visited.add(current);
|
||||||
|
|
||||||
const t: { parentId: string | null } | null =
|
const parents = await prisma.tagCategory.findMany({
|
||||||
await prisma.artTag.findUnique({
|
where: { tagId: current, parentTagId: { not: null } },
|
||||||
where: { id: current },
|
select: { parentTagId: true },
|
||||||
select: { parentId: true },
|
});
|
||||||
});
|
|
||||||
|
|
||||||
current = t?.parentId ?? null;
|
for (const p of parents) {
|
||||||
|
if (p.parentTagId) queue.push(p.parentTagId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/actions/tags/migrateArtTags.ts
Normal file
102
src/actions/tags/migrateArtTags.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
export async function migrateArtTags() {
|
||||||
|
const artTags = await prisma.artTag.findMany({
|
||||||
|
include: {
|
||||||
|
aliases: true,
|
||||||
|
categories: true,
|
||||||
|
artworks: { select: { id: true } },
|
||||||
|
},
|
||||||
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const idMap = new Map<string, string>();
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
for (const artTag of artTags) {
|
||||||
|
const tag = await tx.tag.upsert({
|
||||||
|
where: { slug: artTag.slug },
|
||||||
|
update: {
|
||||||
|
name: artTag.name,
|
||||||
|
description: artTag.description,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: artTag.name,
|
||||||
|
slug: artTag.slug,
|
||||||
|
description: artTag.description,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
idMap.set(artTag.id, tag.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasRows = artTags.flatMap((artTag) => {
|
||||||
|
const tagId = idMap.get(artTag.id);
|
||||||
|
if (!tagId) return [];
|
||||||
|
return artTag.aliases.map((a) => ({
|
||||||
|
tagId,
|
||||||
|
alias: a.alias,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (aliasRows.length > 0) {
|
||||||
|
await tx.tagAlias.createMany({
|
||||||
|
data: aliasRows,
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryRows = artTags.flatMap((artTag) => {
|
||||||
|
const tagId = idMap.get(artTag.id);
|
||||||
|
if (!tagId) return [];
|
||||||
|
const parentTagId = artTag.parentId
|
||||||
|
? idMap.get(artTag.parentId) ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return artTag.categories.map((category) => ({
|
||||||
|
tagId,
|
||||||
|
categoryId: category.id,
|
||||||
|
isParent: artTag.isParent,
|
||||||
|
showOnAnimalPage: artTag.showOnAnimalPage,
|
||||||
|
parentTagId,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (categoryRows.length > 0) {
|
||||||
|
await tx.tagCategory.createMany({
|
||||||
|
data: categoryRows,
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect artwork relations outside the transaction to avoid timeouts.
|
||||||
|
for (const artTag of artTags) {
|
||||||
|
const tagId = idMap.get(artTag.id);
|
||||||
|
if (!tagId) continue;
|
||||||
|
|
||||||
|
for (const artwork of artTag.artworks) {
|
||||||
|
await prisma.artwork.update({
|
||||||
|
where: { id: artwork.id },
|
||||||
|
data: {
|
||||||
|
tagsV2: {
|
||||||
|
connect: { id: tagId },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
tags: artTags.length,
|
||||||
|
aliases: artTags.reduce((sum, t) => sum + t.aliases.length, 0),
|
||||||
|
categoryLinks: artTags.reduce((sum, t) => sum + t.categories.length, 0),
|
||||||
|
};
|
||||||
|
revalidatePath("/tags");
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
@ -27,22 +27,62 @@ export async function updateTag(id: string, rawData: TagFormInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updated = await prisma.$transaction(async (tx) => {
|
const updated = await prisma.$transaction(async (tx) => {
|
||||||
const tag = await tx.artTag.update({
|
const tag = await tx.tag.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
slug: tagSlug,
|
slug: tagSlug,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
isParent: data.isParent,
|
isVisible: data.isVisible ?? true,
|
||||||
showOnAnimalPage: data.showOnAnimalPage,
|
|
||||||
parentId,
|
|
||||||
categories: data.categoryIds
|
|
||||||
? { set: data.categoryIds.map((cid) => ({ id: cid })) }
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const existing = await tx.artTagAlias.findMany({
|
if (data.categoryIds) {
|
||||||
|
const existingLinks = await tx.tagCategory.findMany({
|
||||||
|
where: { tagId: id },
|
||||||
|
select: { id: true, categoryId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const desired = new Set(data.categoryIds);
|
||||||
|
const existingSet = new Set(existingLinks.map((l) => l.categoryId));
|
||||||
|
|
||||||
|
const toCreate = data.categoryIds.filter((cid) => !existingSet.has(cid));
|
||||||
|
const toDeleteIds = existingLinks
|
||||||
|
.filter((l) => !desired.has(l.categoryId))
|
||||||
|
.map((l) => l.id);
|
||||||
|
|
||||||
|
if (toDeleteIds.length > 0) {
|
||||||
|
await tx.tagCategory.deleteMany({
|
||||||
|
where: { id: { in: toDeleteIds } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toCreate.length > 0) {
|
||||||
|
await tx.tagCategory.createMany({
|
||||||
|
data: toCreate.map((categoryId) => ({
|
||||||
|
tagId: id,
|
||||||
|
categoryId,
|
||||||
|
isParent: data.isParent,
|
||||||
|
showOnAnimalPage: data.showOnAnimalPage,
|
||||||
|
parentTagId: parentId,
|
||||||
|
})),
|
||||||
|
skipDuplicates: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingLinks.length > 0) {
|
||||||
|
await tx.tagCategory.updateMany({
|
||||||
|
where: { tagId: id, categoryId: { in: data.categoryIds } },
|
||||||
|
data: {
|
||||||
|
isParent: data.isParent,
|
||||||
|
showOnAnimalPage: data.showOnAnimalPage,
|
||||||
|
parentTagId: parentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await tx.tagAlias.findMany({
|
||||||
where: { tagId: id },
|
where: { tagId: id },
|
||||||
select: { id: true, alias: true },
|
select: { id: true, alias: true },
|
||||||
});
|
});
|
||||||
@ -56,13 +96,13 @@ export async function updateTag(id: string, rawData: TagFormInput) {
|
|||||||
.map((a) => a.id);
|
.map((a) => a.id);
|
||||||
|
|
||||||
if (toDeleteIds.length > 0) {
|
if (toDeleteIds.length > 0) {
|
||||||
await tx.artTagAlias.deleteMany({
|
await tx.tagAlias.deleteMany({
|
||||||
where: { id: { in: toDeleteIds } },
|
where: { id: { in: toDeleteIds } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toCreate.length > 0) {
|
if (toCreate.length > 0) {
|
||||||
await tx.artTagAlias.createMany({
|
await tx.tagAlias.createMany({
|
||||||
data: toCreate.map((alias) => ({ tagId: id, alias })),
|
data: toCreate.map((alias) => ({ tagId: id, alias })),
|
||||||
skipDuplicates: true,
|
skipDuplicates: true,
|
||||||
});
|
});
|
||||||
@ -72,4 +112,4 @@ export async function updateTag(id: string, rawData: TagFormInput) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,23 +3,43 @@ import { prisma } from "@/lib/prisma";
|
|||||||
|
|
||||||
export default async function PortfolioTagsEditPage({ params }: { params: { id: string } }) {
|
export default async function PortfolioTagsEditPage({ params }: { params: { id: string } }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const tag = await prisma.artTag.findUnique({
|
const tag = await prisma.tag.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
categories: true,
|
categoryLinks: {
|
||||||
|
include: {
|
||||||
|
category: true,
|
||||||
|
parentTag: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
aliases: true
|
aliases: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const categories = await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } });
|
const categories = await prisma.artCategory.findMany({
|
||||||
const tags = await prisma.artTag.findMany({ where: { isParent: true }, orderBy: { sortIndex: "asc" } });
|
include: { tagLinks: true },
|
||||||
|
orderBy: { sortIndex: "asc" },
|
||||||
|
});
|
||||||
|
const tags = await prisma.tag.findMany({ orderBy: { sortIndex: "asc" } });
|
||||||
|
|
||||||
|
const animalLink =
|
||||||
|
tag?.categoryLinks.find((link) => link.category.name === "Animal Studies") ??
|
||||||
|
null;
|
||||||
|
const tagWithMeta = tag
|
||||||
|
? {
|
||||||
|
...tag,
|
||||||
|
parentId: animalLink?.parentTagId ?? null,
|
||||||
|
isParent: animalLink?.isParent ?? false,
|
||||||
|
showOnAnimalPage: animalLink?.showOnAnimalPage ?? false,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold mb-4">Edit Tag</h1>
|
<h1 className="text-2xl font-bold mb-4">Edit Tag</h1>
|
||||||
{tag && <EditTagForm tag={tag} categories={categories} allTags={tags} />}
|
{tagWithMeta && <EditTagForm tag={tagWithMeta} categories={categories} allTags={tags} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,11 @@ import NewTagForm from "@/components/tags/NewTagForm";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export default async function PortfolioTagsNewPage() {
|
export default async function PortfolioTagsNewPage() {
|
||||||
const categories = await prisma.artCategory.findMany({ include: { tags: true }, orderBy: { sortIndex: "asc" } });
|
const categories = await prisma.artCategory.findMany({
|
||||||
const tags = await prisma.artTag.findMany({ where: { isParent: true }, orderBy: { sortIndex: "asc" } });
|
include: { tagLinks: true },
|
||||||
|
orderBy: { sortIndex: "asc" },
|
||||||
|
});
|
||||||
|
const tags = await prisma.tag.findMany({ orderBy: { sortIndex: "asc" } });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -11,4 +14,4 @@ export default async function PortfolioTagsNewPage() {
|
|||||||
<NewTagForm categories={categories} allTags={tags} />
|
<NewTagForm categories={categories} allTags={tags} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,40 +3,71 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { PlusCircleIcon } from "lucide-react";
|
import { PlusCircleIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { migrateArtTags } from "@/actions/tags/migrateArtTags";
|
||||||
|
|
||||||
export default async function ArtTagsPage() {
|
export default async function ArtTagsPage() {
|
||||||
const items = await prisma.artTag.findMany({
|
const items = await prisma.tag.findMany({
|
||||||
include: {
|
include: {
|
||||||
parent: { select: { id: true, name: true } },
|
|
||||||
aliases: { select: { alias: true } },
|
aliases: { select: { alias: true } },
|
||||||
categories: { select: { id: true, name: true } },
|
categoryLinks: {
|
||||||
|
include: {
|
||||||
|
category: { select: { id: true, name: true } },
|
||||||
|
parentTag: { select: { id: true, name: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
_count: { select: { artworks: true } },
|
_count: { select: { artworks: true } },
|
||||||
},
|
},
|
||||||
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rows = items.map((tag) => {
|
||||||
|
const categories = tag.categoryLinks.map((link) => link.category);
|
||||||
|
const animalLink = tag.categoryLinks.find(
|
||||||
|
(link) => link.category.name === "Animal Studies",
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: tag.id,
|
||||||
|
name: tag.name,
|
||||||
|
slug: tag.slug,
|
||||||
|
parent: animalLink?.parentTag ?? null,
|
||||||
|
isParent: animalLink?.isParent ?? false,
|
||||||
|
showOnAnimalPage: animalLink?.showOnAnimalPage ?? false,
|
||||||
|
aliases: tag.aliases,
|
||||||
|
categories,
|
||||||
|
_count: tag._count,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-7xl px-4 py-6">
|
<div className="mx-auto w-full max-w-7xl px-4 py-6">
|
||||||
<header className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
<header className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
Art Tags
|
Tags
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Manage tags, aliases, categories, and usage across artworks.
|
Manage tags, aliases, categories, and usage across artworks.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button asChild className="h-11 gap-2">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
<Link href="/tags/new">
|
<form action={migrateArtTags}>
|
||||||
<PlusCircleIcon className="h-4 w-4" />
|
<Button type="submit" variant="secondary" className="h-11">
|
||||||
Add new tag
|
Migrate old tags
|
||||||
</Link>
|
</Button>
|
||||||
</Button>
|
</form>
|
||||||
|
<Button asChild className="h-11 gap-2">
|
||||||
|
<Link href="/tags/new">
|
||||||
|
<PlusCircleIcon className="h-4 w-4" />
|
||||||
|
Add new tag
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{items.length > 0 ? (
|
{rows.length > 0 ? (
|
||||||
<TagTabs tags={items} />
|
<TagTabs tags={rows} />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
There are no tags yet. Consider adding some!
|
There are no tags yet. Consider adding some!
|
||||||
@ -44,4 +75,4 @@ export default async function ArtTagsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -171,7 +171,7 @@ export default function ArtworkDetails({
|
|||||||
v: (
|
v: (
|
||||||
<div className="flex flex-wrap gap-2 text-xs">
|
<div className="flex flex-wrap gap-2 text-xs">
|
||||||
<Badge variant="secondary">{(artwork.categories?.length ?? 0)} categories</Badge>
|
<Badge variant="secondary">{(artwork.categories?.length ?? 0)} categories</Badge>
|
||||||
<Badge variant="secondary">{(artwork.tags?.length ?? 0)} tags</Badge>
|
<Badge variant="secondary">{(artwork.tagsV2?.length ?? 0)} tags</Badge>
|
||||||
<Badge variant="secondary">{(artwork.colors?.length ?? 0)} colors</Badge>
|
<Badge variant="secondary">{(artwork.colors?.length ?? 0)} colors</Badge>
|
||||||
<Badge variant="secondary">{(artwork.variants?.length ?? 0)} variants</Badge>
|
<Badge variant="secondary">{(artwork.variants?.length ?? 0)} variants</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import MultipleSelector from "@/components/ui/multiselect";
|
import MultipleSelector from "@/components/ui/multiselect";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import type { ArtTag } from "@/generated/prisma/client";
|
import type { Tag } from "@/generated/prisma/client";
|
||||||
import { artworkSchema } from "@/schemas/artworks/imageSchema";
|
import { artworkSchema } from "@/schemas/artworks/imageSchema";
|
||||||
import type { ArtworkWithRelations, CategoryWithTags } from "@/types/Artwork";
|
import type { ArtworkWithRelations, CategoryWithTags } from "@/types/Artwork";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@ -20,7 +20,7 @@ export default function EditArtworkForm({ artwork, categories, tags }:
|
|||||||
{
|
{
|
||||||
artwork: ArtworkWithRelations,
|
artwork: ArtworkWithRelations,
|
||||||
categories: CategoryWithTags[]
|
categories: CategoryWithTags[]
|
||||||
tags: ArtTag[]
|
tags: Tag[]
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<z.infer<typeof artworkSchema>>({
|
const form = useForm<z.infer<typeof artworkSchema>>({
|
||||||
@ -39,7 +39,7 @@ export default function EditArtworkForm({ artwork, categories, tags }:
|
|||||||
year: artwork.year || undefined,
|
year: artwork.year || undefined,
|
||||||
creationDate: artwork.creationDate ? new Date(artwork.creationDate) : undefined,
|
creationDate: artwork.creationDate ? new Date(artwork.creationDate) : undefined,
|
||||||
categoryIds: artwork.categories?.map(cat => cat.id) ?? [],
|
categoryIds: artwork.categories?.map(cat => cat.id) ?? [],
|
||||||
tagIds: artwork.tags?.map(tag => tag.id) ?? [],
|
tagIds: artwork.tagsV2?.map(tag => tag.id) ?? [],
|
||||||
newCategoryNames: [],
|
newCategoryNames: [],
|
||||||
newTagNames: []
|
newTagNames: []
|
||||||
}
|
}
|
||||||
@ -264,7 +264,7 @@ export default function EditArtworkForm({ artwork, categories, tags }:
|
|||||||
const preferredTagIds = new Set<string>();
|
const preferredTagIds = new Set<string>();
|
||||||
for (const cat of categories) {
|
for (const cat of categories) {
|
||||||
if (!selectedCategoryIds.includes(cat.id)) continue;
|
if (!selectedCategoryIds.includes(cat.id)) continue;
|
||||||
for (const t of cat.tags) preferredTagIds.add(t.id);
|
for (const link of cat.tagLinks) preferredTagIds.add(link.tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing tag options with groups
|
// Existing tag options with groups
|
||||||
@ -402,4 +402,4 @@ export default function EditArtworkForm({ artwork, categories, tags }:
|
|||||||
</Form>
|
</Form>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ type CatRow = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
_count: { artworks: number, tags: number };
|
_count: { artworks: number, tagLinks: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CategoryTable({ categories }: { categories: CatRow[] }) {
|
export default function CategoryTable({ categories }: { categories: CatRow[] }) {
|
||||||
@ -48,7 +48,7 @@ export default function CategoryTable({ categories }: { categories: CatRow[] })
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right tabular-nums">
|
<TableCell className="text-right tabular-nums">
|
||||||
{c._count.tags}
|
{c._count.tagLinks}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right tabular-nums">
|
<TableCell className="text-right tabular-nums">
|
||||||
|
|||||||
@ -19,6 +19,9 @@ const artworkItems = [
|
|||||||
title: "Categories",
|
title: "Categories",
|
||||||
href: "/categories",
|
href: "/categories",
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const topicItems = [
|
||||||
{
|
{
|
||||||
title: "Tags",
|
title: "Tags",
|
||||||
href: "/tags",
|
href: "/tags",
|
||||||
@ -110,6 +113,25 @@ export default function TopNav() {
|
|||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>Topics</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<ul className="grid w-50 gap-4">
|
||||||
|
{topicItems.map((item) => (
|
||||||
|
<li key={item.title}>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href={item.href}>
|
||||||
|
<div className="text-sm leading-none font-medium">{item.title}</div>
|
||||||
|
<p className="text-muted-foreground line-clamp-2 text-sm leading-snug">
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>Commissions</NavigationMenuTrigger>
|
<NavigationMenuTrigger>Commissions</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
|
|||||||
@ -34,10 +34,15 @@ export const adminNav: AdminNavGroup[] = [
|
|||||||
title: "Artwork Management",
|
title: "Artwork Management",
|
||||||
items: [
|
items: [
|
||||||
{ title: "Categories", href: "/categories" },
|
{ title: "Categories", href: "/categories" },
|
||||||
{ title: "Tags", href: "/tags" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
title: "Topics",
|
||||||
|
items: [{ title: "Tags", href: "/tags" }],
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
type: "group",
|
type: "group",
|
||||||
title: "Commissions",
|
title: "Commissions",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ArtCategory, ArtTag, ArtTagAlias } from "@/generated/prisma/client";
|
import { ArtCategory, Tag, TagAlias } from "@/generated/prisma/client";
|
||||||
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@ -16,17 +16,32 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
|||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
import AliasEditor from "./AliasEditor";
|
import AliasEditor from "./AliasEditor";
|
||||||
|
|
||||||
export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag & { categories: ArtCategory[], aliases: ArtTagAlias[] }, categories: ArtCategory[], allTags: ArtTag[] }) {
|
export default function EditTagForm({
|
||||||
|
tag,
|
||||||
|
categories,
|
||||||
|
allTags,
|
||||||
|
}: {
|
||||||
|
tag: Tag & {
|
||||||
|
categoryLinks: { category: ArtCategory }[];
|
||||||
|
aliases: TagAlias[];
|
||||||
|
parentId?: string | null;
|
||||||
|
isParent?: boolean;
|
||||||
|
showOnAnimalPage?: boolean;
|
||||||
|
};
|
||||||
|
categories: ArtCategory[];
|
||||||
|
allTags: Tag[];
|
||||||
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<TagFormInput>({
|
const form = useForm<TagFormInput>({
|
||||||
resolver: zodResolver(tagSchema),
|
resolver: zodResolver(tagSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
description: tag.description || "",
|
description: tag.description || "",
|
||||||
categoryIds: tag.categories?.map(cat => cat.id) ?? [],
|
categoryIds: tag.categoryLinks?.map((link) => link.category.id) ?? [],
|
||||||
parentId: (tag as any).parentId ?? null,
|
parentId: tag.parentId ?? null,
|
||||||
isParent: tag.isParent ?? false,
|
isParent: tag.isParent ?? false,
|
||||||
showOnAnimalPage: tag.showOnAnimalPage ?? false,
|
showOnAnimalPage: tag.showOnAnimalPage ?? false,
|
||||||
|
isVisible: tag.isVisible ?? true,
|
||||||
aliases: tag.aliases?.map(a => a.alias) ?? []
|
aliases: tag.aliases?.map(a => a.alias) ?? []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -34,12 +49,12 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
|
|||||||
async function onSubmit(values: TagFormInput) {
|
async function onSubmit(values: TagFormInput) {
|
||||||
try {
|
try {
|
||||||
const updated = await updateTag(tag.id, values)
|
const updated = await updateTag(tag.id, values)
|
||||||
console.log("Art tag updated:", updated)
|
console.log("Tag updated:", updated)
|
||||||
toast("Art tag updated.")
|
toast("Tag updated.")
|
||||||
router.push("/tags")
|
router.push("/tags")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
toast("Failed to update art tag.")
|
toast("Failed to update tag.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +199,23 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isVisible"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Visible</FormLabel>
|
||||||
|
<FormDescription></FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Button type="submit">Submit</Button>
|
<Button type="submit">Submit</Button>
|
||||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||||
@ -192,4 +224,4 @@ export default function EditTagForm({ tag, categories, allTags }: { tag: ArtTag
|
|||||||
</Form>
|
</Form>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ArtCategory, ArtTag } from "@/generated/prisma/client";
|
import { ArtCategory, Tag } from "@/generated/prisma/client";
|
||||||
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
import { TagFormInput, tagSchema } from "@/schemas/artworks/tagSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@ -17,7 +17,7 @@ import { Switch } from "../ui/switch";
|
|||||||
import AliasEditor from "./AliasEditor";
|
import AliasEditor from "./AliasEditor";
|
||||||
|
|
||||||
|
|
||||||
export default function NewTagForm({ categories, allTags }: { categories: ArtCategory[], allTags: ArtTag[] }) {
|
export default function NewTagForm({ categories, allTags }: { categories: ArtCategory[], allTags: Tag[] }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<TagFormInput>({
|
const form = useForm<TagFormInput>({
|
||||||
resolver: zodResolver(tagSchema),
|
resolver: zodResolver(tagSchema),
|
||||||
@ -28,6 +28,7 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
parentId: null,
|
parentId: null,
|
||||||
isParent: false,
|
isParent: false,
|
||||||
showOnAnimalPage: false,
|
showOnAnimalPage: false,
|
||||||
|
isVisible: true,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -35,12 +36,12 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
async function onSubmit(values: TagFormInput) {
|
async function onSubmit(values: TagFormInput) {
|
||||||
try {
|
try {
|
||||||
const created = await createTag(values)
|
const created = await createTag(values)
|
||||||
console.log("Art tag created:", created)
|
console.log("Tag created:", created)
|
||||||
toast("Art tag created.")
|
toast("Tag created.")
|
||||||
router.push("/tags")
|
router.push("/tags")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
toast("Failed to create art tag.")
|
toast("Failed to create tag.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,6 +186,23 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isVisible"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Visible</FormLabel>
|
||||||
|
<FormDescription></FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Button type="submit">Submit</Button>
|
<Button type="submit">Submit</Button>
|
||||||
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
|
||||||
@ -193,4 +211,4 @@ export default function NewTagForm({ categories, allTags }: { categories: ArtCat
|
|||||||
</Form>
|
</Form>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export async function getArtworksPage(params: ArtworkListParams) {
|
|||||||
albums: true,
|
albums: true,
|
||||||
categories: true,
|
categories: true,
|
||||||
colors: true,
|
colors: true,
|
||||||
tags: true,
|
tagsV2: true,
|
||||||
variants: true,
|
variants: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ createdAt: "desc" }, { id: "asc" }],
|
orderBy: [{ createdAt: "desc" }, { id: "asc" }],
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export const tagSchema = z.object({
|
|||||||
parentId: z.string().nullable().optional(),
|
parentId: z.string().nullable().optional(),
|
||||||
isParent: z.boolean(),
|
isParent: z.boolean(),
|
||||||
showOnAnimalPage: z.boolean(),
|
showOnAnimalPage: z.boolean(),
|
||||||
|
isVisible: z.boolean().default(true),
|
||||||
|
|
||||||
aliases: z
|
aliases: z
|
||||||
.array(z.string().trim().min(1))
|
.array(z.string().trim().min(1))
|
||||||
@ -19,4 +20,3 @@ export const tagSchema = z.object({
|
|||||||
|
|
||||||
export type TagFormInput = z.input<typeof tagSchema>;
|
export type TagFormInput = z.input<typeof tagSchema>;
|
||||||
export type TagFormOutput = z.output<typeof tagSchema>;
|
export type TagFormOutput = z.output<typeof tagSchema>;
|
||||||
|
|
||||||
|
|||||||
@ -8,12 +8,12 @@ export type ArtworkWithRelations = Prisma.ArtworkGetPayload<{
|
|||||||
albums: true;
|
albums: true;
|
||||||
categories: true;
|
categories: true;
|
||||||
colors: true;
|
colors: true;
|
||||||
tags: true;
|
tagsV2: true;
|
||||||
variants: true;
|
variants: true;
|
||||||
timelapse: true;
|
timelapse: true;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type CategoryWithTags = Prisma.ArtCategoryGetPayload<{
|
export type CategoryWithTags = Prisma.ArtCategoryGetPayload<{
|
||||||
include: { tags: true };
|
include: { tagLinks: { include: { tag: true } } };
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
Reference in New Issue
Block a user