Merge dev into main
This commit is contained in:
@ -53,8 +53,7 @@ model Artwork {
|
|||||||
albums Album[]
|
albums Album[]
|
||||||
categories ArtCategory[]
|
categories ArtCategory[]
|
||||||
colors ArtworkColor[]
|
colors ArtworkColor[]
|
||||||
tags ArtTag[]
|
tags Tag[] @relation("ArtworkTags")
|
||||||
tagsV2 Tag[] @relation("ArtworkTagsV2")
|
|
||||||
variants FileVariant[]
|
variants FileVariant[]
|
||||||
|
|
||||||
@@index([colorStatus])
|
@@index([colorStatus])
|
||||||
@ -102,111 +101,9 @@ model ArtCategory {
|
|||||||
description String?
|
description String?
|
||||||
|
|
||||||
artworks Artwork[]
|
artworks Artwork[]
|
||||||
tags ArtTag[]
|
|
||||||
tagLinks TagCategory[]
|
tagLinks TagCategory[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model ArtTag {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
sortIndex Int @default(0)
|
|
||||||
|
|
||||||
name String @unique
|
|
||||||
slug String @unique
|
|
||||||
isParent Boolean @default(false)
|
|
||||||
showOnAnimalPage Boolean @default(false)
|
|
||||||
|
|
||||||
description String?
|
|
||||||
|
|
||||||
aliases ArtTagAlias[]
|
|
||||||
artworks Artwork[]
|
|
||||||
categories ArtCategory[]
|
|
||||||
|
|
||||||
parentId String?
|
|
||||||
parent ArtTag? @relation("TagHierarchy", fields: [parentId], references: [id], onDelete: SetNull)
|
|
||||||
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 {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
alias String @unique
|
|
||||||
|
|
||||||
tagId String
|
|
||||||
tag ArtTag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([tagId, 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())
|
||||||
@ -315,6 +212,71 @@ model FileVariant {
|
|||||||
@@unique([artworkId, type])
|
@@unique([artworkId, type])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("ArtworkTags")
|
||||||
|
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 Miniature {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
tags Tag[] @relation("MiniatureTags")
|
||||||
|
}
|
||||||
|
|
||||||
model Commission {
|
model Commission {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
@ -13,7 +13,6 @@ export async function deleteArtwork(artworkId: string) {
|
|||||||
colors: true,
|
colors: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
tags: true,
|
tags: true,
|
||||||
tagsV2: true,
|
|
||||||
categories: true,
|
categories: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -75,7 +74,6 @@ export async function deleteArtwork(artworkId: string) {
|
|||||||
where: { id: artworkId },
|
where: { id: artworkId },
|
||||||
data: {
|
data: {
|
||||||
tags: { set: [] },
|
tags: { set: [] },
|
||||||
tagsV2: { set: [] },
|
|
||||||
categories: { set: [] },
|
categories: { set: [] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export async function getSingleArtwork(id: string) {
|
|||||||
categories: true,
|
categories: true,
|
||||||
colors: { include: { color: true } },
|
colors: { include: { color: true } },
|
||||||
// sortContexts: true,
|
// sortContexts: true,
|
||||||
tagsV2: true,
|
tags: 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) => ({ tagsV2: { _count: desc ? "desc" : "asc" } }),
|
tagsCount: (desc) => ({ tags: { _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, tagsV2: true } },
|
_count: { select: { albums: true, categories: true, tags: 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.tagsV2,
|
tagsCount: a._count.tags,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
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
|
||||||
? {
|
? {
|
||||||
tagsV2: {
|
tags: {
|
||||||
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) => ({
|
||||||
|
|||||||
@ -1,102 +1,5 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
|
|
||||||
export async function migrateArtTags() {
|
export async function migrateArtTags() {
|
||||||
const artTags = await prisma.artTag.findMany({
|
throw new Error("Migration disabled: ArtTag models removed.");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/actions/tags/migrateArtworkTagJoin.ts
Normal file
75
src/actions/tags/migrateArtworkTagJoin.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
type JoinMigrationResult = {
|
||||||
|
ok: boolean;
|
||||||
|
copied: number;
|
||||||
|
oldExists: boolean;
|
||||||
|
newExists: boolean;
|
||||||
|
droppedOld: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function migrateArtworkTagJoin(
|
||||||
|
opts: { dropOld?: boolean } = {},
|
||||||
|
): Promise<JoinMigrationResult> {
|
||||||
|
const dropOld = Boolean(opts.dropOld);
|
||||||
|
|
||||||
|
const [oldRow, newRow] = await Promise.all([
|
||||||
|
prisma.$queryRaw<{ name: string | null }[]>`
|
||||||
|
select to_regclass('_ArtworkTagsV2')::text as name;
|
||||||
|
`,
|
||||||
|
prisma.$queryRaw<{ name: string | null }[]>`
|
||||||
|
select to_regclass('_ArtworkTags')::text as name;
|
||||||
|
`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const oldExists = Boolean(oldRow?.[0]?.name);
|
||||||
|
const newExists = Boolean(newRow?.[0]?.name);
|
||||||
|
|
||||||
|
if (!newExists) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
copied: 0,
|
||||||
|
oldExists,
|
||||||
|
newExists,
|
||||||
|
droppedOld: false,
|
||||||
|
message: "New join table _ArtworkTags does not exist. Run the migration first.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldExists) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
copied: 0,
|
||||||
|
oldExists,
|
||||||
|
newExists,
|
||||||
|
droppedOld: false,
|
||||||
|
message: "Old join table _ArtworkTagsV2 not found. Nothing to copy.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const copied = await prisma.$executeRawUnsafe(`
|
||||||
|
INSERT INTO "_ArtworkTags" ("A","B")
|
||||||
|
SELECT "A","B" FROM "_ArtworkTagsV2"
|
||||||
|
ON CONFLICT ("A","B") DO NOTHING;
|
||||||
|
`);
|
||||||
|
|
||||||
|
let droppedOld = false;
|
||||||
|
if (dropOld) {
|
||||||
|
await prisma.$executeRawUnsafe(`DROP TABLE "_ArtworkTagsV2";`);
|
||||||
|
droppedOld = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath("/tags");
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
copied: Number(copied ?? 0),
|
||||||
|
oldExists,
|
||||||
|
newExists,
|
||||||
|
droppedOld,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,14 +1,18 @@
|
|||||||
import { migrateArtTags } from "@/actions/tags/migrateArtTags";
|
import { migrateArtworkTagJoin } from "@/actions/tags/migrateArtworkTagJoin";
|
||||||
import TagTabs from "@/components/tags/TagTabs";
|
import TagTabs from "@/components/tags/TagTabs";
|
||||||
import { Button } from "@/components/ui/button";
|
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";
|
||||||
|
|
||||||
async function migrateTags() {
|
async function migrateArtworkTagJoinCopy() {
|
||||||
"use server";
|
"use server";
|
||||||
|
await migrateArtworkTagJoin();
|
||||||
|
}
|
||||||
|
|
||||||
await migrateArtTags();
|
async function migrateArtworkTagJoinDropOld() {
|
||||||
|
"use server";
|
||||||
|
await migrateArtworkTagJoin({ dropOld: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ArtTagsPage() {
|
export default async function ArtTagsPage() {
|
||||||
@ -58,9 +62,14 @@ export default async function ArtTagsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
<form action={migrateTags}>
|
<form action={migrateArtworkTagJoinCopy}>
|
||||||
<Button type="submit" variant="secondary" className="h-11">
|
<Button type="submit" variant="secondary" className="h-11">
|
||||||
Migrate old tags
|
Copy tag relations
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<form action={migrateArtworkTagJoinDropOld}>
|
||||||
|
<Button type="submit" variant="destructive" className="h-11">
|
||||||
|
Copy + drop old
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
<Button asChild className="h-11 gap-2">
|
<Button asChild className="h-11 gap-2">
|
||||||
@ -70,15 +79,17 @@ export default async function ArtTagsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header >
|
||||||
|
|
||||||
{rows.length > 0 ? (
|
{
|
||||||
<TagTabs tags={rows} />
|
rows.length > 0 ? (
|
||||||
) : (
|
<TagTabs tags={rows} />
|
||||||
<p className="text-muted-foreground">
|
) : (
|
||||||
There are no tags yet. Consider adding some!
|
<p className="text-muted-foreground">
|
||||||
</p>
|
There are no tags yet. Consider adding some!
|
||||||
)}
|
</p>
|
||||||
</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.tagsV2?.length ?? 0)} tags</Badge>
|
<Badge variant="secondary">{(artwork.tags?.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>
|
||||||
|
|||||||
@ -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.tagsV2?.map(tag => tag.id) ?? [],
|
tagIds: artwork.tags?.map(tag => tag.id) ?? [],
|
||||||
newCategoryNames: [],
|
newCategoryNames: [],
|
||||||
newTagNames: []
|
newTagNames: []
|
||||||
}
|
}
|
||||||
@ -283,6 +283,12 @@ export default function EditArtworkForm({ artwork, categories, tags }:
|
|||||||
.filter((t) => selectedTagIds.includes(t.id))
|
.filter((t) => selectedTagIds.includes(t.id))
|
||||||
.map((t) => ({ label: t.name, value: t.id }));
|
.map((t) => ({ label: t.name, value: t.id }));
|
||||||
|
|
||||||
|
const fallbackSelectedOptions =
|
||||||
|
artwork.tags
|
||||||
|
?.filter((t) => selectedTagIds.includes(t.id))
|
||||||
|
.filter((t) => !selectedExistingOptions.some((o) => o.value === t.id))
|
||||||
|
.map((t) => ({ label: t.name, value: t.id })) ?? [];
|
||||||
|
|
||||||
// Selected "new" tags (so they remain visible)
|
// Selected "new" tags (so they remain visible)
|
||||||
const selectedNewOptions = newTagNames.map((name) => ({
|
const selectedNewOptions = newTagNames.map((name) => ({
|
||||||
label: `Create: ${name}`,
|
label: `Create: ${name}`,
|
||||||
@ -302,7 +308,11 @@ export default function EditArtworkForm({ artwork, categories, tags }:
|
|||||||
placeholder="Select or type to create tags"
|
placeholder="Select or type to create tags"
|
||||||
hidePlaceholderWhenSelected
|
hidePlaceholderWhenSelected
|
||||||
selectFirstItem
|
selectFirstItem
|
||||||
value={[...selectedExistingOptions, ...selectedNewOptions]}
|
value={[
|
||||||
|
...selectedExistingOptions,
|
||||||
|
...fallbackSelectedOptions,
|
||||||
|
...selectedNewOptions,
|
||||||
|
]}
|
||||||
creatable
|
creatable
|
||||||
createOption={(raw) => ({
|
createOption={(raw) => ({
|
||||||
value: `__new__:${raw}`,
|
value: `__new__:${raw}`,
|
||||||
|
|||||||
@ -142,15 +142,6 @@ function removePickedOption(groupOption: GroupOption, picked: Option[]) {
|
|||||||
return cloneOption;
|
return cloneOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOptionsExist(groupOption: GroupOption, targetOption: Option[]) {
|
|
||||||
for (const [, value] of Object.entries(groupOption)) {
|
|
||||||
if (value.some((option) => targetOption.find((p) => p.value === option.value))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeInput(s: string) {
|
function normalizeInput(s: string) {
|
||||||
return s.trim().replace(/\s+/g, " ");
|
return s.trim().replace(/\s+/g, " ");
|
||||||
}
|
}
|
||||||
@ -238,7 +229,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
[selected],
|
[selected],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
const handleClickOutside = React.useCallback((event: MouseEvent | TouchEvent) => {
|
||||||
if (
|
if (
|
||||||
dropdownRef.current &&
|
dropdownRef.current &&
|
||||||
!dropdownRef.current.contains(event.target as Node) &&
|
!dropdownRef.current.contains(event.target as Node) &&
|
||||||
@ -248,7 +239,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
inputRef.current.blur();
|
inputRef.current.blur();
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleUnselect = React.useCallback(
|
const handleUnselect = React.useCallback(
|
||||||
(option: Option) => {
|
(option: Option) => {
|
||||||
@ -294,7 +285,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
document.removeEventListener('touchend', handleClickOutside);
|
document.removeEventListener('touchend', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [open]);
|
}, [open, handleClickOutside]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -311,7 +302,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
if (JSON.stringify(newOption) !== JSON.stringify(options)) {
|
if (JSON.stringify(newOption) !== JSON.stringify(options)) {
|
||||||
setOptions(newOption);
|
setOptions(newOption);
|
||||||
}
|
}
|
||||||
}, [arrayDefaultOptions, arrayOptions, groupBy, onSearch, options]);
|
}, [arrayOptions, groupBy, onSearch, options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/** sync search */
|
/** sync search */
|
||||||
@ -334,8 +325,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
};
|
};
|
||||||
|
|
||||||
void exec();
|
void exec();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [debouncedSearchTerm, groupBy, onSearchSync, open, triggerSearchOnFocus]);
|
||||||
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/** async search */
|
/** async search */
|
||||||
@ -360,8 +350,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
};
|
};
|
||||||
|
|
||||||
void exec();
|
void exec();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [debouncedSearchTerm, groupBy, onSearch, open, triggerSearchOnFocus]);
|
||||||
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
|
|
||||||
|
|
||||||
const CreatableItem = () => {
|
const CreatableItem = () => {
|
||||||
if (!creatable) return undefined;
|
if (!creatable) return undefined;
|
||||||
@ -448,14 +437,10 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
return commandProps.filter;
|
return commandProps.filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (creatable) {
|
return (value: string, search: string) => {
|
||||||
return (value: string, search: string) => {
|
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : -1;
|
||||||
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : -1;
|
};
|
||||||
};
|
}, [commandProps?.filter]);
|
||||||
}
|
|
||||||
// Using default filter in `cmdk`. We don't have to provide it.
|
|
||||||
return undefined;
|
|
||||||
}, [creatable, commandProps?.filter]);
|
|
||||||
|
|
||||||
const orderedGroupEntries = React.useMemo(() => {
|
const orderedGroupEntries = React.useMemo(() => {
|
||||||
const entries = Object.entries(selectables);
|
const entries = Object.entries(selectables);
|
||||||
@ -495,7 +480,8 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
} // When onSearch is provided, we don't want to filter the options. You can still override it.
|
} // When onSearch is provided, we don't want to filter the options. You can still override it.
|
||||||
filter={commandFilter()}
|
filter={commandFilter()}
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-10 rounded-md border border-input text-base ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 md:text-sm',
|
'min-h-10 rounded-md border border-input text-base ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 md:text-sm',
|
||||||
{
|
{
|
||||||
@ -508,6 +494,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
inputRef?.current?.focus();
|
inputRef?.current?.focus();
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div className="relative flex flex-wrap gap-1">
|
<div className="relative flex flex-wrap gap-1">
|
||||||
{selected.map((option) => {
|
{selected.map((option) => {
|
||||||
@ -594,7 +581,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
<X />
|
<X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{open && (
|
{open && (
|
||||||
<CommandList
|
<CommandList
|
||||||
@ -626,7 +613,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
|||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={`${option.label}::${option.value}`}
|
||||||
disabled={disabledItem}
|
disabled={disabledItem}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export async function getArtworksPage(params: ArtworkListParams) {
|
|||||||
albums: true,
|
albums: true,
|
||||||
categories: true,
|
categories: true,
|
||||||
colors: true,
|
colors: true,
|
||||||
tagsV2: true,
|
tags: true,
|
||||||
variants: true,
|
variants: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ createdAt: "desc" }, { id: "asc" }],
|
orderBy: [{ createdAt: "desc" }, { id: "asc" }],
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export type ArtworkWithRelations = Prisma.ArtworkGetPayload<{
|
|||||||
albums: true;
|
albums: true;
|
||||||
categories: true;
|
categories: true;
|
||||||
colors: true;
|
colors: true;
|
||||||
tagsV2: true;
|
tags: true;
|
||||||
variants: true;
|
variants: true;
|
||||||
timelapse: true;
|
timelapse: true;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user