feat(M13): auto_pr toggle in product settings — server action + UI component + tests

This commit is contained in:
Janpeter Visser 2026-05-01 13:25:42 +02:00
parent a48f17a705
commit a0256d1859
4 changed files with 128 additions and 0 deletions

View file

@ -0,0 +1,46 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
vi.mock('@/actions/products', () => ({
updateAutoPrAction: vi.fn(),
}))
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
import { updateAutoPrAction } from '@/actions/products'
import { AutoPrToggle } from '@/components/products/auto-pr-toggle'
const mockAction = updateAutoPrAction as ReturnType<typeof vi.fn>
beforeEach(() => {
vi.clearAllMocks()
mockAction.mockResolvedValue({ success: true })
})
describe('AutoPrToggle', () => {
it('renders in off state with aria-checked=false', () => {
render(<AutoPrToggle productId="prod-1" initialValue={false} />)
const toggle = screen.getByRole('switch')
expect(toggle).toHaveAttribute('aria-checked', 'false')
})
it('renders in on state with aria-checked=true', () => {
render(<AutoPrToggle productId="prod-1" initialValue={true} />)
const toggle = screen.getByRole('switch')
expect(toggle).toHaveAttribute('aria-checked', 'true')
})
it('calls updateAutoPrAction with true when toggled on', async () => {
render(<AutoPrToggle productId="prod-1" initialValue={false} />)
fireEvent.click(screen.getByRole('switch'))
expect(mockAction).toHaveBeenCalledWith('prod-1', true)
})
it('calls updateAutoPrAction with false when toggled off', async () => {
render(<AutoPrToggle productId="prod-1" initialValue={true} />)
fireEvent.click(screen.getByRole('switch'))
expect(mockAction).toHaveBeenCalledWith('prod-1', false)
})
})

View file

@ -250,3 +250,19 @@ export async function leaveProductAction(productId: string) {
revalidatePath('/settings')
return { success: true }
}
export async function updateAutoPrAction(id: string, auto_pr: boolean) {
const session = await getSession()
if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
const parsed = z.object({ auto_pr: z.boolean() }).safeParse({ auto_pr })
if (!parsed.success) return { error: 'Ongeldige waarde voor auto_pr' }
const product = await prisma.product.findFirst({ where: { id, user_id: session.userId } })
if (!product) return { error: 'Product niet gevonden' }
await prisma.product.update({ where: { id }, data: { auto_pr: parsed.data.auto_pr } })
revalidatePath(`/products/${id}/settings`)
return { success: true }
}

View file

@ -7,6 +7,7 @@ import { ProductForm } from '@/components/products/product-form'
import { ArchiveProductButton } from '@/components/products/archive-product-button'
import { TeamManager } from '@/components/products/team-manager'
import { updateProductAction } from '@/actions/products'
import { AutoPrToggle } from '@/components/products/auto-pr-toggle'
import Link from 'next/link'
interface Props {
@ -55,6 +56,16 @@ export default async function ProductSettingsPage({ params }: Props) {
}}
/>
<div className="mt-8 pt-6 border-t border-border space-y-3">
<div>
<h2 className="text-sm font-medium text-foreground">Agent-instellingen</h2>
<p className="text-xs text-muted-foreground mt-0.5">
Automatiseer acties na een succesvolle agent-job.
</p>
</div>
<AutoPrToggle productId={id} initialValue={product.auto_pr} />
</div>
<div className="mt-8 pt-6 border-t border-border space-y-3">
<div>
<h2 className="text-sm font-medium text-foreground">Team</h2>

View file

@ -0,0 +1,55 @@
'use client'
import { useState, useTransition } from 'react'
import { cn } from '@/lib/utils'
import { updateAutoPrAction } from '@/actions/products'
import { toast } from 'sonner'
interface AutoPrToggleProps {
productId: string
initialValue: boolean
}
export function AutoPrToggle({ productId, initialValue }: AutoPrToggleProps) {
const [enabled, setEnabled] = useState(initialValue)
const [isPending, startTransition] = useTransition()
function handleToggle() {
const newValue = !enabled
setEnabled(newValue)
startTransition(async () => {
const result = await updateAutoPrAction(productId, newValue)
if (result.error) {
setEnabled(!newValue)
toast.error(typeof result.error === 'string' ? result.error : 'Opslaan mislukt')
}
})
}
return (
<div className="flex items-center gap-3">
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={handleToggle}
disabled={isPending}
className={cn(
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent',
'transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'disabled:cursor-not-allowed disabled:opacity-50',
enabled ? 'bg-primary' : 'bg-input',
)}
>
<span
className={cn(
'pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm',
'transition-transform duration-200',
enabled ? 'translate-x-4' : 'translate-x-0',
)}
/>
</button>
<span className="text-sm text-foreground">Automatisch PR aanmaken na succesvolle agent-job</span>
</div>
)
}