feat(M13): auto_pr toggle in product settings — server action + UI component + tests
This commit is contained in:
parent
a48f17a705
commit
a0256d1859
4 changed files with 128 additions and 0 deletions
46
__tests__/components/products/auto-pr-toggle.test.tsx
Normal file
46
__tests__/components/products/auto-pr-toggle.test.tsx
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
55
components/products/auto-pr-toggle.tsx
Normal file
55
components/products/auto-pr-toggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue