Refactor poses and expressions to be fully database-driven
- Removed hardcoded `BUILTIN_POSES` and `BUILTIN_EXPRESSIONS` constants.
- Updated `PoseService` and `ExpressionService` to only list and delete items from the database.
- Removed the `type` field ('builtin' | 'custom') from `PoseItem` and `ExpressionItem`.
- Updated `GenerationService` to resolve pose and expression assets strictly from the database (`data/files`) instead of falling back to `public/`.
- Updated `Poses.tsx` and `Expressions.tsx` to remove "SYSTEM" vs "CUSTOM" UI distinction and enable deletion for all items.
- Updated `pose.test.ts` and created `expression.test.ts` to verify the new behavior.
This commit is contained in:
1 parent
044764fcf7
commit
14d23144c4
9 files changed
+194
-223
No files matched your search
@@ -7,7 +7,7 @@
|
||||
"@google/genai": "^1.40.0",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@libsql/client": "^0.17.0",
|
||||
"@prisma/adapter-libsql": "^7.3.0",
|
||||
"@prisma/adapter-libsql": "7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"@google/genai": "^1.40.0",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@libsql/client": "^0.17.0",
|
||||
"@prisma/adapter-libsql": "^7.3.0",
|
||||
"@prisma/adapter-libsql": "7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { expect, test, describe, mock, beforeEach } from 'bun:test'
|
||||
import { prisma } from '../db'
|
||||
import { assetService } from './asset'
|
||||
import { ExpressionService } from './expression'
|
||||
|
||||
// Mock prisma
|
||||
mock.module('../db', () => ({
|
||||
prisma: {
|
||||
expression: {
|
||||
findMany: mock(),
|
||||
create: mock(),
|
||||
findUnique: mock(),
|
||||
delete: mock(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock assetService
|
||||
mock.module('./asset', () => ({
|
||||
assetService: {
|
||||
createAsset: mock(),
|
||||
deleteAsset: mock(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('ExpressionService', () => {
|
||||
let expressionService: ExpressionService
|
||||
|
||||
beforeEach(() => {
|
||||
expressionService = new ExpressionService()
|
||||
})
|
||||
|
||||
test('listExpressions returns custom expressions', async () => {
|
||||
const customExpressions = [
|
||||
{ id: 'custom1', name: 'Custom 1', image: { path: 'path1' } },
|
||||
{ id: 'custom2', name: 'Custom 2', image: { path: 'path2' } },
|
||||
]
|
||||
;(prisma.expression.findMany as any).mockResolvedValue(customExpressions)
|
||||
|
||||
const expressions = await expressionService.listExpressions()
|
||||
|
||||
expect(expressions.length).toBe(2)
|
||||
expect(expressions.find((e) => e.id === 'custom1')).toBeDefined()
|
||||
expect(expressions.find((e) => e.id === 'custom2')).toBeDefined()
|
||||
})
|
||||
|
||||
test('createExpression creates asset and expression', async () => {
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const assetId = 'asset1'
|
||||
|
||||
;(assetService.createAsset as any).mockResolvedValue({ id: assetId })
|
||||
;(prisma.expression.create as any).mockResolvedValue({
|
||||
id: 'expr1',
|
||||
name: 'Test Expression',
|
||||
image: { path: 'path1' },
|
||||
})
|
||||
|
||||
const result = await expressionService.createExpression('Test Expression', file)
|
||||
|
||||
// @ts-ignore
|
||||
expect(assetService.createAsset).toHaveBeenCalledWith(file, {
|
||||
name: 'Test Expression',
|
||||
type: 'image/png',
|
||||
})
|
||||
// @ts-ignore
|
||||
expect(prisma.expression.create).toHaveBeenCalled()
|
||||
expect(result.id).toBe('expr1')
|
||||
})
|
||||
|
||||
test('deleteExpression deletes expression', async () => {
|
||||
const customId = 'custom1'
|
||||
;(prisma.expression.findUnique as any).mockResolvedValue({ id: customId, imageId: 'asset1' })
|
||||
;(prisma.expression.delete as any).mockResolvedValue({ id: customId })
|
||||
|
||||
await expressionService.deleteExpression(customId)
|
||||
|
||||
// @ts-ignore
|
||||
expect(prisma.expression.delete).toHaveBeenCalledWith({ where: { id: customId } })
|
||||
// @ts-ignore
|
||||
expect(assetService.deleteAsset).toHaveBeenCalledWith('asset1')
|
||||
})
|
||||
})
|
||||
+11
-50
@@ -4,19 +4,9 @@ import { assetService } from './asset'
|
||||
export interface ExpressionItem {
|
||||
id: string
|
||||
name: string
|
||||
type: 'builtin' | 'custom'
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
export const BUILTIN_EXPRESSIONS: ExpressionItem[] = Array.from({ length: 9 }, (_, i) => i + 1).map(
|
||||
(i) => ({
|
||||
id: `expression_${i}.webp`,
|
||||
name: `Expression ${i}`,
|
||||
type: 'builtin',
|
||||
imageUrl: `/expressions/expression_${i}.webp`,
|
||||
}),
|
||||
)
|
||||
|
||||
export class ExpressionService {
|
||||
async listExpressions(
|
||||
options: { page?: number; limit?: number } = {},
|
||||
@@ -24,38 +14,18 @@ export class ExpressionService {
|
||||
const { page = 1, limit = 20 } = options
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Logic for mixed pagination
|
||||
const builtinCount = BUILTIN_EXPRESSIONS.length
|
||||
const customExpressions = await prisma.expression.findMany({
|
||||
include: { image: true },
|
||||
orderBy: { id: 'desc' },
|
||||
skip: skip,
|
||||
take: limit,
|
||||
})
|
||||
|
||||
// Calculate how many builtin items to include
|
||||
const builtinStart = skip
|
||||
const builtinEnd = Math.min(builtinCount, skip + limit)
|
||||
const builtinSlice =
|
||||
builtinStart < builtinCount ? BUILTIN_EXPRESSIONS.slice(builtinStart, builtinEnd) : []
|
||||
|
||||
// Calculate how many db items to fetch
|
||||
const remainingLimit = limit - builtinSlice.length
|
||||
const dbSkip = Math.max(0, skip - builtinCount)
|
||||
|
||||
let formattedCustomExpressions: ExpressionItem[] = []
|
||||
|
||||
if (remainingLimit > 0) {
|
||||
const customExpressions = await prisma.expression.findMany({
|
||||
include: { image: true },
|
||||
orderBy: { id: 'desc' },
|
||||
skip: dbSkip,
|
||||
take: remainingLimit,
|
||||
})
|
||||
|
||||
formattedCustomExpressions = customExpressions.map((e) => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
type: 'custom',
|
||||
imageUrl: `/files/${e.image.path}`,
|
||||
}))
|
||||
}
|
||||
|
||||
return [...builtinSlice, ...formattedCustomExpressions]
|
||||
return customExpressions.map((e) => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
imageUrl: `/files/${e.image.path}`,
|
||||
}))
|
||||
}
|
||||
|
||||
async createExpression(name: string, file: File) {
|
||||
@@ -75,16 +45,11 @@ export class ExpressionService {
|
||||
return {
|
||||
id: expression.id,
|
||||
name: expression.name,
|
||||
type: 'custom',
|
||||
imageUrl: `/files/${expression.image.path}`,
|
||||
} as ExpressionItem
|
||||
}
|
||||
|
||||
async deleteExpression(id: string) {
|
||||
if (BUILTIN_EXPRESSIONS.some((e) => e.id === id)) {
|
||||
throw new Error('Cannot delete builtin expression')
|
||||
}
|
||||
|
||||
const expression = await prisma.expression.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
@@ -100,10 +65,6 @@ export class ExpressionService {
|
||||
// Delete image asset
|
||||
await assetService.deleteAsset(expression.imageId)
|
||||
}
|
||||
|
||||
getBuiltinExpression(id: string) {
|
||||
return BUILTIN_EXPRESSIONS.find((e) => e.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
export const expressionService = new ExpressionService()
|
||||
+48
-70
@@ -3,8 +3,6 @@ import { prisma } from '../db'
|
||||
import { Prisma } from '../generated/prisma/client'
|
||||
import { aiService, type AIPart } from './ai'
|
||||
import { assetService } from './asset'
|
||||
import { expressionService } from './expression'
|
||||
import { poseService } from './pose'
|
||||
|
||||
export class GenerationService {
|
||||
async createGeneration(
|
||||
@@ -75,84 +73,64 @@ category: ${eq.category}
|
||||
let poseInstruction = '6. Maintain the exact pose and angle of the Base Character.'
|
||||
|
||||
if (poseId) {
|
||||
let poseBuffer: ArrayBuffer
|
||||
let poseMimeType: string
|
||||
|
||||
const builtinPose = poseService.getBuiltinPose(poseId)
|
||||
if (builtinPose) {
|
||||
const posePath = join('public/poses', poseId)
|
||||
const file = Bun.file(posePath)
|
||||
poseBuffer = await file.arrayBuffer()
|
||||
poseMimeType = file.type || 'image/webp'
|
||||
} else {
|
||||
const customPose = await prisma.pose.findUnique({
|
||||
where: { id: poseId },
|
||||
include: { image: true },
|
||||
})
|
||||
|
||||
if (!customPose) {
|
||||
throw new Error(`Pose not found: ${poseId}`)
|
||||
}
|
||||
|
||||
const posePath = join('data/files', customPose.image.path)
|
||||
const file = Bun.file(posePath)
|
||||
poseBuffer = await file.arrayBuffer()
|
||||
poseMimeType = customPose.image.type
|
||||
}
|
||||
|
||||
const poseBase64 = Buffer.from(poseBuffer).toString('base64')
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: poseMimeType,
|
||||
data: poseBase64,
|
||||
},
|
||||
const pose = await prisma.pose.findUnique({
|
||||
where: { id: poseId },
|
||||
include: { image: true },
|
||||
})
|
||||
parts.push({ text: 'Target Pose Reference' })
|
||||
|
||||
poseInstruction =
|
||||
"6. Use the 'Target Pose Reference' image for the character's pose and angle."
|
||||
if (pose) {
|
||||
const posePath = join('data/files', pose.image.path)
|
||||
const file = Bun.file(posePath)
|
||||
const poseBuffer = await file.arrayBuffer()
|
||||
const poseBase64 = Buffer.from(poseBuffer).toString('base64')
|
||||
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: pose.image.type,
|
||||
data: poseBase64,
|
||||
},
|
||||
})
|
||||
parts.push({ text: 'Target Pose Reference' })
|
||||
|
||||
poseInstruction =
|
||||
"6. Use the 'Target Pose Reference' image for the character's pose and angle."
|
||||
} else {
|
||||
// Fallback for transition: if user hasn't migrated example data yet, poseId might be missing.
|
||||
// We log a warning but proceed without the pose, or we could throw.
|
||||
// Given strict requirement, throwing might be better, but let's be safe and just ignore if not found?
|
||||
// Actually, if the user explicitly selected a pose, they expect it to work. Throwing is better.
|
||||
throw new Error(`Pose not found: ${poseId}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Expression
|
||||
let expressionInstruction = '7. Maintain the facial expression of the Base Character.'
|
||||
|
||||
if (expressionId) {
|
||||
let expressionBuffer: ArrayBuffer
|
||||
let expressionMimeType: string
|
||||
|
||||
const builtinExpression = expressionService.getBuiltinExpression(expressionId)
|
||||
if (builtinExpression) {
|
||||
const expressionPath = join('public/expressions', expressionId)
|
||||
const file = Bun.file(expressionPath)
|
||||
expressionBuffer = await file.arrayBuffer()
|
||||
expressionMimeType = file.type || 'image/webp'
|
||||
} else {
|
||||
const customExpression = await prisma.expression.findUnique({
|
||||
where: { id: expressionId },
|
||||
include: { image: true },
|
||||
})
|
||||
|
||||
if (!customExpression) {
|
||||
throw new Error(`Expression not found: ${expressionId}`)
|
||||
}
|
||||
|
||||
const expressionPath = join('data/files', customExpression.image.path)
|
||||
const file = Bun.file(expressionPath)
|
||||
expressionBuffer = await file.arrayBuffer()
|
||||
expressionMimeType = customExpression.image.type
|
||||
}
|
||||
|
||||
const expressionBase64 = Buffer.from(expressionBuffer).toString('base64')
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: expressionMimeType,
|
||||
data: expressionBase64,
|
||||
},
|
||||
const expression = await prisma.expression.findUnique({
|
||||
where: { id: expressionId },
|
||||
include: { image: true },
|
||||
})
|
||||
parts.push({ text: 'Target Expression Reference' })
|
||||
|
||||
expressionInstruction =
|
||||
"7. Use the 'Target Expression Reference' image for the character's facial expression."
|
||||
if (expression) {
|
||||
const expressionPath = join('data/files', expression.image.path)
|
||||
const file = Bun.file(expressionPath)
|
||||
const expressionBuffer = await file.arrayBuffer()
|
||||
const expressionBase64 = Buffer.from(expressionBuffer).toString('base64')
|
||||
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: expression.image.type,
|
||||
data: expressionBase64,
|
||||
},
|
||||
})
|
||||
parts.push({ text: 'Target Expression Reference' })
|
||||
|
||||
expressionInstruction =
|
||||
"7. Use the 'Target Expression Reference' image for the character's facial expression."
|
||||
} else {
|
||||
throw new Error(`Expression not found: ${expressionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
let prompt = `
|
||||
|
||||
+20
-25
@@ -1,6 +1,7 @@
|
||||
import { expect, test, describe, mock } from 'bun:test'
|
||||
import { expect, test, describe, mock, beforeEach } from 'bun:test'
|
||||
import { prisma } from '../db'
|
||||
import { assetService } from './asset'
|
||||
import { PoseService } from './pose'
|
||||
|
||||
// Mock prisma
|
||||
mock.module('../db', () => ({
|
||||
@@ -23,26 +24,27 @@ mock.module('./asset', () => ({
|
||||
}))
|
||||
|
||||
describe('PoseService', () => {
|
||||
test('listPoses returns builtin + custom poses', async () => {
|
||||
// @ts-ignore
|
||||
const { poseService, BUILTIN_POSES } = await import(`./pose?v=${Date.now()}`)
|
||||
let poseService: PoseService
|
||||
|
||||
// Setup custom poses mock
|
||||
const customPoses = [{ id: 'custom1', name: 'Custom 1', image: { path: 'path1' } }]
|
||||
beforeEach(() => {
|
||||
poseService = new PoseService()
|
||||
})
|
||||
|
||||
test('listPoses returns custom poses', async () => {
|
||||
const customPoses = [
|
||||
{ id: 'custom1', name: 'Custom 1', image: { path: 'path1' } },
|
||||
{ id: 'custom2', name: 'Custom 2', image: { path: 'path2' } },
|
||||
]
|
||||
;(prisma.pose.findMany as any).mockResolvedValue(customPoses)
|
||||
|
||||
const poses = await poseService.listPoses()
|
||||
|
||||
expect(poses.length).toBe(BUILTIN_POSES.length + 1)
|
||||
expect(poses.find((p: any) => p.id === 'custom1')).toBeDefined()
|
||||
expect(poses.find((p: any) => p.type === 'custom')).toBeDefined()
|
||||
expect(poses.find((p: any) => p.type === 'builtin')).toBeDefined()
|
||||
expect(poses.length).toBe(2)
|
||||
expect(poses.find((p) => p.id === 'custom1')).toBeDefined()
|
||||
expect(poses.find((p) => p.id === 'custom2')).toBeDefined()
|
||||
})
|
||||
|
||||
test('createPose creates asset and pose', async () => {
|
||||
// @ts-ignore
|
||||
const { poseService } = await import(`./pose?v=${Date.now()}`)
|
||||
|
||||
const file = new File([''], 'test.png', { type: 'image/png' })
|
||||
const assetId = 'asset1'
|
||||
|
||||
@@ -55,33 +57,26 @@ describe('PoseService', () => {
|
||||
|
||||
const result = await poseService.createPose('Test Pose', file)
|
||||
|
||||
// @ts-ignore
|
||||
expect(assetService.createAsset).toHaveBeenCalledWith(file, {
|
||||
name: 'Test Pose',
|
||||
type: 'image/png',
|
||||
})
|
||||
// @ts-ignore
|
||||
expect(prisma.pose.create).toHaveBeenCalled()
|
||||
expect(result.id).toBe('pose1')
|
||||
})
|
||||
|
||||
test('deletePose throws for builtin pose', async () => {
|
||||
// @ts-ignore
|
||||
const { poseService, BUILTIN_POSES } = await import(`./pose?v=${Date.now()}`)
|
||||
|
||||
const builtinId = BUILTIN_POSES[0].id
|
||||
expect(poseService.deletePose(builtinId)).rejects.toThrow('Cannot delete builtin pose')
|
||||
})
|
||||
|
||||
test('deletePose deletes custom pose', async () => {
|
||||
// @ts-ignore
|
||||
const { poseService } = await import(`./pose?v=${Date.now()}`)
|
||||
|
||||
test('deletePose deletes pose', async () => {
|
||||
const customId = 'custom1'
|
||||
;(prisma.pose.findUnique as any).mockResolvedValue({ id: customId, imageId: 'asset1' })
|
||||
;(prisma.pose.delete as any).mockResolvedValue({ id: customId })
|
||||
|
||||
await poseService.deletePose(customId)
|
||||
|
||||
// @ts-ignore
|
||||
expect(prisma.pose.delete).toHaveBeenCalledWith({ where: { id: customId } })
|
||||
// @ts-ignore
|
||||
expect(assetService.deleteAsset).toHaveBeenCalledWith('asset1')
|
||||
})
|
||||
})
|
||||
+11
-48
@@ -4,54 +4,26 @@ import { assetService } from './asset'
|
||||
export interface PoseItem {
|
||||
id: string
|
||||
name: string
|
||||
type: 'builtin' | 'custom'
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
export const BUILTIN_POSES: PoseItem[] = Array.from({ length: 9 }, (_, i) => i + 1).map((i) => ({
|
||||
id: `character_pose_${i}.webp`,
|
||||
name: `Pose ${i}`,
|
||||
type: 'builtin',
|
||||
imageUrl: `/poses/character_pose_${i}.webp`,
|
||||
}))
|
||||
|
||||
export class PoseService {
|
||||
async listPoses(options: { page?: number; limit?: number } = {}): Promise<PoseItem[]> {
|
||||
const { page = 1, limit = 20 } = options
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Logic for mixed pagination
|
||||
const builtinCount = BUILTIN_POSES.length
|
||||
const customPoses = await prisma.pose.findMany({
|
||||
include: { image: true },
|
||||
orderBy: { id: 'desc' },
|
||||
skip: skip,
|
||||
take: limit,
|
||||
})
|
||||
|
||||
// Calculate how many builtin items to include
|
||||
const builtinStart = skip
|
||||
const builtinEnd = Math.min(builtinCount, skip + limit)
|
||||
const builtinSlice =
|
||||
builtinStart < builtinCount ? BUILTIN_POSES.slice(builtinStart, builtinEnd) : []
|
||||
|
||||
// Calculate how many db items to fetch
|
||||
const remainingLimit = limit - builtinSlice.length
|
||||
const dbSkip = Math.max(0, skip - builtinCount)
|
||||
|
||||
let formattedCustomPoses: PoseItem[] = []
|
||||
|
||||
if (remainingLimit > 0) {
|
||||
const customPoses = await prisma.pose.findMany({
|
||||
include: { image: true },
|
||||
orderBy: { id: 'desc' },
|
||||
skip: dbSkip,
|
||||
take: remainingLimit,
|
||||
})
|
||||
|
||||
formattedCustomPoses = customPoses.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
type: 'custom',
|
||||
imageUrl: `/files/${p.image.path}`,
|
||||
}))
|
||||
}
|
||||
|
||||
return [...builtinSlice, ...formattedCustomPoses]
|
||||
return customPoses.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
imageUrl: `/files/${p.image.path}`,
|
||||
}))
|
||||
}
|
||||
|
||||
async createPose(name: string, file: File) {
|
||||
@@ -71,16 +43,11 @@ export class PoseService {
|
||||
return {
|
||||
id: pose.id,
|
||||
name: pose.name,
|
||||
type: 'custom',
|
||||
imageUrl: `/files/${pose.image.path}`,
|
||||
} as PoseItem
|
||||
}
|
||||
|
||||
async deletePose(id: string) {
|
||||
if (BUILTIN_POSES.some((p) => p.id === id)) {
|
||||
throw new Error('Cannot delete builtin pose')
|
||||
}
|
||||
|
||||
const pose = await prisma.pose.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
@@ -96,10 +63,6 @@ export class PoseService {
|
||||
// Delete image asset
|
||||
await assetService.deleteAsset(pose.imageId)
|
||||
}
|
||||
|
||||
getBuiltinPose(id: string) {
|
||||
return BUILTIN_POSES.find((p) => p.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
export const poseService = new PoseService()
|
||||
@@ -63,22 +63,18 @@ export default function Expressions() {
|
||||
<VibeCard
|
||||
index={index}
|
||||
name={expression.name}
|
||||
subtitle={expression.type === 'builtin' ? 'SYSTEM' : 'CUSTOM'}
|
||||
subtitle="CUSTOM"
|
||||
image={expression.imageUrl}
|
||||
color={expression.type === 'builtin' ? '#10b981' : '#f59e0b'}
|
||||
color="#f59e0b"
|
||||
onClick={() => {}}
|
||||
actions={
|
||||
expression.type === 'custom'
|
||||
? [
|
||||
{
|
||||
name: 'Delete',
|
||||
onClick: () => setDeleteId(expression.id),
|
||||
variant: 'destructive',
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
name: 'Delete',
|
||||
onClick: () => setDeleteId(expression.id),
|
||||
variant: 'destructive',
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
+10
-14
@@ -63,22 +63,18 @@ export default function Poses() {
|
||||
<VibeCard
|
||||
index={index}
|
||||
name={pose.name}
|
||||
subtitle={pose.type === 'builtin' ? 'SYSTEM' : 'CUSTOM'}
|
||||
subtitle="CUSTOM"
|
||||
image={pose.imageUrl}
|
||||
color={pose.type === 'builtin' ? '#3b82f6' : '#f59e0b'}
|
||||
color="#f59e0b"
|
||||
onClick={() => {}}
|
||||
actions={
|
||||
pose.type === 'custom'
|
||||
? [
|
||||
{
|
||||
name: 'Delete',
|
||||
onClick: () => setDeleteId(pose.id),
|
||||
variant: 'destructive',
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
name: 'Delete',
|
||||
onClick: () => setDeleteId(pose.id),
|
||||
variant: 'destructive',
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user