feat(product-dialog): conform aan dialog-pattern + entity-profile
Story 2 van PBI "Alle dialogen conform docs/patterns/dialog.md". - lib/schemas/product.ts — gedeeld zod-schema (Dialog API) - actions/products.ts — createProductAction/updateProductAction returnen nu code+fieldErrors voor 422-validatie en code: 403 voor demo/auth - ProductDialog adopt useDirtyCloseGuard, useDialogSubmitShortcut, entityDialog* layout-classes; 422-fieldErrors mappen naar form.setError - docs/specs/dialogs/product.md — entity-profile Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b05c4d241b
commit
03a248b0fb
6 changed files with 310 additions and 181 deletions
|
|
@ -10,8 +10,10 @@ import { SessionData, sessionOptions } from '@/lib/session'
|
|||
import { Role } from '@prisma/client'
|
||||
import { isValidCode, MAX_CODE_LENGTH, normalizeCode } from '@/lib/code'
|
||||
import { productAccessFilter } from '@/lib/product-access'
|
||||
import { productSchema as productInput, type ProductInput } from '@/lib/schemas/product'
|
||||
|
||||
const productSchema = z.object({
|
||||
// Legacy FormData schema for ProductForm components (other constraints than dialog)
|
||||
const productFormDataSchema = z.object({
|
||||
name: z.string().min(1, 'Naam is verplicht').max(100, 'Naam mag maximaal 100 tekens bevatten'),
|
||||
code: z
|
||||
.string()
|
||||
|
|
@ -29,25 +31,13 @@ const productSchema = z.object({
|
|||
.max(500, 'Definition of Done mag maximaal 500 tekens bevatten'),
|
||||
})
|
||||
|
||||
// Dialog-based schema (data-object API)
|
||||
const productInput = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
code: z.string().max(20).optional(),
|
||||
description: z.string().max(4000).optional(),
|
||||
repo_url: z
|
||||
.string()
|
||||
.url()
|
||||
.regex(/^https:\/\/github\.com\//)
|
||||
.optional()
|
||||
.nullable(),
|
||||
definition_of_done: z.string().max(4000).optional(),
|
||||
auto_pr: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export type ProductInput = z.infer<typeof productInput>
|
||||
|
||||
type ProductActionResult = { success: true; productId: string } | { error: string }
|
||||
type UpdateProductResult = { success: true } | { error: string }
|
||||
type ProductFieldErrors = Partial<Record<keyof ProductInput, string[]>>
|
||||
type ProductActionResult =
|
||||
| { success: true; productId: string }
|
||||
| { error: string; code?: number; fieldErrors?: ProductFieldErrors }
|
||||
type UpdateProductResult =
|
||||
| { success: true }
|
||||
| { error: string; code?: number; fieldErrors?: ProductFieldErrors }
|
||||
|
||||
async function getSession() {
|
||||
return getIronSession<SessionData>(await cookies(), sessionOptions)
|
||||
|
|
@ -56,20 +46,30 @@ async function getSession() {
|
|||
// Data-object API used by ProductDialog
|
||||
export async function createProductAction(data: ProductInput): Promise<ProductActionResult> {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
|
||||
if (!session.userId) return { error: 'Niet ingelogd', code: 403 }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus', code: 403 }
|
||||
|
||||
const parsed = productInput.safeParse(data)
|
||||
if (!parsed.success) return { error: parsed.error.flatten().formErrors[0] ?? 'Ongeldige invoer' }
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
error: 'Validatie mislukt',
|
||||
code: 422,
|
||||
fieldErrors: parsed.error.flatten().fieldErrors as ProductFieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const code = normalizeCode(parsed.data.code)
|
||||
if (code !== null && !isValidCode(code)) {
|
||||
return { error: 'Code mag alleen letters, cijfers, punten, koppeltekens of underscores bevatten' }
|
||||
return {
|
||||
error: 'Validatie mislukt',
|
||||
code: 422,
|
||||
fieldErrors: { code: ['Alleen letters, cijfers, punten, koppeltekens of underscores'] },
|
||||
}
|
||||
}
|
||||
|
||||
if (code) {
|
||||
const dup = await prisma.product.findFirst({ where: { user_id: session.userId, code } })
|
||||
if (dup) return { error: 'Code is al in gebruik' }
|
||||
if (dup) return { error: 'Validatie mislukt', code: 422, fieldErrors: { code: ['Code is al in gebruik'] } }
|
||||
}
|
||||
|
||||
const userId = session.userId
|
||||
|
|
@ -97,21 +97,31 @@ export async function createProductAction(data: ProductInput): Promise<ProductAc
|
|||
// Data-object API used by ProductDialog
|
||||
export async function updateProductAction(id: string, data: ProductInput): Promise<UpdateProductResult> {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
|
||||
if (!session.userId) return { error: 'Niet ingelogd', code: 403 }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus', code: 403 }
|
||||
|
||||
const parsed = productInput.safeParse(data)
|
||||
if (!parsed.success) return { error: parsed.error.flatten().formErrors[0] ?? 'Ongeldige invoer' }
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
error: 'Validatie mislukt',
|
||||
code: 422,
|
||||
fieldErrors: parsed.error.flatten().fieldErrors as ProductFieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const product = await prisma.product.findFirst({
|
||||
where: { id, ...productAccessFilter(session.userId) },
|
||||
select: { id: true },
|
||||
})
|
||||
if (!product) return { error: 'Product niet gevonden of geen toegang' }
|
||||
if (!product) return { error: 'Product niet gevonden of geen toegang', code: 403 }
|
||||
|
||||
const code = normalizeCode(parsed.data.code)
|
||||
if (code !== null && !isValidCode(code)) {
|
||||
return { error: 'Code mag alleen letters, cijfers, punten, koppeltekens of underscores bevatten' }
|
||||
return {
|
||||
error: 'Validatie mislukt',
|
||||
code: 422,
|
||||
fieldErrors: { code: ['Alleen letters, cijfers, punten, koppeltekens of underscores'] },
|
||||
}
|
||||
}
|
||||
|
||||
const userId = session.userId
|
||||
|
|
@ -149,7 +159,7 @@ export async function createProductFormAction(_prevState: unknown, formData: For
|
|||
if (!session.userId) return { error: 'Niet ingelogd' }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
|
||||
|
||||
const parsed = productSchema.safeParse({
|
||||
const parsed = productFormDataSchema.safeParse({
|
||||
name: formData.get('name'),
|
||||
code: (formData.get('code') as string) || undefined,
|
||||
description: formData.get('description') || undefined,
|
||||
|
|
@ -198,7 +208,7 @@ export async function updateProductFormAction(_prevState: unknown, formData: For
|
|||
const id = formData.get('id') as string
|
||||
if (!id) return { error: 'Product niet gevonden' }
|
||||
|
||||
const parsed = productSchema.safeParse({
|
||||
const parsed = productFormDataSchema.safeParse({
|
||||
name: formData.get('name'),
|
||||
code: (formData.get('code') as string) || undefined,
|
||||
description: formData.get('description') || undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue