feat(solo): BatchEnqueueBlockerDialog component
Nieuw dialoogvenster dat gebruiker waarschuwt bij gedetecteerde blocker: toont blockerReason in NL, prefixCount taken vóór blokkade, confirm-knop (disabled met tooltip bij count=0) en annuleer-knop. 7 tests voor rendering, click-handlers en disabled-state.
This commit is contained in:
parent
3ca842ff80
commit
80a7d793b6
2 changed files with 201 additions and 0 deletions
114
__tests__/components/solo/batch-enqueue-blocker-dialog.test.tsx
Normal file
114
__tests__/components/solo/batch-enqueue-blocker-dialog.test.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// @vitest-environment jsdom
|
||||
import '@testing-library/jest-dom'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
|
||||
vi.mock('@/components/ui/dialog', () => ({
|
||||
Dialog: ({ open, children }: { open: boolean; onOpenChange?: (v: boolean) => void; children: React.ReactNode }) =>
|
||||
open ? <div data-testid="dialog">{children}</div> : null,
|
||||
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: React.ReactNode }) => <h2>{children}</h2>,
|
||||
}))
|
||||
vi.mock('@/components/ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
variant,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
variant?: string
|
||||
}) => (
|
||||
<button onClick={onClick} disabled={disabled} data-variant={variant}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ render: r, children }: { render?: React.ReactElement; children?: React.ReactNode }) =>
|
||||
r ? <>{r}</> : <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<span data-testid="tooltip-content">{children}</span>
|
||||
),
|
||||
}))
|
||||
|
||||
import { BatchEnqueueBlockerDialog } from '@/components/solo/batch-enqueue-blocker-dialog'
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
open: true,
|
||||
onOpenChange: vi.fn(),
|
||||
prefixCount: 3,
|
||||
blockerReason: 'task-review' as const,
|
||||
blockerLabel: 'Story X — Task Y (in review)',
|
||||
onConfirm: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('BatchEnqueueBlockerDialog', () => {
|
||||
it('renders title and blocker info for task-review', () => {
|
||||
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} />)
|
||||
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Blokkade gedetecteerd')
|
||||
expect(screen.getByText(/Een taak staat op 'review'/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Story X — Task Y/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders correct blocker label for pbi-blocked', () => {
|
||||
render(
|
||||
<BatchEnqueueBlockerDialog
|
||||
{...DEFAULT_PROPS}
|
||||
blockerReason="pbi-blocked"
|
||||
blockerLabel="PBI Z — geblokkeerd"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText(/De PBI is geblokkeerd/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/PBI Z/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onConfirm when primary button is clicked', () => {
|
||||
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/Stuur 3 taken tot aan blokkade/))
|
||||
|
||||
expect(DEFAULT_PROPS.onConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls onCancel when cancel button is clicked', () => {
|
||||
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Annuleer'))
|
||||
|
||||
expect(DEFAULT_PROPS.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('disables confirm button and shows tooltip when prefixCount is 0', () => {
|
||||
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} prefixCount={0} />)
|
||||
|
||||
const confirmBtn = screen.getByText(/Stuur 0/).closest('button')
|
||||
expect(confirmBtn).toBeDisabled()
|
||||
expect(screen.getByTestId('tooltip-content')).toHaveTextContent('Geen taken vóór blokkade')
|
||||
})
|
||||
|
||||
it('does not render when open is false', () => {
|
||||
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} open={false} />)
|
||||
|
||||
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses singular taak when prefixCount is 1', () => {
|
||||
render(<BatchEnqueueBlockerDialog {...DEFAULT_PROPS} prefixCount={1} />)
|
||||
|
||||
expect(screen.getByText(/Stuur 1 taak tot aan blokkade/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/1 taak vóór de blokkade/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
87
components/solo/batch-enqueue-blocker-dialog.tsx
Normal file
87
components/solo/batch-enqueue-blocker-dialog.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
'use client'
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
interface BatchEnqueueBlockerDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (v: boolean) => void
|
||||
prefixCount: number
|
||||
blockerReason: 'task-review' | 'pbi-blocked'
|
||||
blockerLabel: string
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const BLOCKER_REASON_LABELS: Record<BatchEnqueueBlockerDialogProps['blockerReason'], string> = {
|
||||
'task-review': "Een taak staat op 'review'",
|
||||
'pbi-blocked': 'De PBI is geblokkeerd',
|
||||
}
|
||||
|
||||
export function BatchEnqueueBlockerDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
prefixCount,
|
||||
blockerReason,
|
||||
blockerLabel,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: BatchEnqueueBlockerDialogProps) {
|
||||
const noTasksBeforeBlocker = prefixCount === 0
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Blokkade gedetecteerd</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 py-2 text-sm text-foreground">
|
||||
<p>
|
||||
{BLOCKER_REASON_LABELS[blockerReason]}:{' '}
|
||||
<span className="font-medium">{blockerLabel}</span>.
|
||||
</p>
|
||||
{noTasksBeforeBlocker ? (
|
||||
<p className="text-muted-foreground">Er zijn geen taken vóór de blokkade om in te plannen.</p>
|
||||
) : (
|
||||
<p>
|
||||
{prefixCount === 1
|
||||
? `Er is ${prefixCount} taak vóór de blokkade.`
|
||||
: `Er zijn ${prefixCount} taken vóór de blokkade.`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-outline-variant">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Annuleer
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
disabled={noTasksBeforeBlocker}
|
||||
>
|
||||
{prefixCount === 1
|
||||
? `Stuur ${prefixCount} taak tot aan blokkade`
|
||||
: `Stuur ${prefixCount} taken tot aan blokkade`}
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{noTasksBeforeBlocker && (
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
Geen taken vóór blokkade
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue