Sprint: Verbeteren debug mode (#179)
* feat(PBI-49): add debugProps helper + Vitest test
Adds lib/debug.ts with debugProps(id, component, file) that returns
data-debug-id and data-debug-label attrs in dev mode, empty object in
production. Adds __tests__/lib/debug.test.ts covering both modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(PBI-49): add debug-id pattern doc + CLAUDE.md reference
Adds docs/patterns/debug-id.md documenting the named-component boundary
rule (6 punten), helper-voorbeeld, skip-criteria en motivatie voor
handmatige pad-argumenten. Voegt verwijzing toe aan CLAUDE.md
patterns-tabel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(PBI-49): migrate 17 shared/ components to debugProps helper
Replace hardcoded data-debug-id + data-debug-label attribute pairs with
{...debugProps(id, component, file)} spread in all 17 components/shared/
files. Existing debug-ids preserved unchanged.
* feat(PBI-49): add debugProps to backlog/, sprint/, solo/ components
* feat(PBI-49): add debugProps to jobs/ + ideas/ components
* feat(PBI-49): add debugProps to products/ + settings/ + notifications/ components
* feat(PBI-49): add debugProps to admin/ + dashboard/ + dialogs/ + mobile/ + split-pane/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(PBI-49): use attr(data-debug-id) for debug tooltip in globals.css
* refactor(PBI-49): remove data-debug-label from debugProps helper + test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(PBI-49): strip unused component/file args from debugProps in shared/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to StatusBar, NavBar, PanelNavBar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to components/sprint/*
- new-sprint-dialog: __submit on submit button
- sprint-backlog: __list on SprintBacklogLeft + SprintBacklogRight scroll areas
- sprint-board-client: root wrapper div (display:contents) + __drag-overlay
- sprint-header: __title on goal button, __dates on dates button, __actions on action cluster
- sprint-run-controls: root on controls div, __start/__cancel on action buttons; __blockers-dialog on dialog content
- start-sprint-button: root on trigger button, __dialog on dialog content, __submit on submit button
- sync-active-sprint-cookie: no debug-id (returns null, side-effect only), comment added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to components/backlog/*
* feat(PBI-49): add BEM sub-element data-debug-id to components/ideas/*
* feat(PBI-49): add BEM sub-element data-debug-id to components/dashboard/* + components/markdown.tsx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to new-product-button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to components/solo/*
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-elements to nav-status-indicators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to components/jobs/*
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to components/products/*
* feat(PBI-49): add BEM sub-element data-debug-id to components/notifications/*
- answer-modal: __content (scroll area), __submit (footer)
- notifications-bridge: skip comment (bridge, non-rendering wrapper)
- notifications-realtime-mount: skip comment (returns null)
- notifications-sheet: __header, __items (questions list)
- push-toggle: __switch (button), __label (button text) on subscribed/unsubscribed states
* feat(PBI-49): add BEM sub-element data-debug-id to components/settings/*
- leave-product-button: root only (single-button component)
- min-quota-editor: __input (number input), __save (save button)
- profile-editor: __username (bio/short-description input), __save (submit)
- role-manager: __roles (checkbox list), __add (save button)
- token-manager: __tokens (active tokens list), __generate (create button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(PBI-49): add BEM sub-element data-debug-id to admin, auth, dialogs, entity-dialog, mobile, split-pane
* docs(PBI-49): add debug-labels BEM pattern doc + CLAUDE.md entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ce43f7720a
commit
d292e445d9
93 changed files with 600 additions and 218 deletions
|
|
@ -98,6 +98,8 @@ Volledige MCP-tool documentatie: [docs/runbooks/mcp-integration.md](./docs/runbo
|
|||
| Story met UI-component | `docs/patterns/story-with-ui-component.md` |
|
||||
| Web Push | `docs/patterns/web-push.md` |
|
||||
| Job-config resolver (PBI-67) | `lib/job-config.ts` ↔ `scrum4me-mcp/src/lib/job-config.ts` |
|
||||
| Debug-id op component-root | `docs/patterns/debug-id.md` |
|
||||
| Debug-labels (BEM) | `docs/patterns/debug-labels.md` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
23
__tests__/lib/debug.test.ts
Normal file
23
__tests__/lib/debug.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
describe('debugProps', () => {
|
||||
it('returns data-debug-id attr in dev mode', () => {
|
||||
const result = debugProps('sprint-board', 'SprintBoard', 'components/sprint/sprint-board.tsx')
|
||||
expect(result).toEqual({
|
||||
'data-debug-id': 'sprint-board',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns empty object in production mode', () => {
|
||||
const original = process.env.NODE_ENV
|
||||
try {
|
||||
vi.stubEnv('NODE_ENV', 'production')
|
||||
const result = debugProps('sprint-board', 'SprintBoard', 'components/sprint/sprint-board.tsx')
|
||||
expect(result).toEqual({})
|
||||
} finally {
|
||||
vi.stubEnv('NODE_ENV', original ?? 'test')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -11,7 +11,7 @@ body.debug-mode [data-debug-id] {
|
|||
position: relative;
|
||||
}
|
||||
body.debug-mode [data-debug-id]:hover::after {
|
||||
content: attr(data-debug-label);
|
||||
content: attr(data-debug-id);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { cancelJobAction, deleteJobAction } from '@/actions/admin/jobs'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type Job = {
|
||||
id: string
|
||||
|
|
@ -100,7 +101,7 @@ function JobRow({ job }: { job: Job }) {
|
|||
|
||||
function StatusTable({ jobs }: { jobs: Job[] }) {
|
||||
return (
|
||||
<Table>
|
||||
<Table data-debug-id="admin-jobs-table__table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
|
|
@ -171,7 +172,7 @@ function CostRow({ job }: { job: Job }) {
|
|||
|
||||
function CostsTable({ jobs }: { jobs: Job[] }) {
|
||||
return (
|
||||
<Table>
|
||||
<Table data-debug-id="admin-jobs-table__table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
|
|
@ -203,8 +204,8 @@ export function JobsTable({ jobs }: { jobs: Job[] }) {
|
|||
const [view, setView] = useState<'status' | 'costs'>('status')
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-1">
|
||||
<div className="space-y-3" {...debugProps('admin-jobs-table', 'JobsTable', 'components/admin/jobs-table.tsx')}>
|
||||
<div className="flex gap-1" data-debug-id="admin-jobs-table__header">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={view === 'status' ? 'default' : 'outline'}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { adminArchiveProductAction, adminDeleteProductAction } from '@/actions/admin/products'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type Product = {
|
||||
id: string
|
||||
|
|
@ -78,8 +79,8 @@ function DeleteDialog({ product }: { product: Product }) {
|
|||
|
||||
export function ProductsTable({ products }: { products: Product[] }) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<Table {...debugProps('admin-products-table', 'ProductsTable', 'components/admin/products-table.tsx')}>
|
||||
<TableHeader data-debug-id="admin-products-table__header">
|
||||
<TableRow>
|
||||
<TableHead>Naam</TableHead>
|
||||
<TableHead>Eigenaar</TableHead>
|
||||
|
|
@ -90,7 +91,7 @@ export function ProductsTable({ products }: { products: Product[] }) {
|
|||
<TableHead className="text-right">Acties</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableBody data-debug-id="admin-products-table__table">
|
||||
{products.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
updateUserRolesAction,
|
||||
setMustResetPasswordAction,
|
||||
} from '@/actions/admin/users'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type UserWithRoles = {
|
||||
id: string
|
||||
|
|
@ -190,8 +191,8 @@ export function UsersTable({
|
|||
currentUserId: string
|
||||
}) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<Table {...debugProps('admin-users-table', 'UsersTable', 'components/admin/users-table.tsx')}>
|
||||
<TableHeader data-debug-id="admin-users-table__header">
|
||||
<TableRow>
|
||||
<TableHead>Gebruiker</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
|
|
@ -201,7 +202,7 @@ export function UsersTable({
|
|||
<TableHead className="text-right">Acties</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableBody data-debug-id="admin-users-table__table">
|
||||
{users.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.username}</TableCell>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import { useActionState } from 'react'
|
|||
import { useFormStatus } from 'react-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type ActionResult = { error: string | Record<string, string[]> } | undefined
|
||||
|
||||
function SubmitButton({ label }: { label: string }) {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" className="w-full" disabled={pending}>
|
||||
<Button type="submit" className="w-full" disabled={pending} data-debug-id="auth-form__submit">
|
||||
{pending ? 'Even wachten…' : label}
|
||||
</Button>
|
||||
)
|
||||
|
|
@ -34,7 +35,7 @@ export function AuthForm({ action, submitLabel }: AuthFormProps) {
|
|||
const errorMessage = getErrorMessage(state)
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-4">
|
||||
<form action={formAction} className="space-y-4" {...debugProps('auth-form', 'AuthForm', 'components/auth/auth-form.tsx')}>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="username" className="text-sm font-medium text-foreground">
|
||||
Gebruikersnaam
|
||||
|
|
@ -48,6 +49,7 @@ export function AuthForm({ action, submitLabel }: AuthFormProps) {
|
|||
minLength={3}
|
||||
placeholder="jouw-naam"
|
||||
className="bg-input-background border-border focus-visible:ring-primary"
|
||||
data-debug-id="auth-form__username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -64,6 +66,7 @@ export function AuthForm({ action, submitLabel }: AuthFormProps) {
|
|||
minLength={8}
|
||||
placeholder="••••••••"
|
||||
className="bg-input-background border-border focus-visible:ring-primary"
|
||||
data-debug-id="auth-form__password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export const PRIORITY_BORDER: Record<number, string> = {
|
||||
1: 'border-l-4 border-l-priority-critical',
|
||||
|
|
@ -38,9 +39,10 @@ export const BacklogCard = forwardRef<HTMLDivElement, BacklogCardProps>(function
|
|||
className,
|
||||
)}
|
||||
{...rest}
|
||||
{...debugProps('backlog-card', 'BacklogCard', 'components/backlog/backlog-card.tsx')}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm leading-snug line-clamp-2 flex-1">{title}</p>
|
||||
<p className="text-sm leading-snug line-clamp-2 flex-1" {...debugProps('backlog-card__title')}>{title}</p>
|
||||
{code && <CodeBadge code={code} className="shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
{(badge || actions) && (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface EmptyPanelProps {
|
||||
title?: string
|
||||
|
|
@ -15,8 +16,8 @@ interface EmptyPanelProps {
|
|||
|
||||
export function EmptyPanel({ title, message, action }: EmptyPanelProps) {
|
||||
return (
|
||||
<div className="p-8 text-center text-muted-foreground space-y-3">
|
||||
{title && <p className="text-sm font-medium text-foreground">{title}</p>}
|
||||
<div className="p-8 text-center text-muted-foreground space-y-3" {...debugProps('empty-panel', 'EmptyPanel', 'components/backlog/empty-panel.tsx')}>
|
||||
{title && <p className="text-sm font-medium text-foreground" {...debugProps('empty-panel__title')}>{title}</p>}
|
||||
<p className="text-sm">{message}</p>
|
||||
{action && (
|
||||
<DemoTooltip show={action.disabled ?? false}>
|
||||
|
|
@ -25,6 +26,7 @@ export function EmptyPanel({ title, message, action }: EmptyPanelProps) {
|
|||
variant="outline"
|
||||
disabled={action.disabled}
|
||||
onClick={action.disabled ? undefined : action.onClick}
|
||||
{...debugProps('empty-panel__cta')}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
} from '@/components/shared/entity-dialog-layout'
|
||||
import { createPbiAction, updatePbiAction } from '@/actions/pbis'
|
||||
import type { PbiStatusApi } from '@/lib/task-status'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export interface PbiDialogPbi {
|
||||
id: string
|
||||
|
|
@ -120,6 +121,7 @@ export function PbiDialog({ state, onClose, isDemo = false }: PbiDialogProps) {
|
|||
showCloseButton={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={entityDialogContentClasses}
|
||||
{...debugProps('pbi-dialog', 'PbiDialog', 'components/backlog/pbi-dialog.tsx')}
|
||||
>
|
||||
<div className={entityDialogHeaderClasses}>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
|
|
@ -168,6 +170,7 @@ export function PbiDialog({ state, onClose, isDemo = false }: PbiDialogProps) {
|
|||
disabled={isDemo}
|
||||
aria-invalid={!!fieldError('title')}
|
||||
className={fieldError('title') ? 'border-error' : ''}
|
||||
{...debugProps('pbi-dialog__title')}
|
||||
/>
|
||||
{fieldError('title') && <p className="text-xs text-error">{fieldError('title')}</p>}
|
||||
</div>
|
||||
|
|
@ -207,7 +210,7 @@ export function PbiDialog({ state, onClose, isDemo = false }: PbiDialogProps) {
|
|||
Annuleren
|
||||
</Button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button type="submit" form="pbi-form" disabled={pending || isDemo}>
|
||||
<Button type="submit" form="pbi-form" disabled={pending || isDemo} {...debugProps('pbi-dialog__submit')}>
|
||||
{pending ? '…' : isEdit ? 'Opslaan' : 'Aanmaken'}
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { useBacklogStore } from '@/stores/backlog-store'
|
|||
import { deletePbiAction } from '@/actions/pbis'
|
||||
import { reorderPbisAction, updatePbiPriorityAction } from '@/actions/stories'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { PbiDialog, type PbiDialogState } from './pbi-dialog'
|
||||
import { BacklogCard } from './backlog-card'
|
||||
import { EmptyPanel } from './empty-panel'
|
||||
|
|
@ -390,8 +391,8 @@ export function PbiList({ productId, isDemo }: PbiListProps) {
|
|||
const activePbi = activeDragId ? pbiMap[activeDragId] : null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-border bg-surface-container-low shrink-0">
|
||||
<div className="flex flex-col h-full" {...debugProps('pbi-list', 'PbiList', 'components/backlog/pbi-list.tsx')}>
|
||||
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-border bg-surface-container-low shrink-0" {...debugProps('pbi-list__header')}>
|
||||
{filterPriority !== 'all' && (
|
||||
<button
|
||||
onClick={() => setFilterPriority('all')}
|
||||
|
|
@ -551,7 +552,7 @@ export function PbiList({ productId, isDemo }: PbiListProps) {
|
|||
items={filtered.map(p => p.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="p-3 flex flex-col gap-2">
|
||||
<div className="p-3 flex flex-col gap-2" {...debugProps('pbi-list__items')}>
|
||||
{filtered.map(pbi => (
|
||||
<SortablePbiRow
|
||||
key={pbi.id}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
} from '@/components/shared/entity-dialog-layout'
|
||||
import { createStoryAction, updateStoryAction, deleteStoryAction, getStoryLogsAction } from '@/actions/stories'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import type { Story } from './story-panel'
|
||||
|
||||
export type StoryDialogState =
|
||||
|
|
@ -147,6 +148,7 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
|
|||
showCloseButton={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={entityDialogContentClasses}
|
||||
{...debugProps('story-dialog', 'StoryDialog', 'components/backlog/story-dialog.tsx')}
|
||||
>
|
||||
<div className={cn(entityDialogHeaderClasses, 'flex-col items-stretch gap-1')}>
|
||||
<div className="flex items-start gap-2">
|
||||
|
|
@ -219,6 +221,7 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
|
|||
disabled={isDemo}
|
||||
aria-invalid={!!fieldError('title')}
|
||||
className={fieldError('title') ? 'border-error' : ''}
|
||||
{...debugProps('story-dialog__title')}
|
||||
/>
|
||||
{fieldError('title') && <p className="text-xs text-error">{fieldError('title')}</p>}
|
||||
</div>
|
||||
|
|
@ -317,7 +320,7 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
|
|||
Annuleren
|
||||
</Button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button type="submit" form="story-form" disabled={pending || isDemo}>
|
||||
<Button type="submit" form="story-form" disabled={pending || isDemo} {...debugProps('story-dialog__submit')}>
|
||||
{pending ? '…' : isEdit ? 'Opslaan' : 'Aanmaken'}
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { usePlannerStore } from '@/stores/planner-store'
|
|||
import { useBacklogStore } from '@/stores/backlog-store'
|
||||
import { reorderStoriesAction } from '@/actions/stories'
|
||||
import { StoryDialog, type StoryDialogState } from './story-dialog'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { BacklogCard } from './backlog-card'
|
||||
import { EmptyPanel } from './empty-panel'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
|
|
@ -210,7 +211,7 @@ export function StoryPanel({ productId, isDemo }: StoryPanelProps) {
|
|||
const hasActiveFilters = filterStatus !== null || filterPriority !== null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...debugProps('story-panel', 'StoryPanel', 'components/backlog/story-panel.tsx')}>
|
||||
<PanelNavBar
|
||||
title="Stories"
|
||||
actions={
|
||||
|
|
@ -260,7 +261,7 @@ export function StoryPanel({ productId, isDemo }: StoryPanelProps) {
|
|||
}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="flex-1 overflow-y-auto p-4" {...debugProps('story-panel__tasks')}>
|
||||
{selectedPbiId === null ? (
|
||||
<EmptyPanel message="Selecteer een PBI om de stories te bekijken." />
|
||||
) : rawStories.length === 0 ? (
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { useSelectionStore } from '@/stores/selection-store'
|
|||
import { useBacklogStore, type BacklogTask } from '@/stores/backlog-store'
|
||||
import { reorderTasksAction } from '@/actions/tasks'
|
||||
import { BacklogCard } from './backlog-card'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { EmptyPanel } from './empty-panel'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
|
@ -152,15 +153,18 @@ export function TaskPanel({ isDemo, closePath }: TaskPanelProps) {
|
|||
if (!selectedStoryId) return
|
||||
router.push(`${closePath}?newTask=1&storyId=${selectedStoryId}`)
|
||||
}}
|
||||
{...debugProps('task-panel__actions')}
|
||||
>
|
||||
+ Nieuwe taak
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
)
|
||||
|
||||
const dp = debugProps('task-panel', 'TaskPanel', 'components/backlog/task-panel.tsx')
|
||||
|
||||
if (tasks === null) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...dp}>
|
||||
<PanelNavBar title="Taken" actions={navActions} />
|
||||
<EmptyPanel message="Selecteer een story om de taken te bekijken." />
|
||||
</div>
|
||||
|
|
@ -169,7 +173,7 @@ export function TaskPanel({ isDemo, closePath }: TaskPanelProps) {
|
|||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...dp}>
|
||||
<PanelNavBar title="Taken" actions={navActions} />
|
||||
<EmptyPanel
|
||||
message="Nog geen taken voor deze story."
|
||||
|
|
@ -186,7 +190,7 @@ export function TaskPanel({ isDemo, closePath }: TaskPanelProps) {
|
|||
const activeTask = activeDragId ? tasks.find((t) => t.id === activeDragId) : null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...dp}>
|
||||
<PanelNavBar title="Taken" actions={navActions} />
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<DndContext
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState } from 'react'
|
|||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ProductDialog } from '@/components/dialogs/product-dialog'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export function NewProductButton() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
@ -11,7 +12,9 @@ export function NewProductButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>+ Nieuw product</Button>
|
||||
<div {...debugProps('new-product-button', 'NewProductButton', 'components/dashboard/new-product-button.tsx')}>
|
||||
<Button onClick={() => setOpen(true)} data-debug-id="new-product-button__trigger">+ Nieuw product</Button>
|
||||
</div>
|
||||
<ProductDialog
|
||||
mode="create"
|
||||
open={open}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
|||
import { restoreProductAction } from '@/actions/products'
|
||||
import { setActiveProductAction } from '@/actions/active-product'
|
||||
import { ProductDialog, type ProductDialogProduct } from '@/components/dialogs/product-dialog'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
|
|
@ -53,7 +54,7 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
|
|||
|
||||
if (products.length === 0) {
|
||||
return (
|
||||
<div className="bg-surface-container-low rounded-xl border border-border p-12 text-center space-y-3">
|
||||
<div className="bg-surface-container-low rounded-xl border border-border p-12 text-center space-y-3" {...debugProps('product-list', 'ProductList', 'components/dashboard/product-list.tsx')}>
|
||||
<p className="text-muted-foreground">
|
||||
{showArchived
|
||||
? 'Geen gearchiveerde producten.'
|
||||
|
|
@ -69,7 +70,7 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-3" {...debugProps('product-list', 'ProductList', 'components/dashboard/product-list.tsx')}>
|
||||
{products.map(product => (
|
||||
<div
|
||||
key={product.id}
|
||||
|
|
@ -77,8 +78,9 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
|
|||
className={`group bg-surface-container-low border border-border rounded-xl p-4 transition-colors ${
|
||||
showArchived ? 'opacity-60' : 'cursor-pointer hover:border-primary'
|
||||
}`}
|
||||
data-debug-id="product-list__items"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start justify-between gap-4" data-debug-id="product-list__header">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{product.code && <CodeBadge code={product.code} />}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
import { productSchema, type ProductInput } from '@/lib/schemas/product'
|
||||
import { createProductAction, updateProductAction } from '@/actions/products'
|
||||
import { useProductsStore } from '@/stores/products-store'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export interface ProductDialogProduct {
|
||||
id: string
|
||||
|
|
@ -159,6 +160,7 @@ export function ProductDialog(props: Props) {
|
|||
showCloseButton={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={entityDialogContentClasses}
|
||||
{...debugProps('product-dialog', 'ProductDialog', 'components/dialogs/product-dialog.tsx')}
|
||||
>
|
||||
<div className={entityDialogHeaderClasses}>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
|
|
@ -170,6 +172,7 @@ export function ProductDialog(props: Props) {
|
|||
id="product-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className={entityDialogBodyClasses}
|
||||
data-debug-id="product-dialog__content"
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
<label htmlFor="product-name" className="text-sm font-medium">
|
||||
|
|
@ -294,7 +297,7 @@ export function ProductDialog(props: Props) {
|
|||
Annuleren
|
||||
</Button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button type="submit" form="product-form" disabled={isPending || isDemo}>
|
||||
<Button type="submit" form="product-form" disabled={isPending || isDemo} data-debug-id="product-dialog__submit">
|
||||
{isPending ? '…' : mode === 'edit' ? 'Opslaan' : 'Aanmaken'}
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
|
|
@ -33,7 +34,7 @@ export function DirtyCloseGuard({ isDirty, onConfirm, children }: DirtyCloseGuar
|
|||
<>
|
||||
{children(attemptClose)}
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogContent size="sm" {...debugProps('dirty-close-guard', 'DirtyCloseGuard', 'components/entity-dialog/dirty-close-guard.tsx')}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Wijzigingen niet opgeslagen</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
|
|
@ -41,11 +42,12 @@ export function DirtyCloseGuard({ isDirty, onConfirm, children }: DirtyCloseGuar
|
|||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setOpen(false)}>
|
||||
<AlertDialogCancel data-debug-id="dirty-close-guard__cancel" onClick={() => setOpen(false)}>
|
||||
Blijven
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
data-debug-id="dirty-close-guard__confirm"
|
||||
onClick={() => { setOpen(false); onConfirm() }}
|
||||
>
|
||||
Weggooien
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Download } from 'lucide-react'
|
|||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { downloadIdeaMdAction } from '@/actions/ideas'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -47,6 +48,7 @@ export function DownloadMdButton({ ideaId, kind, hasContent }: Props) {
|
|||
onClick={handleClick}
|
||||
disabled={pending || !hasContent}
|
||||
title={hasContent ? `Download ${kind}_md` : 'Geen content'}
|
||||
{...debugProps('download-md-button', 'DownloadMdButton', 'components/ideas/download-md-button.tsx')}
|
||||
>
|
||||
<Download className="size-3.5 mr-1" />
|
||||
.md
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { getIdeaStatusBadge } from '@/lib/idea-status-colors'
|
|||
import type { IdeaStatusApi } from '@/lib/idea-status'
|
||||
import { isIdeaEditable } from '@/lib/idea-status'
|
||||
import type { IdeaDto } from '@/lib/idea-dto'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { updateIdeaAction, archiveIdeaAction, updateSecondaryProductsAction } from '@/actions/ideas'
|
||||
import { IdeaRowActions } from '@/components/ideas/idea-row-actions'
|
||||
import { IdeaMdEditor } from '@/components/ideas/idea-md-editor'
|
||||
|
|
@ -132,7 +133,7 @@ export function IdeaDetailLayout({
|
|||
const badge = getIdeaStatusBadge(API_TO_DB[idea.status])
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto w-full space-y-6">
|
||||
<div className="p-6 max-w-5xl mx-auto w-full space-y-6" {...debugProps('idea-detail-layout', 'IdeaDetailLayout', 'components/ideas/idea-detail-layout.tsx')}>
|
||||
{/* Breadcrumb / back-link */}
|
||||
<Link
|
||||
href="/ideas"
|
||||
|
|
@ -143,7 +144,7 @@ export function IdeaDetailLayout({
|
|||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<header className="flex flex-wrap items-start justify-between gap-4">
|
||||
<header className="flex flex-wrap items-start justify-between gap-4" data-debug-id="idea-detail-layout__header">
|
||||
<div className="space-y-1">
|
||||
<p className="font-mono text-xs text-muted-foreground">{idea.code}</p>
|
||||
<h1 className="text-2xl font-medium text-foreground">{idea.title}</h1>
|
||||
|
|
@ -183,7 +184,7 @@ export function IdeaDetailLayout({
|
|||
<IdeaPbiLinkCard idea={idea} isDemo={isDemo} />
|
||||
|
||||
{/* Tab-switcher */}
|
||||
<nav className="border-b border-input flex gap-1">
|
||||
<nav className="border-b border-input flex gap-1" data-debug-id="idea-detail-layout__main">
|
||||
{([
|
||||
{ key: 'idee' as TabKey, label: 'Idee', disabled: false, hasContent: true },
|
||||
{ key: 'grill' as TabKey, label: 'Grill', disabled: !grill_md, hasContent: !!grill_md },
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
|||
import { getIdeaStatusBadge } from '@/lib/idea-status-colors'
|
||||
import type { IdeaStatusApi } from '@/lib/idea-status'
|
||||
import type { IdeaDto } from '@/lib/idea-dto'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { createIdeaAction, archiveIdeaAction } from '@/actions/ideas'
|
||||
import { IdeaRowActions } from '@/components/ideas/idea-row-actions'
|
||||
|
||||
|
|
@ -258,9 +259,9 @@ export function IdeaList({ ideas, products, isDemo }: IdeaListProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4" {...debugProps('idea-list', 'IdeaList', 'components/ideas/idea-list.tsx')}>
|
||||
{/* Top-bar: search + nieuw-knop */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3" data-debug-id="idea-list__toolbar">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
|
|
@ -402,7 +403,7 @@ export function IdeaList({ ideas, products, isDemo }: IdeaListProps) {
|
|||
: 'Geen ideeën die aan de filters voldoen.'}
|
||||
</p>
|
||||
) : (
|
||||
<Table>
|
||||
<Table data-debug-id="idea-list__items">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-24"><SortHeader col="code" label="Code" sortKey={sortKey} sortDir={sortDir} onSort={handleSort} /></TableHead>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { toast } from 'sonner'
|
|||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { parsePlanMd, type PlanParseError } from '@/lib/idea-plan-parser'
|
||||
import { updateGrillMdAction, updatePlanMdAction } from '@/actions/ideas'
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ export function IdeaMdEditor({ ideaId, kind, initialValue, onCancel }: Props) {
|
|||
const dirty = value !== initialValue
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3" {...debugProps('idea-md-editor', 'IdeaMdEditor', 'components/ideas/idea-md-editor.tsx')}>
|
||||
{errors.length > 0 && (
|
||||
<div className="rounded-md border border-status-blocked/30 bg-status-blocked/10 p-3 space-y-1">
|
||||
<p className="text-xs font-medium text-status-blocked">
|
||||
|
|
@ -138,6 +139,7 @@ export function IdeaMdEditor({ ideaId, kind, initialValue, onCancel }: Props) {
|
|||
onKeyDown={onKeyDown}
|
||||
rows={24}
|
||||
className="font-mono text-sm leading-relaxed"
|
||||
data-debug-id="idea-md-editor__textarea"
|
||||
placeholder={
|
||||
kind === 'grill'
|
||||
? '# Idee — ...\n## Scope\n...'
|
||||
|
|
@ -159,6 +161,7 @@ export function IdeaMdEditor({ ideaId, kind, initialValue, onCancel }: Props) {
|
|||
size="sm"
|
||||
onClick={save}
|
||||
disabled={!dirty || submitting || (errors.length > 0 && kind === 'plan')}
|
||||
data-debug-id="idea-md-editor__save"
|
||||
>
|
||||
<Save className="size-3.5 mr-1" />
|
||||
Opslaan
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { ExternalLink, Link2Off } from 'lucide-react'
|
|||
import { toast } from 'sonner'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { relinkIdeaPlanAction } from '@/actions/ideas'
|
||||
import type { IdeaDto } from '@/lib/idea-dto'
|
||||
|
||||
|
|
@ -27,9 +28,9 @@ export function IdeaPbiLinkCard({ idea, isDemo }: Props) {
|
|||
|
||||
if (idea.pbi && idea.product_id) {
|
||||
return (
|
||||
<div className="rounded-md border border-status-done/30 bg-status-done/10 p-4 flex items-center gap-3">
|
||||
<div className="rounded-md border border-status-done/30 bg-status-done/10 p-4 flex items-center gap-3" {...debugProps('idea-pbi-link-card', 'IdeaPbiLinkCard', 'components/ideas/idea-pbi-link-card.tsx')}>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs uppercase tracking-wide text-status-done font-medium">
|
||||
<p className="text-xs uppercase tracking-wide text-status-done font-medium" data-debug-id="idea-pbi-link-card__title">
|
||||
Gepland
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
|
|
@ -37,6 +38,7 @@ export function IdeaPbiLinkCard({ idea, isDemo }: Props) {
|
|||
<Link
|
||||
href={`/products/${idea.product_id}`}
|
||||
className="font-medium text-status-done hover:underline inline-flex items-center gap-1"
|
||||
data-debug-id="idea-pbi-link-card__link"
|
||||
>
|
||||
{idea.pbi.code} — {idea.pbi.title}
|
||||
<ExternalLink className="size-3" />
|
||||
|
|
@ -62,7 +64,7 @@ export function IdeaPbiLinkCard({ idea, isDemo }: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-status-blocked/30 bg-status-blocked/10 p-4 space-y-2">
|
||||
<div className="rounded-md border border-status-blocked/30 bg-status-blocked/10 p-4 space-y-2" {...debugProps('idea-pbi-link-card', 'IdeaPbiLinkCard', 'components/ideas/idea-pbi-link-card.tsx')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2Off className="size-4 text-status-blocked" />
|
||||
<p className="text-sm font-medium text-status-blocked">
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
} from '@/components/ui/tooltip'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { useSoloStore } from '@/stores/solo-store'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import {
|
||||
startGrillJobAction,
|
||||
startMakePlanJobAction,
|
||||
|
|
@ -134,7 +135,7 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1" {...debugProps('idea-row-actions', 'IdeaRowActions', 'components/ideas/idea-row-actions.tsx')}>
|
||||
{/* Bekijk PBI — alleen zichtbaar in PLANNED */}
|
||||
{status === 'planned' && idea.pbi && idea.product_id && (
|
||||
<Button
|
||||
|
|
@ -148,24 +149,28 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
|
|||
)}
|
||||
|
||||
{/* Grill Me */}
|
||||
<ActionButton
|
||||
label="Grill"
|
||||
icon={<Flame className="size-3.5" />}
|
||||
enabled={grillEnabled}
|
||||
blockedReason={grillBlockedReason}
|
||||
isDemo={isDemo}
|
||||
onClick={() => runStart(startGrillJobAction)}
|
||||
/>
|
||||
<span data-debug-id="idea-row-actions__grill">
|
||||
<ActionButton
|
||||
label="Grill"
|
||||
icon={<Flame className="size-3.5" />}
|
||||
enabled={grillEnabled}
|
||||
blockedReason={grillBlockedReason}
|
||||
isDemo={isDemo}
|
||||
onClick={() => runStart(startGrillJobAction)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{/* Make Plan */}
|
||||
<ActionButton
|
||||
label="Plan"
|
||||
icon={<Sparkles className="size-3.5" />}
|
||||
enabled={makePlanEnabled}
|
||||
blockedReason={makePlanBlockedReason}
|
||||
isDemo={isDemo}
|
||||
onClick={() => runStart(startMakePlanJobAction)}
|
||||
/>
|
||||
<span data-debug-id="idea-row-actions__plan">
|
||||
<ActionButton
|
||||
label="Plan"
|
||||
icon={<Sparkles className="size-3.5" />}
|
||||
enabled={makePlanEnabled}
|
||||
blockedReason={makePlanBlockedReason}
|
||||
isDemo={isDemo}
|
||||
onClick={() => runStart(startMakePlanJobAction)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{/* Materialiseer */}
|
||||
<ActionButton
|
||||
|
|
@ -217,6 +222,7 @@ export function IdeaRowActions({ idea, isDemo, onArchive }: IdeaRowActionsProps)
|
|||
disabled={isDemo || pending}
|
||||
aria-label="Archiveer idee"
|
||||
title="Archiveer"
|
||||
data-debug-id="idea-row-actions__delete"
|
||||
>
|
||||
<Archive className="size-4" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { Badge } from '@/components/ui/badge'
|
|||
import { StoryLog } from '@/components/shared/story-log'
|
||||
import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-status'
|
||||
import type { ClaudeJobStatusApi } from '@/lib/job-status'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import type { IdeaSyncData } from '@/app/(app)/ideas/[id]/sync-tab-server'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -88,9 +89,9 @@ export function IdeaSyncTab({ data }: Props) {
|
|||
if (!pbi) return null
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4" {...debugProps('idea-sync-tab', 'IdeaSyncTab', 'components/ideas/idea-sync-tab.tsx')}>
|
||||
{/* Header: PBI-link + PR-status */}
|
||||
<div className="flex flex-wrap items-center gap-3 rounded-md border border-border bg-surface-container p-3">
|
||||
<div className="flex flex-wrap items-center gap-3 rounded-md border border-border bg-surface-container p-3" data-debug-id="idea-sync-tab__header">
|
||||
<a
|
||||
href={`/backlog/${pbi.id}`}
|
||||
className="font-mono text-sm text-primary hover:underline"
|
||||
|
|
@ -118,6 +119,7 @@ export function IdeaSyncTab({ data }: Props) {
|
|||
</div>
|
||||
|
||||
{/* Stories */}
|
||||
<div data-debug-id="idea-sync-tab__items">
|
||||
{pbi.stories.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
Deze PBI heeft nog geen stories.
|
||||
|
|
@ -228,6 +230,7 @@ export function IdeaSyncTab({ data }: Props) {
|
|||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { toast } from 'sonner'
|
|||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { answerQuestion } from '@/actions/questions'
|
||||
import { UserChatInput } from '@/components/ideas/user-chat-input'
|
||||
|
||||
|
|
@ -124,13 +125,13 @@ export function IdeaTimeline({
|
|||
const showChatInput = planMd !== null
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4" {...debugProps('idea-timeline', 'IdeaTimeline', 'components/ideas/idea-timeline.tsx')}>
|
||||
{merged.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-8 text-center italic">
|
||||
Nog geen activiteit op dit idee.
|
||||
</p>
|
||||
) : (
|
||||
<ol className="border-l-2 border-input pl-4 space-y-3 ml-2">
|
||||
<ol className="border-l-2 border-input pl-4 space-y-3 ml-2" data-debug-id="idea-timeline__items">
|
||||
{merged.map((entry, i) => {
|
||||
// Expliciete locale + format om SSR/CSR hydration-mismatch te voorkomen
|
||||
// (server-locale verschilde van browser-locale).
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { toast } from 'sonner'
|
|||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { createUserQuestionAction } from '@/actions/user-questions'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -39,7 +40,7 @@ export function UserChatInput({ ideaId, isDemo = false }: Props) {
|
|||
|
||||
if (isDemo) {
|
||||
return (
|
||||
<div className="rounded-md border border-input bg-surface-container p-3">
|
||||
<div className="rounded-md border border-input bg-surface-container p-3" {...debugProps('user-chat-input', 'UserChatInput', 'components/ideas/user-chat-input.tsx')}>
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
Demo-modus: vragen stellen is niet beschikbaar.
|
||||
</p>
|
||||
|
|
@ -48,7 +49,7 @@ export function UserChatInput({ ideaId, isDemo = false }: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-md border border-input bg-surface-container p-3">
|
||||
<div className="space-y-2 rounded-md border border-input bg-surface-container p-3" {...debugProps('user-chat-input', 'UserChatInput', 'components/ideas/user-chat-input.tsx')}>
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Stel een vraag over dit plan
|
||||
</label>
|
||||
|
|
@ -58,12 +59,14 @@ export function UserChatInput({ ideaId, isDemo = false }: Props) {
|
|||
rows={3}
|
||||
placeholder="Bijv. Waarom is gekozen voor X in plaats van Y?"
|
||||
disabled={pending}
|
||||
data-debug-id="user-chat-input__textarea"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={pending || !text.trim()}
|
||||
onClick={submit}
|
||||
data-debug-id="user-chat-input__submit"
|
||||
>
|
||||
<Send className="size-4" />
|
||||
{pending ? 'Bezig…' : 'Verzend'}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-status'
|
||||
import { jobStatusToApi } from '@/lib/job-status'
|
||||
import type { ClaudeJobKind, ClaudeJobStatus } from '@prisma/client'
|
||||
|
|
@ -61,8 +62,9 @@ export default function JobCard({
|
|||
'border rounded-lg p-3 cursor-pointer hover:bg-surface-container transition-colors text-sm',
|
||||
isSelected && 'ring-2 ring-primary',
|
||||
)}
|
||||
{...debugProps('job-card', 'JobCard', 'components/jobs/job-card.tsx')}
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex justify-between items-center gap-2" data-debug-id="job-card__status">
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded border bg-muted text-muted-foreground font-mono">
|
||||
{KIND_LABELS[kind]}
|
||||
</span>
|
||||
|
|
@ -70,8 +72,8 @@ export default function JobCard({
|
|||
{JOB_STATUS_LABELS[apiStatus]}
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-medium truncate mt-1">{titleText}</p>
|
||||
<div className="flex items-end justify-between gap-2 mt-0.5">
|
||||
<p className="font-medium truncate mt-1" data-debug-id="job-card__title">{titleText}</p>
|
||||
<div className="flex items-end justify-between gap-2 mt-0.5" data-debug-id="job-card__actions">
|
||||
<p className="text-xs text-muted-foreground truncate">{detailText}</p>
|
||||
<span className="text-[10px] text-muted-foreground shrink-0 tabular-nums">
|
||||
{new Date(createdAt).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' })}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-st
|
|||
import { jobStatusToApi } from '@/lib/job-status'
|
||||
import type { JobWithRelations } from '@/actions/jobs-page'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { restartClaudeJobAction } from '@/actions/claude-jobs'
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ export default function JobDetailPane({ job, isDemo }: JobDetailPaneProps) {
|
|||
|
||||
if (!job) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-center h-full text-sm text-muted-foreground" {...debugProps('job-detail-pane', 'JobDetailPane', 'components/jobs/job-detail-pane.tsx')}>
|
||||
Selecteer een job om details te zien
|
||||
</div>
|
||||
)
|
||||
|
|
@ -75,7 +76,8 @@ export default function JobDetailPane({ job, isDemo }: JobDetailPaneProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full p-4">
|
||||
<div className="overflow-y-auto h-full p-4" {...debugProps('job-detail-pane', 'JobDetailPane', 'components/jobs/job-detail-pane.tsx')}>
|
||||
<div data-debug-id="job-detail-pane__header">
|
||||
<FieldRow label="Status">
|
||||
<span className={cn('text-xs px-2 py-0.5 rounded-full border font-medium', JOB_STATUS_COLORS[apiStatus])}>
|
||||
{JOB_STATUS_LABELS[apiStatus]}
|
||||
|
|
@ -118,7 +120,8 @@ export default function JobDetailPane({ job, isDemo }: JobDetailPaneProps) {
|
|||
</pre>
|
||||
) : '—'}
|
||||
</FieldRow>
|
||||
<div className="pt-3 mt-3 border-t border-border/50">
|
||||
</div>
|
||||
<div className="pt-3 mt-3 border-t border-border/50" data-debug-id="job-detail-pane__body">
|
||||
<p className="text-xs text-muted-foreground mb-1.5">Beschrijving</p>
|
||||
{job.description ? (
|
||||
<pre className="text-xs whitespace-pre-wrap break-words bg-muted/40 rounded p-3 font-sans">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import type { JobWithRelations } from '@/actions/jobs-page'
|
||||
|
||||
interface FieldRowProps {
|
||||
|
|
@ -42,7 +43,7 @@ interface JobUsagePaneProps {
|
|||
export default function JobUsagePane({ job }: JobUsagePaneProps) {
|
||||
if (!job) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-center h-full text-sm text-muted-foreground" {...debugProps('job-usage-pane', 'JobUsagePane', 'components/jobs/job-usage-pane.tsx')}>
|
||||
Selecteer een job om gebruik te zien
|
||||
</div>
|
||||
)
|
||||
|
|
@ -57,15 +58,19 @@ export default function JobUsagePane({ job }: JobUsagePaneProps) {
|
|||
const costLabel = job.costUsd != null ? `$${job.costUsd.toFixed(4)}` : '—'
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full p-4">
|
||||
<FieldRow label="Model">{job.modelId ?? '—'}</FieldRow>
|
||||
<FieldRow label="Tokens invoer">{formatNumber(job.inputTokens)}</FieldRow>
|
||||
<FieldRow label="Tokens uitvoer">{formatNumber(job.outputTokens)}</FieldRow>
|
||||
<FieldRow label="Cache read">{formatNumber(job.cacheReadTokens)}</FieldRow>
|
||||
<FieldRow label="Cache write">{formatNumber(job.cacheWriteTokens)}</FieldRow>
|
||||
<FieldRow label="Tokens totaal">{formatNumber(totalTokens || null)}</FieldRow>
|
||||
<FieldRow label="Kosten (USD)">{costLabel}</FieldRow>
|
||||
<FieldRow label="Duur">{formatDuration(job.startedAt, job.finishedAt)}</FieldRow>
|
||||
<div className="overflow-y-auto h-full p-4" {...debugProps('job-usage-pane', 'JobUsagePane', 'components/jobs/job-usage-pane.tsx')}>
|
||||
<div data-debug-id="job-usage-pane__header">
|
||||
<FieldRow label="Model">{job.modelId ?? '—'}</FieldRow>
|
||||
</div>
|
||||
<div data-debug-id="job-usage-pane__table">
|
||||
<FieldRow label="Tokens invoer">{formatNumber(job.inputTokens)}</FieldRow>
|
||||
<FieldRow label="Tokens uitvoer">{formatNumber(job.outputTokens)}</FieldRow>
|
||||
<FieldRow label="Cache read">{formatNumber(job.cacheReadTokens)}</FieldRow>
|
||||
<FieldRow label="Cache write">{formatNumber(job.cacheWriteTokens)}</FieldRow>
|
||||
<FieldRow label="Tokens totaal">{formatNumber(totalTokens || null)}</FieldRow>
|
||||
<FieldRow label="Kosten (USD)">{costLabel}</FieldRow>
|
||||
<FieldRow label="Duur">{formatDuration(job.startedAt, job.finishedAt)}</FieldRow>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import JobsColumn from './jobs-column'
|
|||
import JobDetailPane from './job-detail-pane'
|
||||
import JobUsagePane from './job-usage-pane'
|
||||
import SprintSubTasksPane from './sprint-sub-tasks-pane'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { useJobsStore } from '@/stores/jobs-store'
|
||||
import useJobsRealtime from '@/hooks/use-jobs-realtime'
|
||||
import type { ClaudeJobStatusApi } from '@/lib/job-status'
|
||||
|
|
@ -61,7 +62,7 @@ export default function JobsBoard({ initialActiveJobs, initialDoneJobs, isDemo }
|
|||
jobId={selectedJobId}
|
||||
isSprintJob={selectedJob?.kind === 'SPRINT_IMPLEMENTATION'}
|
||||
/>
|
||||
<div className="flex gap-1 px-3 pt-3 pb-2 border-b shrink-0">
|
||||
<div className="flex gap-1 px-3 pt-3 pb-2 border-b shrink-0" data-debug-id="jobs-board__toolbar">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={view === 'detail' ? 'default' : 'outline'}
|
||||
|
|
@ -96,11 +97,15 @@ export default function JobsBoard({ initialActiveJobs, initialDoneJobs, isDemo }
|
|||
)
|
||||
|
||||
return (
|
||||
<SplitPane
|
||||
panes={[leftPane, middlePane, rightPane]}
|
||||
defaultSplit={[25, 50, 25]}
|
||||
cookieKey="jobs"
|
||||
tabLabels={['Actief', 'Details', 'Klaar']}
|
||||
/>
|
||||
<div className="h-full" {...debugProps('jobs-board', 'JobsBoard', 'components/jobs/jobs-board.tsx')}>
|
||||
<div className="h-full" data-debug-id="jobs-board__columns">
|
||||
<SplitPane
|
||||
panes={[leftPane, middlePane, rightPane]}
|
||||
defaultSplit={[25, 50, 25]}
|
||||
cookieKey="jobs"
|
||||
tabLabels={['Actief', 'Details', 'Klaar']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import JobCard from './job-card'
|
|||
import { JOB_STATUS_LABELS } from '@/components/shared/job-status'
|
||||
import { jobStatusToApi, type ClaudeJobStatusApi } from '@/lib/job-status'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import type { JobWithRelations } from '@/actions/jobs-page'
|
||||
import type { ClaudeJobKind } from '@prisma/client'
|
||||
|
||||
|
|
@ -170,8 +171,8 @@ export default function JobsColumn({
|
|||
const activeFilterCount = filterKinds.size + filterStatuses.size
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center justify-between gap-2 px-2 py-1.5 border-b border-border bg-surface-container-low shrink-0">
|
||||
<div className="flex flex-col h-full" {...debugProps('jobs-column', 'JobsColumn', 'components/jobs/jobs-column.tsx')}>
|
||||
<div className="flex items-center justify-between gap-2 px-2 py-1.5 border-b border-border bg-surface-container-low shrink-0" data-debug-id="jobs-column__header">
|
||||
<span className="text-xs font-medium text-muted-foreground px-1">{title}</span>
|
||||
<div className="flex items-center gap-1.5 flex-wrap justify-end">
|
||||
{Array.from(filterKinds).map((k) => (
|
||||
|
|
@ -240,7 +241,7 @@ export default function JobsColumn({
|
|||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1 p-2 space-y-2">
|
||||
<div className="overflow-y-auto flex-1 p-2 space-y-2" data-debug-id="jobs-column__items">
|
||||
{filtered.map((j) => (
|
||||
<JobCard
|
||||
key={j.id}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { JOB_STATUS_LABELS, JOB_STATUS_COLORS } from '@/components/shared/job-status'
|
||||
import type { ClaudeJobStatusApi } from '@/lib/job-status'
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ function SubTaskList({ jobId }: { jobId: string }) {
|
|||
if (subTasks.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="border-b p-2 space-y-1 max-h-44 overflow-y-auto shrink-0">
|
||||
<div className="border-b p-2 space-y-1 max-h-44 overflow-y-auto shrink-0" data-debug-id="sprint-sub-tasks-pane__items">
|
||||
{subTasks.map(t => {
|
||||
const apiStatus = t.status.toLowerCase() as ClaudeJobStatusApi
|
||||
return (
|
||||
|
|
@ -63,5 +64,9 @@ function SubTaskList({ jobId }: { jobId: string }) {
|
|||
|
||||
export default function SprintSubTasksPane({ jobId, isSprintJob }: SprintSubTasksPaneProps) {
|
||||
if (!isSprintJob || !jobId) return null
|
||||
return <SubTaskList key={jobId} jobId={jobId} />
|
||||
return (
|
||||
<div {...debugProps('sprint-sub-tasks-pane', 'SprintSubTasksPane', 'components/jobs/sprint-sub-tasks-pane.tsx')}>
|
||||
<SubTaskList key={jobId} jobId={jobId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string
|
||||
|
|
@ -9,7 +10,7 @@ interface MarkdownProps {
|
|||
|
||||
export function Markdown({ children, className }: MarkdownProps) {
|
||||
return (
|
||||
<div className={cn('prose prose-sm dark:prose-invert max-w-none', className)}>
|
||||
<div className={cn('prose prose-sm dark:prose-invert max-w-none', className)} {...debugProps('markdown', 'Markdown', 'components/markdown.tsx')}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
disallowedElements={['script', 'iframe']}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { RotateCw } from 'lucide-react'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export function LandscapeGuard({ children }: { children: React.ReactNode }) {
|
||||
const [isPortrait, setIsPortrait] = useState(false)
|
||||
|
|
@ -22,9 +23,10 @@ export function LandscapeGuard({ children }: { children: React.ReactNode }) {
|
|||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 bg-background text-foreground p-6"
|
||||
{...debugProps('landscape-guard', 'LandscapeGuard', 'components/mobile/landscape-guard.tsx')}
|
||||
>
|
||||
<RotateCw className="size-12 text-primary" />
|
||||
<p className="text-base font-medium text-center">Draai je telefoon naar landscape</p>
|
||||
<p className="text-base font-medium text-center" data-debug-id="landscape-guard__title">Draai je telefoon naar landscape</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { logoutAction } from '@/actions/auth'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export function LogoutButton() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
@ -31,6 +32,7 @@ export function LogoutButton() {
|
|||
variant="outline"
|
||||
onClick={() => setOpen(true)}
|
||||
className="w-full justify-center gap-2"
|
||||
{...debugProps('logout-button', 'LogoutButton', 'components/mobile/logout-button.tsx')}
|
||||
>
|
||||
<LogOut className="size-4" aria-hidden="true" />
|
||||
Uitloggen
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Link from 'next/link'
|
|||
import { usePathname } from 'next/navigation'
|
||||
import { ListTree, Activity, Settings } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface MobileTabBarProps {
|
||||
activeProductId: string | null
|
||||
|
|
@ -42,6 +43,7 @@ export function MobileTabBar({ activeProductId }: MobileTabBarProps) {
|
|||
<nav
|
||||
aria-label="Hoofdnavigatie"
|
||||
className="fixed bottom-0 left-0 right-0 z-40 flex border-t border-border bg-surface-container-low"
|
||||
{...debugProps('mobile-tab-bar', 'MobileTabBar', 'components/mobile/mobile-tab-bar.tsx')}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon
|
||||
|
|
@ -52,6 +54,7 @@ export function MobileTabBar({ activeProductId }: MobileTabBarProps) {
|
|||
href={tab.href}
|
||||
aria-label={tab.label}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
data-debug-id={`mobile-tab-bar__tab-${tab.label.toLowerCase()}`}
|
||||
className={cn(
|
||||
'flex-1 h-14 flex items-center justify-center transition-colors',
|
||||
active
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
import { ANSWER_MAX_CHARS } from '@/lib/schemas/question-answer'
|
||||
import { answerQuestion } from '@/actions/questions'
|
||||
import { useNotificationsStore, type NotificationQuestion } from '@/stores/notifications-store'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface AnswerModalProps {
|
||||
question: NotificationQuestion | null
|
||||
|
|
@ -82,6 +83,7 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) {
|
|||
showCloseButton={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={entityDialogContentClasses}
|
||||
{...debugProps('answer-modal', 'AnswerModal', 'components/notifications/answer-modal.tsx')}
|
||||
>
|
||||
<div className={entityDialogHeaderClasses}>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
@ -96,7 +98,7 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6">
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6" data-debug-id="answer-modal__content">
|
||||
<Link
|
||||
href={
|
||||
question.kind === 'idea'
|
||||
|
|
@ -156,7 +158,7 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className={entityDialogFooterClasses}>
|
||||
<div className={entityDialogFooterClasses} data-debug-id="answer-modal__submit">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={closeGuard.attemptClose} disabled={pending}>
|
||||
Annuleren
|
||||
|
|
|
|||
|
|
@ -106,5 +106,6 @@ export async function NotificationsBridge({ userId }: NotificationsBridgeProps)
|
|||
const initial: NotificationQuestion[] = [...storyQuestions, ...ideaQuestions]
|
||||
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
|
||||
|
||||
// debug-id: skip — bridge component (niet-renderende wrapper, zie docs/patterns/debug-id.md)
|
||||
return <NotificationsRealtimeMount initial={initial} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@ export function NotificationsRealtimeMount({ initial }: Props) {
|
|||
}, [initial])
|
||||
|
||||
useNotificationsRealtime()
|
||||
// debug-id: skip — render-loos mount (returns null, zie docs/patterns/debug-id.md)
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { AnswerModal } from './answer-modal'
|
|||
import { PushToggle } from './push-toggle'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { NotificationQuestion } from '@/stores/notifications-store'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface NotificationsSheetProps {
|
||||
trigger: React.ReactNode
|
||||
|
|
@ -40,8 +41,8 @@ export function NotificationsSheet({
|
|||
<>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger render={trigger as React.ReactElement} />
|
||||
<SheetContent side="right" className="w-full sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetContent side="right" className="w-full sm:max-w-md" {...debugProps('notifications-sheet', 'NotificationsSheet', 'components/notifications/notifications-sheet.tsx')}>
|
||||
<SheetHeader data-debug-id="notifications-sheet__header">
|
||||
<SheetTitle>Vragen van Claude ({questions.length})</SheetTitle>
|
||||
<SheetDescription>
|
||||
Beantwoord open vragen om Claude verder te laten werken.
|
||||
|
|
@ -53,7 +54,7 @@ export function NotificationsSheet({
|
|||
Geen openstaande vragen. Lekker bezig!
|
||||
</div>
|
||||
) : (
|
||||
<ul className="mt-4 flex flex-col gap-2 px-4 pb-4">
|
||||
<ul className="mt-4 flex flex-col gap-2 px-4 pb-4" data-debug-id="notifications-sheet__items">
|
||||
{questions.map((q) => {
|
||||
// story-questions: forYou wanneer assignee = ingelogd; idee-vragen
|
||||
// zijn altijd "voor jou" (idee is strikt user_id-only).
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
subscribeToPush,
|
||||
unsubscribeFromPush,
|
||||
} from '@/lib/push-client'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type PushStatus =
|
||||
| 'loading'
|
||||
|
|
@ -85,7 +86,7 @@ export function PushToggle({ vapidPublicKey }: PushToggleProps) {
|
|||
|
||||
if (status === 'ios-needs-install') {
|
||||
return (
|
||||
<div className="rounded-lg bg-surface-variant p-3 text-sm text-on-surface-variant">
|
||||
<div className="rounded-lg bg-surface-variant p-3 text-sm text-on-surface-variant" {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
|
||||
Op iPhone/iPad: tik op het delen-icoon en kies{' '}
|
||||
<strong>Zet op beginscherm</strong>. Daarna kun je notificaties activeren.
|
||||
</div>
|
||||
|
|
@ -94,7 +95,7 @@ export function PushToggle({ vapidPublicKey }: PushToggleProps) {
|
|||
|
||||
if (status === 'denied') {
|
||||
return (
|
||||
<p className="text-sm text-on-surface-variant">
|
||||
<p className="text-sm text-on-surface-variant" {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
|
||||
Notificaties zijn geblokkeerd. Schakel ze in via je browser-instellingen.
|
||||
</p>
|
||||
)
|
||||
|
|
@ -102,15 +103,19 @@ export function PushToggle({ vapidPublicKey }: PushToggleProps) {
|
|||
|
||||
if (status === 'unsubscribed') {
|
||||
return (
|
||||
<Button variant="default" size="sm" onClick={handleSubscribe}>
|
||||
Activeer push
|
||||
</Button>
|
||||
<div {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
|
||||
<Button variant="default" size="sm" onClick={handleSubscribe} data-debug-id="push-toggle__switch">
|
||||
<span data-debug-id="push-toggle__label">Activeer push</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="sm" onClick={handleUnsubscribe}>
|
||||
Push uitzetten
|
||||
</Button>
|
||||
<div {...debugProps('push-toggle', 'PushToggle', 'components/notifications/push-toggle.tsx')}>
|
||||
<Button variant="outline" size="sm" onClick={handleUnsubscribe} data-debug-id="push-toggle__switch">
|
||||
<span data-debug-id="push-toggle__label">Push uitzetten</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useState, useTransition } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { archiveProductAction } from '@/actions/products'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface ArchiveProductButtonProps {
|
||||
productId: string
|
||||
|
|
@ -20,7 +21,7 @@ export function ArchiveProductButton({ productId }: ArchiveProductButtonProps) {
|
|||
|
||||
if (confirming) {
|
||||
return (
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<div className="flex gap-2 shrink-0" {...debugProps('archive-product-button', 'ArchiveProductButton', 'components/products/archive-product-button.tsx')}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
|
|
@ -47,6 +48,7 @@ export function ArchiveProductButton({ productId }: ArchiveProductButtonProps) {
|
|||
size="sm"
|
||||
className="shrink-0 border-error/40 text-error hover:bg-error/10"
|
||||
onClick={() => setConfirming(true)}
|
||||
{...debugProps('archive-product-button', 'ArchiveProductButton', 'components/products/archive-product-button.tsx')}
|
||||
>
|
||||
Archiveren
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState, useTransition } from 'react'
|
|||
import { cn } from '@/lib/utils'
|
||||
import { updateAutoPrAction } from '@/actions/products'
|
||||
import { toast } from 'sonner'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface AutoPrToggleProps {
|
||||
productId: string
|
||||
|
|
@ -27,13 +28,14 @@ export function AutoPrToggle({ productId, initialValue }: AutoPrToggleProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3" {...debugProps('auto-pr-toggle', 'AutoPrToggle', 'components/products/auto-pr-toggle.tsx')}>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
onClick={handleToggle}
|
||||
disabled={isPending}
|
||||
data-debug-id="auto-pr-toggle__switch"
|
||||
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',
|
||||
|
|
@ -49,7 +51,7 @@ export function AutoPrToggle({ productId, initialValue }: AutoPrToggleProps) {
|
|||
)}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-sm text-foreground">Automatisch PR aanmaken na succesvolle agent-job</span>
|
||||
<span className="text-sm text-foreground" data-debug-id="auto-pr-toggle__label">Automatisch PR aanmaken na succesvolle agent-job</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState } from 'react'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { ProductDialog, type ProductDialogProduct } from '@/components/dialogs/product-dialog'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Props {
|
||||
product: ProductDialogProduct
|
||||
|
|
@ -16,7 +17,7 @@ export function EditProductButton({ product, isDemo = false, size = 'sm', varian
|
|||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<span {...debugProps('edit-product-button', 'EditProductButton', 'components/products/edit-product-button.tsx')}>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
variant={variant}
|
||||
|
|
@ -34,6 +35,6 @@ export function EditProductButton({ product, isDemo = false, size = 'sm', varian
|
|||
product={product}
|
||||
isDemo={isDemo}
|
||||
/>
|
||||
</>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from '@/components/ui/select'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface PrStrategySelectProps {
|
||||
productId: string
|
||||
|
|
@ -46,9 +47,9 @@ export function PrStrategySelect({ productId, initialValue }: PrStrategySelectPr
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2" {...debugProps('pr-strategy-select', 'PrStrategySelect', 'components/products/pr-strategy-select.tsx')}>
|
||||
<Select value={value} onValueChange={handleChange} disabled={isPending}>
|
||||
<SelectTrigger className="w-full max-w-xl">
|
||||
<SelectTrigger className="w-full max-w-xl" data-debug-id="pr-strategy-select__trigger">
|
||||
{STRATEGY_LABELS[value]}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
|
|||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type FieldErrors = Record<string, string[]>
|
||||
type ActionResult = { error?: string | FieldErrors; success?: boolean } | undefined
|
||||
|
|
@ -49,7 +50,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
|
|||
const globalError = getGlobalError(state?.error)
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-5">
|
||||
<form action={formAction} className="space-y-5" {...debugProps('product-form', 'ProductForm', 'components/products/product-form.tsx')}>
|
||||
{defaultValues?.id && (
|
||||
<input type="hidden" name="id" value={defaultValues.id} />
|
||||
)}
|
||||
|
|
@ -82,6 +83,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
|
|||
defaultValue={defaultValues?.name}
|
||||
placeholder="bijv. DevPlanner"
|
||||
className={fieldError('name') ? 'border-error' : ''}
|
||||
data-debug-id="product-form__name"
|
||||
/>
|
||||
{fieldError('name') && (
|
||||
<p className="text-xs text-error">{fieldError('name')}</p>
|
||||
|
|
@ -117,6 +119,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
|
|||
defaultValue={defaultValues?.repo_url ?? ''}
|
||||
placeholder="https://github.com/..."
|
||||
className={fieldError('repo_url') ? 'border-error' : ''}
|
||||
data-debug-id="product-form__repo"
|
||||
/>
|
||||
{fieldError('repo_url') && (
|
||||
<p className="text-xs text-error">{fieldError('repo_url')}</p>
|
||||
|
|
@ -147,7 +150,7 @@ export function ProductForm({ action, submitLabel, defaultValues }: ProductFormP
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-1">
|
||||
<div className="flex gap-3 pt-1" data-debug-id="product-form__submit">
|
||||
<SubmitButton label={submitLabel} />
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useActionState, useState, useTransition } from 'react'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { addProductMemberAction, removeProductMemberAction } from '@/actions/products'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Member {
|
||||
id: string
|
||||
|
|
@ -29,11 +30,11 @@ export function TeamManager({ productId, members }: TeamManagerProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4" {...debugProps('team-manager', 'TeamManager', 'components/products/team-manager.tsx')}>
|
||||
{members.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Nog geen teamleden toegevoegd.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
<ul className="space-y-2" data-debug-id="team-manager__members">
|
||||
{members.map(m => (
|
||||
<li key={m.id} className="flex items-center justify-between gap-3 rounded-lg bg-surface-container px-3 py-2">
|
||||
<span className="text-sm font-medium text-foreground">{m.username}</span>
|
||||
|
|
@ -51,7 +52,7 @@ export function TeamManager({ productId, members }: TeamManagerProps) {
|
|||
</ul>
|
||||
)}
|
||||
|
||||
<form action={formAction} className="flex gap-2">
|
||||
<form action={formAction} className="flex gap-2" data-debug-id="team-manager__invite">
|
||||
<input type="hidden" name="productId" value={productId} />
|
||||
<Input
|
||||
name="username"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState, useTransition } from 'react'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { leaveProductAction } from '@/actions/products'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface LeaveProductButtonProps {
|
||||
productId: string
|
||||
|
|
@ -22,7 +23,7 @@ export function LeaveProductButton({ productId, isDemo = false }: LeaveProductBu
|
|||
|
||||
if (confirming) {
|
||||
return (
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<div className="flex gap-2 shrink-0" {...debugProps('leave-product-button', 'LeaveProductButton', 'components/settings/leave-product-button.tsx')}>
|
||||
<Button variant="destructive" size="sm" disabled={isPending} onClick={handleLeave}>
|
||||
{isPending ? 'Bezig…' : 'Ja, verlaten'}
|
||||
</Button>
|
||||
|
|
@ -34,16 +35,18 @@ export function LeaveProductButton({ productId, isDemo = false }: LeaveProductBu
|
|||
}
|
||||
|
||||
return (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 border-error/40 text-error hover:bg-error/10"
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setConfirming(true)}
|
||||
>
|
||||
Verlaten
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
<span {...debugProps('leave-product-button', 'LeaveProductButton', 'components/settings/leave-product-button.tsx')}>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 border-error/40 text-error hover:bg-error/10"
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setConfirming(true)}
|
||||
>
|
||||
Verlaten
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { toast } from 'sonner'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { updateMinQuotaPctAction } from '@/actions/settings'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface MinQuotaEditorProps {
|
||||
currentValue: number
|
||||
|
|
@ -27,7 +28,7 @@ export function MinQuotaEditor({ currentValue, isDemo }: MinQuotaEditorProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3" {...debugProps('min-quota-editor', 'MinQuotaEditor', 'components/settings/min-quota-editor.tsx')}>
|
||||
<div>
|
||||
<label htmlFor="min-quota-pct" className="text-sm font-medium text-foreground">
|
||||
Minimaal beschikbaar Claude-quota voordat de worker een job oppakt (%)
|
||||
|
|
@ -46,10 +47,11 @@ export function MinQuotaEditor({ currentValue, isDemo }: MinQuotaEditorProps) {
|
|||
onChange={e => setValue(Number(e.target.value))}
|
||||
disabled={isDemo || isPending}
|
||||
className="w-24 rounded border border-border bg-surface-container px-3 py-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"
|
||||
data-debug-id="min-quota-editor__input"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">%</span>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button onClick={handleSave} disabled={isDemo || isPending} size="sm">
|
||||
<Button onClick={handleSave} disabled={isDemo || isPending} size="sm" data-debug-id="min-quota-editor__save">
|
||||
Opslaan
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
|
|||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { updateProfileAction } from '@/actions/profile'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface ProfileEditorProps {
|
||||
email: string | null
|
||||
|
|
@ -63,7 +64,7 @@ export function ProfileEditor({ email, bio, bioDetail, hasAvatar, avatarVersion
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5" {...debugProps('profile-editor', 'ProfileEditor', 'components/settings/profile-editor.tsx')}>
|
||||
<div className="flex items-center gap-5">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -137,6 +138,7 @@ export function ProfileEditor({ email, bio, bioDetail, hasAvatar, avatarVersion
|
|||
placeholder="Bijv. Full-stack developer bij Acme"
|
||||
maxLength={160}
|
||||
disabled={isPending}
|
||||
data-debug-id="profile-editor__username"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Max. 160 tekens</p>
|
||||
</div>
|
||||
|
|
@ -158,7 +160,7 @@ export function ProfileEditor({ email, bio, bioDetail, hasAvatar, avatarVersion
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="submit" size="sm" disabled={isPending}>
|
||||
<Button type="submit" size="sm" disabled={isPending} data-debug-id="profile-editor__save">
|
||||
{isPending ? 'Opslaan…' : 'Opslaan'}
|
||||
</Button>
|
||||
{state && 'success' in state && (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { toast } from 'sonner'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { updateRolesAction } from '@/actions/settings'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
const ALL_ROLES = [
|
||||
{ value: 'PRODUCT_OWNER', label: 'Product Owner' },
|
||||
|
|
@ -46,9 +47,9 @@ export function RoleManager({ currentRoles, isDemo }: RoleManagerProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="bg-surface-container-low border border-border rounded-xl p-5 space-y-4">
|
||||
<div className="bg-surface-container-low border border-border rounded-xl p-5 space-y-4" {...debugProps('role-manager', 'RoleManager', 'components/settings/role-manager.tsx')}>
|
||||
<h2 className="text-sm font-medium text-foreground">Mijn rollen</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<div className="flex flex-wrap gap-3" data-debug-id="role-manager__roles">
|
||||
{ALL_ROLES.map(role => (
|
||||
<label key={role.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
|
|
@ -65,7 +66,7 @@ export function RoleManager({ currentRoles, isDemo }: RoleManagerProps) {
|
|||
{error && <p className="text-xs text-error">{error}</p>}
|
||||
{saved && <p className="text-xs text-success">Rollen opgeslagen.</p>}
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" onClick={handleSave} disabled={isDemo}>Opslaan</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={isDemo} data-debug-id="role-manager__add">Opslaan</Button>
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
|
|||
import { Input } from '@/components/ui/input'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { createApiTokenAction, revokeApiTokenAction } from '@/actions/api-tokens'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Token {
|
||||
id: string
|
||||
|
|
@ -23,7 +24,7 @@ function CreateSubmitButton({ isDemo }: { isDemo: boolean }) {
|
|||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button type="submit" disabled={isDemo || pending}>
|
||||
<Button type="submit" disabled={isDemo || pending} data-debug-id="token-manager__generate">
|
||||
{pending ? 'Aanmaken…' : 'Token aanmaken'}
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
|
|
@ -63,7 +64,7 @@ export function TokenManager({ tokens, isDemo }: TokenManagerProps) {
|
|||
const revokedTokens = tokens.filter(t => t.revoked_at)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6" {...debugProps('token-manager', 'TokenManager', 'components/settings/token-manager.tsx')}>
|
||||
{/* New token revealed */}
|
||||
{newToken && (
|
||||
<div className="bg-success-container border border-success/30 rounded-xl p-4 space-y-3">
|
||||
|
|
@ -103,7 +104,7 @@ export function TokenManager({ tokens, isDemo }: TokenManagerProps) {
|
|||
{activeTokens.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Geen actieve tokens.</p>
|
||||
) : (
|
||||
<div className="bg-surface-container-low border border-border rounded-xl divide-y divide-border">
|
||||
<div className="bg-surface-container-low border border-border rounded-xl divide-y divide-border" data-debug-id="token-manager__tokens">
|
||||
{activeTokens.map(token => (
|
||||
<div key={token.id} className="flex items-center justify-between px-4 py-3 gap-3">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useTransition } from 'react'
|
|||
import { toast } from 'sonner'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { setActiveProductAction } from '@/actions/active-product'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Props {
|
||||
productId: string
|
||||
|
|
@ -28,7 +29,7 @@ export function ActivateProductButton({ productId, isDemo, redirectTo, label = '
|
|||
}
|
||||
|
||||
return (
|
||||
<span data-debug-id="activate-product-button" data-debug-label="ActivateProductButton — shared/activate-product-button.tsx">
|
||||
<span {...debugProps('activate-product-button')}>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={() => !isDemo && handleActivate()}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
|
||||
import { toast } from 'sonner'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
const ALERT_MESSAGES: Record<string, string> = {
|
||||
product_unavailable: 'Je actieve product is niet meer beschikbaar',
|
||||
|
|
@ -24,5 +25,5 @@ export function AlertToast() {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [alert])
|
||||
|
||||
return <span data-debug-id="alert-toast" data-debug-label="AlertToast — shared/alert-toast.tsx" hidden />
|
||||
return <span {...debugProps('alert-toast')} hidden />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface AppIconProps {
|
||||
size?: number
|
||||
className?: string
|
||||
|
|
@ -13,8 +15,7 @@ export function AppIcon({ size = 32, className }: AppIconProps) {
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-label="Scrum4Me"
|
||||
data-debug-id="app-icon"
|
||||
data-debug-label="AppIcon — shared/app-icon.tsx"
|
||||
{...debugProps('app-icon')}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="s4m-bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface CodeBadgeProps {
|
||||
code: string | null | undefined
|
||||
|
|
@ -9,8 +10,7 @@ export function CodeBadge({ code, className }: CodeBadgeProps) {
|
|||
if (!code) return null
|
||||
return (
|
||||
<span
|
||||
data-debug-id="code-badge"
|
||||
data-debug-label="CodeBadge — shared/code-badge.tsx"
|
||||
{...debugProps('code-badge')}
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-md border border-border bg-surface-container px-1.5 py-0.5 font-mono text-[11px] leading-none text-muted-foreground',
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface DemoTooltipProps {
|
||||
show: boolean
|
||||
|
|
@ -10,10 +11,10 @@ interface DemoTooltipProps {
|
|||
// Wraps children with a "Niet beschikbaar in demo-modus" tooltip when show=true.
|
||||
// Uses a span trigger so tooltip works on disabled elements.
|
||||
export function DemoTooltip({ show, children }: DemoTooltipProps) {
|
||||
if (!show) return <span data-debug-id="demo-tooltip" data-debug-label="DemoTooltip — shared/demo-tooltip.tsx">{children}</span>
|
||||
if (!show) return <span {...debugProps('demo-tooltip')}>{children}</span>
|
||||
|
||||
return (
|
||||
<span data-debug-id="demo-tooltip" data-debug-label="DemoTooltip — shared/demo-tooltip.tsx">
|
||||
<span {...debugProps('demo-tooltip')}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="inline-flex" />}>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
'use client'
|
||||
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
// Shows a warning banner on screens narrower than 1024px.
|
||||
export function MinWidthBanner() {
|
||||
return (
|
||||
<div
|
||||
data-debug-id="min-width-banner"
|
||||
data-debug-label="MinWidthBanner — shared/min-width-banner.tsx"
|
||||
{...debugProps('min-width-banner')}
|
||||
className="lg:hidden bg-warning/10 border-b border-warning/30 px-4 py-2 text-center text-xs text-warning"
|
||||
>
|
||||
Scrum4Me is ontworpen voor schermen van minimaal 1024px breed. Sommige functies zijn mogelijk niet goed bruikbaar op dit scherm.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { NotificationsBell } from '@/components/shared/notifications-bell'
|
|||
import { SoloNavStatusIndicators } from '@/components/solo/nav-status-indicators'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { setActiveProductAction } from '@/actions/active-product'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface NavBarProps {
|
||||
isDemo: boolean
|
||||
|
|
@ -112,13 +113,12 @@ export function NavBar({
|
|||
|
||||
return (
|
||||
<header
|
||||
data-debug-id="nav-bar"
|
||||
data-debug-label="NavBar — shared/nav-bar.tsx"
|
||||
{...debugProps('nav-bar')}
|
||||
className="bg-surface-container-low border-b border-border h-14 flex items-center px-4 shrink-0"
|
||||
>
|
||||
{/* Links: logo + nav */}
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<Link href="/" className="flex items-center gap-2 font-medium text-foreground">
|
||||
<Link href="/" data-debug-id="nav-bar__app-icon" className="flex items-center gap-2 font-medium text-foreground">
|
||||
<AppIcon size={24} />
|
||||
<span className="text-primary font-semibold">Scrum4Me</span>
|
||||
{isDemo && (
|
||||
|
|
@ -157,6 +157,7 @@ export function NavBar({
|
|||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
disabled={isPending}
|
||||
data-debug-id="nav-bar__product-switcher"
|
||||
className="flex items-center gap-1 text-sm font-medium text-foreground hover:text-primary transition-colors px-2 rounded-md hover:bg-surface-container focus:outline-none"
|
||||
>
|
||||
<span className="truncate max-w-[180px]">
|
||||
|
|
@ -195,7 +196,9 @@ export function NavBar({
|
|||
<div className="flex items-center gap-2 flex-1 justify-end">
|
||||
<SoloNavStatusIndicators hasActiveProduct={!!activeProduct} minQuotaPct={minQuotaPct} />
|
||||
<NotificationsBell currentUserId={userId} isDemo={isDemo} />
|
||||
<UserMenu userId={userId} username={username} email={email} roles={roles} />
|
||||
<span data-debug-id="nav-bar__user-menu">
|
||||
<UserMenu userId={userId} username={username} email={email} roles={roles} />
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Bell } from 'lucide-react'
|
|||
import { useNotificationsStore } from '@/stores/notifications-store'
|
||||
import { NotificationsSheet } from '@/components/notifications/notifications-sheet'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface NotificationsBellProps {
|
||||
currentUserId: string
|
||||
|
|
@ -27,7 +28,7 @@ export function NotificationsBell({ currentUserId, isDemo }: NotificationsBellPr
|
|||
)
|
||||
|
||||
return (
|
||||
<span data-debug-id="notifications-bell" data-debug-label="NotificationsBell — shared/notifications-bell.tsx">
|
||||
<span {...debugProps('notifications-bell')}>
|
||||
<NotificationsSheet
|
||||
currentUserId={currentUserId}
|
||||
isDemo={isDemo}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface PanelNavBarProps {
|
||||
title: string
|
||||
|
|
@ -9,12 +10,11 @@ interface PanelNavBarProps {
|
|||
export function PanelNavBar({ title, actions, className }: PanelNavBarProps) {
|
||||
return (
|
||||
<div
|
||||
data-debug-id="panel-nav-bar"
|
||||
data-debug-label="PanelNavBar — shared/panel-nav-bar.tsx"
|
||||
{...debugProps('panel-nav-bar')}
|
||||
className={cn('flex items-center justify-between px-4 py-2 border-b border-border bg-surface-container-low shrink-0', className)}
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground">{title}</span>
|
||||
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||
<span data-debug-id="panel-nav-bar__title" className="text-sm font-medium text-foreground">{title}</span>
|
||||
{actions && <div data-debug-id="panel-nav-bar__actions" className="flex items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { PbiStatusApi } from '@/lib/task-status'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export const PBI_STATUS_LABELS: Record<PbiStatusApi, string> = {
|
||||
ready: 'Klaar voor sprint',
|
||||
|
|
@ -26,7 +27,7 @@ interface PbiStatusSelectProps {
|
|||
|
||||
export function PbiStatusSelect({ value, onChange, className }: PbiStatusSelectProps) {
|
||||
return (
|
||||
<span data-debug-id="pbi-status-select" data-debug-label="PbiStatusSelect — shared/pbi-status-select.tsx">
|
||||
<span {...debugProps('pbi-status-select')}>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(v) => { if (v) onChange(v as PbiStatusApi) }}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export const PRIORITY_LABELS: Record<number, string> = {
|
||||
1: 'Kritiek',
|
||||
|
|
@ -25,7 +26,7 @@ interface PrioritySelectProps {
|
|||
|
||||
export function PrioritySelect({ value, onChange, className }: PrioritySelectProps) {
|
||||
return (
|
||||
<span data-debug-id="priority-select" data-debug-label="PrioritySelect — shared/priority-select.tsx">
|
||||
<span {...debugProps('priority-select')}>
|
||||
<Select
|
||||
value={String(value)}
|
||||
onValueChange={(v) => { if (v) onChange(parseInt(v)) }}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useEffect } from 'react'
|
||||
import { useProductStore } from '@/stores/product-store'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export function SetCurrentProduct({ id, name }: { id: string; name: string }) {
|
||||
const { setCurrentProduct, clearCurrentProduct } = useProductStore()
|
||||
|
|
@ -11,5 +12,5 @@ export function SetCurrentProduct({ id, name }: { id: string; name: string }) {
|
|||
return () => clearCurrentProduct()
|
||||
}, [id, name, setCurrentProduct, clearCurrentProduct])
|
||||
|
||||
return <span data-debug-id="set-current-product" data-debug-label="SetCurrentProduct — shared/set-current-product.tsx" hidden />
|
||||
return <span {...debugProps('set-current-product')} hidden />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { cn } from '@/lib/utils'
|
||||
import { setActiveSprintAction } from '@/actions/active-sprint'
|
||||
import type { SprintStatusApi } from '@/lib/task-status'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type SprintItem = { id: string; code: string; sprint_goal: string; status: SprintStatusApi }
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ export function SprintSwitcher({
|
|||
|
||||
if (sprints.length === 0) {
|
||||
return (
|
||||
<span data-debug-id="sprint-switcher" data-debug-label="SprintSwitcher — shared/sprint-switcher.tsx">
|
||||
<span {...debugProps('sprint-switcher')}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
|
|
@ -85,7 +86,7 @@ export function SprintSwitcher({
|
|||
}
|
||||
|
||||
return (
|
||||
<span data-debug-id="sprint-switcher" data-debug-label="SprintSwitcher — shared/sprint-switcher.tsx">
|
||||
<span {...debugProps('sprint-switcher')}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
disabled={isPending}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { DebugToggle } from './status-bar-debug-toggle'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE
|
||||
? new Date(process.env.NEXT_PUBLIC_BUILD_DATE).toLocaleDateString('nl-NL', {
|
||||
|
|
@ -17,11 +18,10 @@ export function StatusBar() {
|
|||
return (
|
||||
<footer
|
||||
className="shrink-0 border-t border-border bg-surface-container-low h-14 px-4 flex items-center justify-between text-sm text-muted-foreground select-none"
|
||||
data-debug-id="status-bar"
|
||||
data-debug-label="StatusBar — shared/status-bar.tsx"
|
||||
{...debugProps('status-bar')}
|
||||
>
|
||||
<span>© {new Date().getFullYear()} Scrum4Me</span>
|
||||
<span>v{version} · gebouwd op {buildDate}{isDev && <DebugToggle />}</span>
|
||||
<span data-debug-id="status-bar__copyright">© {new Date().getFullYear()} Scrum4Me</span>
|
||||
<span data-debug-id="status-bar__build-info">v{version} · gebouwd op {buildDate}{isDev && <DebugToggle />}</span>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface StoryLogEntry {
|
||||
id: string
|
||||
type: string
|
||||
|
|
@ -35,8 +37,7 @@ export function StoryLog({ logs, repoUrl }: StoryLogProps) {
|
|||
if (logs.length === 0) {
|
||||
return (
|
||||
<p
|
||||
data-debug-id="story-log"
|
||||
data-debug-label="StoryLog — shared/story-log.tsx"
|
||||
{...debugProps('story-log')}
|
||||
className="text-sm text-muted-foreground text-center py-4"
|
||||
>
|
||||
Nog geen activiteit. Gebruik de REST API om logs toe te voegen.
|
||||
|
|
@ -46,8 +47,7 @@ export function StoryLog({ logs, repoUrl }: StoryLogProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
data-debug-id="story-log"
|
||||
data-debug-label="StoryLog — shared/story-log.tsx"
|
||||
{...debugProps('story-log')}
|
||||
className="space-y-3"
|
||||
>
|
||||
{logs.map(log => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
type AvatarSize = 'xs' | 'sm' | 'md' | 'lg'
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ export function UserAvatar({ userId, username, size = 'md', className }: UserAva
|
|||
const initials = username.slice(0, 2).toUpperCase()
|
||||
|
||||
return (
|
||||
<span data-debug-id="user-avatar" data-debug-label="UserAvatar — shared/user-avatar.tsx">
|
||||
<span {...debugProps('user-avatar')}>
|
||||
<Avatar className={cn(SIZE_CLASSES[size], className)}>
|
||||
<AvatarImage src={`/api/users/${userId}/avatar`} alt={username} />
|
||||
<AvatarFallback className="bg-primary-container text-primary-container-foreground font-medium">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
PRODUCT_OWNER: 'Product Owner',
|
||||
|
|
@ -46,7 +47,7 @@ export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<span data-debug-id="user-menu" data-debug-label="UserMenu — shared/user-menu.tsx">
|
||||
<span {...debugProps('user-menu')}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
entityDialogFooterClasses,
|
||||
entityDialogHeaderClasses,
|
||||
} from '@/components/shared/entity-dialog-layout'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface BatchEnqueueBlockerDialogProps {
|
||||
open: boolean
|
||||
|
|
@ -37,12 +38,12 @@ export function BatchEnqueueBlockerDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent showCloseButton={false} className={entityDialogContentClasses}>
|
||||
<DialogContent showCloseButton={false} className={entityDialogContentClasses} {...debugProps('batch-enqueue-blocker-dialog', 'BatchEnqueueBlockerDialog', 'components/solo/batch-enqueue-blocker-dialog.tsx')}>
|
||||
<div className={entityDialogHeaderClasses}>
|
||||
<DialogTitle className="text-xl font-semibold">Blokkade gedetecteerd</DialogTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6 text-sm text-foreground">
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6 text-sm text-foreground" data-debug-id="batch-enqueue-blocker-dialog__content">
|
||||
<p>
|
||||
{BLOCKER_REASON_LABELS[blockerReason]}:{' '}
|
||||
<span className="font-medium">{blockerLabel}</span>.
|
||||
|
|
@ -60,7 +61,7 @@ export function BatchEnqueueBlockerDialog({
|
|||
|
||||
<div className={entityDialogFooterClasses}>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
<Button variant="ghost" onClick={onCancel} data-debug-id="batch-enqueue-blocker-dialog__cancel">
|
||||
Annuleer
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
|
|
@ -71,6 +72,7 @@ export function BatchEnqueueBlockerDialog({
|
|||
<Button
|
||||
onClick={onConfirm}
|
||||
disabled={noTasksBeforeBlocker}
|
||||
data-debug-id="batch-enqueue-blocker-dialog__confirm"
|
||||
>
|
||||
{prefixCount === 1
|
||||
? `Stuur ${prefixCount} taak tot aan blokkade`
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useSoloStore } from '@/stores/solo-store'
|
|||
import type { RealtimeStatus } from '@/stores/solo-store'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
function RealtimeIndicator({
|
||||
status,
|
||||
|
|
@ -63,12 +64,14 @@ export function SoloNavStatusIndicators({
|
|||
workerQuotaPct < minQuotaPct
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<RealtimeIndicator
|
||||
status={realtimeStatus}
|
||||
showConnectingIndicator={showConnectingIndicator}
|
||||
/>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-3 px-2" {...debugProps('solo-nav-status-indicators', 'SoloNavStatusIndicators', 'components/solo/nav-status-indicators.tsx')}>
|
||||
<span data-debug-id="nav-status-indicators__queue">
|
||||
<RealtimeIndicator
|
||||
status={realtimeStatus}
|
||||
showConnectingIndicator={showConnectingIndicator}
|
||||
/>
|
||||
</span>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground" data-debug-id="nav-status-indicators__running">
|
||||
<span className={cn(
|
||||
'size-2 rounded-full',
|
||||
isStandby
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Link from 'next/link'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface NoActiveSprintProps {
|
||||
productId: string
|
||||
|
|
@ -7,9 +8,9 @@ interface NoActiveSprintProps {
|
|||
|
||||
export function NoActiveSprint({ productId, productName }: NoActiveSprintProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6" {...debugProps('no-active-sprint', 'NoActiveSprint', 'components/solo/no-active-sprint.tsx')}>
|
||||
<div className="text-4xl text-muted-foreground">🏃</div>
|
||||
<h2 className="text-lg font-medium text-foreground">Geen actieve sprint</h2>
|
||||
<h2 className="text-lg font-medium text-foreground" data-debug-id="no-active-sprint__title">Geen actieve sprint</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-sm">
|
||||
Er is nog geen actieve sprint voor <span className="font-medium text-foreground">{productName}</span>.
|
||||
Start een sprint in het Sprint Board om hier je taken te zien.
|
||||
|
|
@ -17,6 +18,7 @@ export function NoActiveSprint({ productId, productName }: NoActiveSprintProps)
|
|||
<Link
|
||||
href={`/products/${productId}/sprint`}
|
||||
className="text-sm text-primary hover:underline"
|
||||
data-debug-id="no-active-sprint__cta"
|
||||
>
|
||||
Naar Sprint Board →
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Link from 'next/link'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
|
|
@ -12,7 +13,7 @@ interface ProductPickerProps {
|
|||
|
||||
export function ProductPicker({ products }: ProductPickerProps) {
|
||||
return (
|
||||
<div className="p-6 max-w-2xl mx-auto w-full">
|
||||
<div className="p-6 max-w-2xl mx-auto w-full" {...debugProps('product-picker', 'ProductPicker', 'components/solo/product-picker.tsx')}>
|
||||
<h1 className="text-xl font-medium text-foreground mb-2">Solo bord</h1>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
Kies een product om je persoonlijke Kanban-bord te openen.
|
||||
|
|
@ -26,7 +27,7 @@ export function ProductPicker({ products }: ProductPickerProps) {
|
|||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-2" data-debug-id="product-picker__items">
|
||||
{products.map(product => (
|
||||
<Link
|
||||
key={product.id}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
import { useSoloRealtime } from '@/lib/realtime/use-solo-realtime'
|
||||
|
||||
// render-loos — geen JSX-root, data-debug-id niet van toepassing
|
||||
export function SoloRealtimeBridge({ productId }: { productId: string | null }) {
|
||||
useSoloRealtime(productId)
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useSoloStore } from '@/stores/solo-store'
|
|||
import { taskStatusToApi } from '@/lib/task-status'
|
||||
import { previewEnqueueAllAction, enqueueClaudeJobsBatchAction } from '@/actions/claude-jobs'
|
||||
import { BatchEnqueueBlockerDialog } from './batch-enqueue-blocker-dialog'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { SplitPane } from '@/components/split-pane/split-pane'
|
||||
|
|
@ -200,8 +201,8 @@ export function SoloBoard({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4 gap-4 min-h-0">
|
||||
<div className="flex items-start justify-between gap-4 shrink-0">
|
||||
<div className="flex flex-col h-full p-4 gap-4 min-h-0" {...debugProps('solo-board', 'SoloBoard', 'components/solo/solo-board.tsx')}>
|
||||
<div className="flex items-start justify-between gap-4 shrink-0" data-debug-id="solo-board__header">
|
||||
<div className="min-w-0 flex items-center gap-3">
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
|
|
@ -231,7 +232,7 @@ export function SoloBoard({
|
|||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="flex-1 min-h-0" data-debug-id="solo-board__columns">
|
||||
<SplitPane
|
||||
cookieKey={`solo-${productId}`}
|
||||
defaultSplit={[33, 33, 34]}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useDroppable } from '@dnd-kit/core'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SoloTaskCard } from './solo-task-card'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import type { SoloTask } from './solo-board'
|
||||
|
||||
export const COLUMN_CONFIG = {
|
||||
|
|
@ -40,13 +41,14 @@ export function SoloColumn({ status, tasks, isDemo, onTaskClick }: SoloColumnPro
|
|||
'flex flex-col h-full rounded-lg border border-border overflow-hidden',
|
||||
isOver && 'ring-2 ring-primary ring-inset',
|
||||
)}
|
||||
{...debugProps('solo-column', 'SoloColumn', 'components/solo/solo-column.tsx')}
|
||||
>
|
||||
<div className={cn('flex items-center gap-2 px-3 py-2', config.headerClass)}>
|
||||
<div className={cn('flex items-center gap-2 px-3 py-2', config.headerClass)} data-debug-id="solo-column__header">
|
||||
<span className="text-sm font-medium">{config.label}</span>
|
||||
<span className="text-xs opacity-60 ml-auto">{tasks.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col gap-2 p-2 overflow-y-auto min-h-[140px]">
|
||||
<div className="flex-1 flex flex-col gap-2 p-2 overflow-y-auto min-h-[140px]" data-debug-id="solo-column__tasks">
|
||||
{tasks.map(task => (
|
||||
<SoloTaskCard
|
||||
key={task.id}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { JOB_STATUS_LABELS, JOB_STATUS_COLORS, JOB_STATUS_ACTIVE } from '@/compo
|
|||
import { useSoloStore } from '@/stores/solo-store'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
|
||||
import type { SoloTask } from './solo-board'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
const PRIORITY_BORDER: Record<number, string> = {
|
||||
1: 'border-l-4 border-l-priority-critical',
|
||||
|
|
@ -47,9 +48,10 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
|
|||
isDemo ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing',
|
||||
)}
|
||||
{...(!isDemo ? { ...attributes, ...listeners } : {})}
|
||||
{...debugProps('solo-task-card', 'SoloTaskCard', 'components/solo/solo-task-card.tsx')}
|
||||
>
|
||||
{/* Regel 1: taaknaam + task_code */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start justify-between gap-2" data-debug-id="solo-task-card__title">
|
||||
<p className="text-sm text-foreground leading-snug flex-1">{task.title}</p>
|
||||
{task.task_code && (
|
||||
<TooltipProvider>
|
||||
|
|
@ -106,7 +108,7 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
|
|||
</div>
|
||||
|
||||
{/* Regel 4: story-info + job-badge */}
|
||||
<div className="flex items-center justify-between gap-2 mt-0.5">
|
||||
<div className="flex items-center justify-between gap-2 mt-0.5" data-debug-id="solo-task-card__status">
|
||||
<p className="text-xs text-muted-foreground truncate flex-1">
|
||||
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
|
||||
{task.story_title}
|
||||
|
|
@ -145,6 +147,7 @@ export function SoloTaskCardOverlay({ task }: { task: SoloTask }) {
|
|||
'bg-surface-container rounded border border-primary px-3 py-2 shadow-xl opacity-90',
|
||||
PRIORITY_BORDER[task.priority],
|
||||
)}
|
||||
{...debugProps('solo-task-card-overlay', 'SoloTaskCardOverlay', 'components/solo/solo-task-card.tsx')}
|
||||
>
|
||||
{/* Regel 1 */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useSoloStore } from '@/stores/solo-store'
|
|||
import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getBranchUrl } from '@/lib/job-status-url'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import type { SoloTask } from './solo-board'
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
|
|
@ -206,7 +207,7 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className={entityDialogBodyClasses}>
|
||||
<div className={entityDialogBodyClasses} data-debug-id="task-detail-dialog__content">
|
||||
{task.description && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1.5">Beschrijving</p>
|
||||
|
|
@ -380,7 +381,7 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
|
|||
export function TaskDetailDialog({ task, productId, isDemo, repoUrl, onClose }: TaskDetailDialogProps) {
|
||||
return (
|
||||
<Dialog open={!!task} onOpenChange={(open) => { if (!open) onClose() }}>
|
||||
<DialogContent showCloseButton={false} className={entityDialogContentClasses}>
|
||||
<DialogContent showCloseButton={false} className={entityDialogContentClasses} {...debugProps('task-detail-dialog', 'TaskDetailDialog', 'components/solo/task-detail-dialog.tsx')}>
|
||||
{task && (
|
||||
<TaskDetailContent
|
||||
key={task.id}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
|||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import { claimStoryAction } from '@/actions/stories'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export interface UnassignedStoryTask {
|
||||
id: string
|
||||
|
|
@ -156,12 +157,12 @@ export function UnassignedStoriesSheet({
|
|||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="right">
|
||||
<SheetContent side="right" {...debugProps('unassigned-stories-sheet', 'UnassignedStoriesSheet', 'components/solo/unassigned-stories-sheet.tsx')}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Openstaande stories</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex-1 overflow-y-auto" data-debug-id="unassigned-stories-sheet__content">
|
||||
{stories.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
|
|
@ -169,7 +170,7 @@ export function UnassignedStoriesSheet({
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2" data-debug-id="unassigned-stories-sheet__items">
|
||||
{stories.map(story => (
|
||||
<ClaimStoryRow
|
||||
key={story.id}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Fragment, useRef, useState, useEffect, useCallback } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
const COOKIE_PREFIX = 'sp:'
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365
|
||||
|
|
@ -123,7 +124,7 @@ export function SplitPane({
|
|||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...debugProps('split-pane', 'SplitPane', 'components/split-pane/split-pane.tsx')}>
|
||||
<div className="flex items-center border-b border-border shrink-0">
|
||||
{activeTab > 0 && (
|
||||
<button
|
||||
|
|
@ -157,11 +158,12 @@ export function SplitPane({
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex h-full overflow-hidden select-none">
|
||||
<div ref={containerRef} className="flex h-full overflow-hidden select-none" {...debugProps('split-pane', 'SplitPane', 'components/split-pane/split-pane.tsx')}>
|
||||
{panes.map((pane, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && (
|
||||
<div
|
||||
data-debug-id="split-pane__divider"
|
||||
onMouseDown={() => setDragging(i - 1)}
|
||||
className={cn(
|
||||
'w-1 shrink-0 bg-border hover:bg-primary transition-colors cursor-col-resize',
|
||||
|
|
@ -172,6 +174,7 @@ export function SplitPane({
|
|||
<div
|
||||
className="flex flex-col overflow-hidden"
|
||||
style={i === n - 1 ? { flex: 1 } : { width: `${splits[i]}%` }}
|
||||
data-debug-id={i === 0 ? 'split-pane__left' : i === n - 1 ? 'split-pane__right' : undefined}
|
||||
>
|
||||
{pane}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
entityDialogHeaderClasses,
|
||||
} from '@/components/shared/entity-dialog-layout'
|
||||
import { createSprintWithPbisAction } from '@/actions/sprints'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface NewSprintDialogProps {
|
||||
open: boolean
|
||||
|
|
@ -102,6 +103,7 @@ export function NewSprintDialog({
|
|||
showCloseButton={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={entityDialogContentClasses}
|
||||
{...debugProps('new-sprint-dialog', 'NewSprintDialog', 'components/sprint/new-sprint-dialog.tsx')}
|
||||
>
|
||||
<div className={entityDialogHeaderClasses}>
|
||||
<DialogTitle className="text-xl font-semibold">Nieuwe sprint</DialogTitle>
|
||||
|
|
@ -173,6 +175,7 @@ export function NewSprintDialog({
|
|||
type="submit"
|
||||
form="new-sprint-form"
|
||||
disabled={isPending || !sprintGoal.trim() || pbiIds.length === 0}
|
||||
data-debug-id="new-sprint-dialog__submit"
|
||||
>
|
||||
{isPending ? 'Aanmaken…' : 'Sprint aanmaken'}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { PbiDialog, type PbiDialogState } from '@/components/backlog/pbi-dialog'
|
|||
import { StoryDialog, type StoryDialogState } from '@/components/backlog/story-dialog'
|
||||
import type { PbiStatusApi } from '@/lib/task-status'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
OPEN: 'bg-status-todo/15 text-status-todo border-status-todo/30',
|
||||
|
|
@ -276,7 +277,7 @@ export function SprintBacklogLeft({
|
|||
const orderedStories = order.map(id => storyMap[id]).filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...debugProps('sprint-backlog-left', 'SprintBacklogLeft', 'components/sprint/sprint-backlog.tsx')}>
|
||||
<PanelNavBar
|
||||
title="Sprint Backlog"
|
||||
actions={
|
||||
|
|
@ -293,6 +294,7 @@ export function SprintBacklogLeft({
|
|||
/>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
data-debug-id="sprint-backlog-left__list"
|
||||
className={cn(
|
||||
'flex-1 overflow-y-auto transition-colors',
|
||||
isOver && 'bg-primary/5 ring-2 ring-inset ring-primary/20 rounded'
|
||||
|
|
@ -631,10 +633,11 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, pr
|
|||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...debugProps('sprint-backlog-right', 'SprintBacklogRight', 'components/sprint/sprint-backlog.tsx')}>
|
||||
<PanelNavBar title="Product Backlog" actions={headerActions} />
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
data-debug-id="sprint-backlog-right__list"
|
||||
className={cn(
|
||||
'flex-1 overflow-y-auto py-2 transition-colors',
|
||||
isOver && 'bg-error/5 ring-2 ring-inset ring-error/20 rounded'
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
removeStoryFromSprintAction,
|
||||
reorderSprintStoriesAction,
|
||||
} from '@/actions/sprints'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface SprintBoardClientProps {
|
||||
productId: string
|
||||
|
|
@ -193,6 +194,7 @@ export function SprintBoardClient({
|
|||
const selectedTasks = selectedStoryId ? (tasksByStory[selectedStoryId] ?? []) : []
|
||||
|
||||
return (
|
||||
<div {...debugProps('sprint-board-client')} className="contents">
|
||||
<DndContext
|
||||
id="sprint-board"
|
||||
sensors={sensors}
|
||||
|
|
@ -244,12 +246,13 @@ export function SprintBoardClient({
|
|||
/>
|
||||
<DragOverlay>
|
||||
{activeDragStory && (
|
||||
<div className="flex items-center gap-3 px-6 py-2 bg-popover border border-primary rounded shadow-lg text-sm opacity-95 w-72">
|
||||
<div className="flex items-center gap-3 px-6 py-2 bg-popover border border-primary rounded shadow-lg text-sm opacity-95 w-72" data-debug-id="sprint-board-client__drag-overlay">
|
||||
<span className="text-muted-foreground select-none">⠿</span>
|
||||
<span className="truncate flex-1">{activeDragStory.title}</span>
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from '@/components/shared/entity-dialog-layout'
|
||||
import { updateSprintGoalAction, updateSprintDatesAction, completeSprintAction, setAllSprintTasksDoneAction } from '@/actions/sprints'
|
||||
import type { SprintStory } from './sprint-backlog'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
interface Sprint {
|
||||
id: string
|
||||
|
|
@ -130,7 +131,7 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0">
|
||||
<div className="px-4 py-3 border-b border-border bg-surface-container-low shrink-0" {...debugProps('sprint-header', 'SprintHeader', 'components/sprint/sprint-header.tsx')}>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -153,7 +154,7 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
|
|||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<button onClick={() => !isDemo && setEditingGoal(true)} className="text-left mt-0.5 group">
|
||||
<button onClick={() => !isDemo && setEditingGoal(true)} className="text-left mt-0.5 group" data-debug-id="sprint-header__title">
|
||||
<p className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
{sprint.sprint_goal}
|
||||
</p>
|
||||
|
|
@ -161,9 +162,9 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0" data-debug-id="sprint-header__actions">
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" variant="ghost" disabled={isDemo} className="text-muted-foreground" onClick={() => !isDemo && setEditingDates(true)}>
|
||||
<Button size="sm" variant="ghost" disabled={isDemo} className="text-muted-foreground" data-debug-id="sprint-header__dates" onClick={() => !isDemo && setEditingDates(true)}>
|
||||
{sprint.start_date && sprint.end_date
|
||||
? `${toDateInputValue(sprint.start_date)} → ${toDateInputValue(sprint.end_date)}`
|
||||
: 'Datums instellen'}
|
||||
|
|
|
|||
|
|
@ -174,13 +174,14 @@ export function SprintRunControls({
|
|||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2" data-debug-id="sprint-run-controls">
|
||||
{canStart && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleStart}
|
||||
disabled={pending || isDemo}
|
||||
className="text-xs"
|
||||
data-debug-id="sprint-run-controls__start"
|
||||
>
|
||||
Start Sprint
|
||||
</Button>
|
||||
|
|
@ -192,6 +193,7 @@ export function SprintRunControls({
|
|||
disabled={pending || isDemo}
|
||||
variant="default"
|
||||
className="text-xs"
|
||||
data-debug-id="sprint-run-controls__start"
|
||||
>
|
||||
Hervat sprint
|
||||
</Button>
|
||||
|
|
@ -203,6 +205,7 @@ export function SprintRunControls({
|
|||
disabled={pending || isDemo}
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
data-debug-id="sprint-run-controls__cancel"
|
||||
>
|
||||
Annuleer sprint-run
|
||||
</Button>
|
||||
|
|
@ -210,7 +213,7 @@ export function SprintRunControls({
|
|||
</div>
|
||||
|
||||
<Dialog open={blockers !== null} onOpenChange={(open) => { if (!open) setBlockers(null) }}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent className="max-w-lg" data-debug-id="sprint-run-controls__blockers-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Sprint kan nog niet starten</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
|
|||
return (
|
||||
<>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" onClick={() => setOpen(true)} disabled={isDemo}>
|
||||
<Button size="sm" onClick={() => setOpen(true)} disabled={isDemo} data-debug-id="start-sprint-button">
|
||||
Sprint starten
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
|
|
@ -89,6 +89,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
|
|||
showCloseButton={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={entityDialogContentClasses}
|
||||
data-debug-id="start-sprint-button__dialog"
|
||||
>
|
||||
<div className={entityDialogHeaderClasses}>
|
||||
<DialogTitle className="text-xl font-semibold">Nieuwe Sprint starten</DialogTitle>
|
||||
|
|
@ -171,7 +172,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
|
|||
<Button type="button" variant="ghost" onClick={closeGuard.attemptClose} disabled={pending}>
|
||||
Annuleren
|
||||
</Button>
|
||||
<Button type="submit" form="start-sprint-form" disabled={pending}>
|
||||
<Button type="submit" form="start-sprint-form" disabled={pending} data-debug-id="start-sprint-button__submit">
|
||||
{pending ? 'Aanmaken…' : 'Sprint starten'}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,5 +12,6 @@ export function SyncActiveSprintCookie({ productId, sprintId }: Props) {
|
|||
useEffect(() => {
|
||||
syncActiveSprintCookieAction(productId, sprintId)
|
||||
}, [productId, sprintId])
|
||||
// No data-debug-id: this component renders null (side-effect only).
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { PRIORITY_BORDER } from '@/components/backlog/backlog-card'
|
|||
import { useSprintStore } from '@/stores/sprint-store'
|
||||
import { updateTaskStatusAction, reorderTasksAction } from '@/actions/tasks'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const STATUS_CYCLE: Record<string, 'TO_DO' | 'IN_PROGRESS' | 'DONE'> = {
|
||||
|
|
@ -200,7 +201,7 @@ export function TaskList({ storyId, sprintId: _sprintId, productId: _productId,
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-col h-full" {...debugProps('task-list', 'TaskList', 'components/sprint/task-list.tsx')}>
|
||||
<PanelNavBar
|
||||
title="Taken"
|
||||
actions={
|
||||
|
|
@ -220,9 +221,9 @@ export function TaskList({ storyId, sprintId: _sprintId, productId: _productId,
|
|||
}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex-1 overflow-y-auto" data-debug-id="task-list__items">
|
||||
{orderedTasks.length === 0 ? (
|
||||
<div className="text-center mt-8 space-y-3">
|
||||
<div className="text-center mt-8 space-y-3" data-debug-id="task-list__empty">
|
||||
<p className="text-sm text-muted-foreground">Geen taken voor deze story.</p>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ Auto-generated on 2026-05-09 from front-matter and headings.
|
|||
| Title | Status | Updated |
|
||||
|---|---|---|
|
||||
| [Bidirectionele async-comms MCP-agent ↔ user](./patterns/claude-question-channel.md) | active | 2026-05-03 |
|
||||
| [Debug-id op component-root](./patterns/debug-id.md) | active | 2026-05-09 |
|
||||
| [Debug-labels: BEM data-debug-id patroon](./patterns/debug-labels.md) | active | 2026-05-09 |
|
||||
| [Entity Dialog](./patterns/dialog.md) | active | 2026-05-08 |
|
||||
| [iron-session](./patterns/iron-session.md) | active | 2026-05-03 |
|
||||
| [Prisma Client singleton](./patterns/prisma-client.md) | active | 2026-05-03 |
|
||||
|
|
|
|||
64
docs/patterns/debug-id.md
Normal file
64
docs/patterns/debug-id.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
title: "Debug-id op component-root"
|
||||
status: active
|
||||
audience: [ai-agent, contributor]
|
||||
language: nl
|
||||
last_updated: 2026-05-09
|
||||
when_to_read: "Wanneer je een named-component aanmaakt of aanpast."
|
||||
---
|
||||
|
||||
# Patroon: Debug-id op component-root
|
||||
|
||||
## Regel: named-component boundary
|
||||
|
||||
Elk named-component plaatst `data-debug-id` en `data-debug-label` via de
|
||||
`debugProps`-helper op zijn root JSX-element. Zes concrete regels:
|
||||
|
||||
1. **Import** `debugProps` uit `@/lib/debug` — geen inline attribuut schrijven.
|
||||
2. **Spread** het resultaat op het root element: `{...debugProps(id, component, file)}`.
|
||||
3. **`id`** is kebab-case van de componentnaam, bijv. `sprint-board`.
|
||||
4. **`component`** is de PascalCase naam zoals die geëxporteerd wordt, bijv. `SprintBoard`.
|
||||
5. **`file`** is het relatieve pad vanaf de repo-root, bijv. `components/sprint/sprint-board.tsx`.
|
||||
6. **Root = het buitenste JSX-element** dat de component rendert — niet een wrapper div die je extra toevoegt.
|
||||
|
||||
In productie (`NODE_ENV=production`) retourneert `debugProps` een leeg object `{}`
|
||||
zodat er geen debug-attributen in de gebundelde HTML staan.
|
||||
|
||||
## Helper-voorbeeld
|
||||
|
||||
```tsx
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export function SprintBoard({ ... }: SprintBoardProps) {
|
||||
return (
|
||||
<div
|
||||
className="..."
|
||||
{...debugProps('sprint-board', 'SprintBoard', 'components/sprint/sprint-board.tsx')}
|
||||
>
|
||||
{/* inhoud */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Skip-criteria
|
||||
|
||||
Voeg **geen** `debugProps` toe aan:
|
||||
|
||||
| Categorie | Reden |
|
||||
|---|---|
|
||||
| `components/ui/*` | shadcn-primitives — ongebrand, niet onze componenten |
|
||||
| Bridges / mounts | Niet-renderende wrappers zoals `notifications-bridge`, `realtime-bridge`, `sync-active-sprint-cookie` |
|
||||
| Hooks-only files | Files die alleen hooks exporteren en niets renderen |
|
||||
|
||||
## Motivatie: geen build-time injectie van pad
|
||||
|
||||
Een alternatief is het bestandspad automatisch injecteren via een Babel/SWC-plugin
|
||||
of een ESLint-codefixin. Dit is bewust **niet** gekozen omdat:
|
||||
|
||||
- de plugin afhankelijk wordt van de build-toolchain-configuratie (Next.js, Turbopack),
|
||||
- bij rename/move van een bestand de injectie verouderd raakt zonder dat de compiler waarschuwt,
|
||||
- expliciete argumenten in de broncode reviewbaar en grep-baar zijn.
|
||||
|
||||
Het handmatig meegeven van `id`, `component` en `file` maakt de intentie zichtbaar
|
||||
en voorkomt verborgen afhankelijkheden.
|
||||
114
docs/patterns/debug-labels.md
Normal file
114
docs/patterns/debug-labels.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
title: "Debug-labels: BEM data-debug-id patroon"
|
||||
status: active
|
||||
audience: [ai-agent, contributor]
|
||||
language: nl
|
||||
last_updated: 2026-05-09
|
||||
when_to_read: "Wanneer je een component aanmaakt of aanpast en debug-ids wilt toevoegen aan sub-elementen."
|
||||
---
|
||||
|
||||
# Patroon: Debug-labels (BEM data-debug-id)
|
||||
|
||||
## Doel
|
||||
|
||||
`data-debug-id` geeft Claude (en ontwikkelaars) een ondubbelzinnige naam voor elk
|
||||
DOM-element. In plaats van "de blauwe knop rechtsonder" zeg je `status-bar__build-info`
|
||||
— uniek, herleidbaar naar de broncode, en grep-baar.
|
||||
|
||||
## Toggle
|
||||
|
||||
Een `{ }`-knop verschijnt alleen in development (`NODE_ENV !== 'production'`) in de
|
||||
`StatusBar`. De knop beheert `localStorage['scrum4me:debug-mode']` via
|
||||
`stores/debug-store.ts` en zet de klasse `debug-mode` op `<body>`.
|
||||
|
||||
In `app/globals.css` activeren de regels onder `body.debug-mode [data-debug-id]`:
|
||||
- een dashed outline rondom elk geïnstrumenteerd element,
|
||||
- een hover-tooltip die de waarde van `data-debug-id` toont.
|
||||
|
||||
In productie worden geen `data-debug-id`-attributen gerenderd — `debugProps()` retourneert
|
||||
een leeg object wanneer `NODE_ENV === 'production'`.
|
||||
|
||||
## Patroon
|
||||
|
||||
### Root-element
|
||||
|
||||
De root van elke named-component gebruikt `debugProps()` uit `@/lib/debug`:
|
||||
|
||||
```tsx
|
||||
import { debugProps } from '@/lib/debug'
|
||||
|
||||
export function StatusBar() {
|
||||
return (
|
||||
<footer
|
||||
className="..."
|
||||
{...debugProps('status-bar')}
|
||||
>
|
||||
{/* inhoud */}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`debugProps(id)` plaatst `data-debug-id={id}` in development en `{}` in productie.
|
||||
De `id` is de kebab-case variant van de bestandsnaam, bijv. `status-bar` voor
|
||||
`components/shared/status-bar.tsx`.
|
||||
|
||||
### Sub-elementen (BEM)
|
||||
|
||||
Interactieve of significante sub-elementen krijgen een inline `data-debug-id` met
|
||||
BEM-notatie: `<root>__<sub>`. Sub-elementen schrijven het attribuut **direct** (niet
|
||||
via `debugProps`), want ze zijn altijd genest binnen het root-element en zichtbaar
|
||||
alleen samen met de root:
|
||||
|
||||
```tsx
|
||||
export function StatusBar() {
|
||||
return (
|
||||
<footer
|
||||
className="..."
|
||||
{...debugProps('status-bar')}
|
||||
>
|
||||
<span data-debug-id="status-bar__copyright">
|
||||
© {new Date().getFullYear()} Scrum4Me
|
||||
</span>
|
||||
<span data-debug-id="status-bar__build-info">
|
||||
v{version} · gebouwd op {buildDate}
|
||||
{isDev && <DebugToggle />}
|
||||
</span>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Welke sub-elementen instrumenteren
|
||||
|
||||
| Instrumenteer | Voorbeelden |
|
||||
|---|---|
|
||||
| Interactieve elementen | `<button>`, triggers, links, tabs |
|
||||
| Sectie-headers | `<h1>` t/m `<h6>` |
|
||||
| Primaire inhoudstitels of -tekst | hoofdtitel van een kaart, badge-label |
|
||||
|
||||
Bij twijfel: **skip**. Liever te weinig dan ruis.
|
||||
|
||||
## Wat NIET instrumenteren
|
||||
|
||||
| Categorie | Reden |
|
||||
|---|---|
|
||||
| `components/ui/*` | shadcn-primitives — herbruikt op te veel plekken, id zou clashen |
|
||||
| `app/(...)/page.tsx` | v1 scope is alleen `components/` |
|
||||
| Bridges / mounts | Niet-renderende wrappers (`notifications-bridge`, `realtime-bridge`, …) |
|
||||
| Hooks-only files | Files die alleen hooks exporteren en niets renderen |
|
||||
|
||||
## Geen `data-debug-label`
|
||||
|
||||
Het attribuut heet uitsluitend `data-debug-id`. Er bestaat geen `data-debug-label`.
|
||||
De `app/globals.css` tooltip leest `attr(data-debug-id)` — een tweede attribuut
|
||||
zou alleen verwarring geven.
|
||||
|
||||
## Gerelateerde bestanden
|
||||
|
||||
| Bestand | Rol |
|
||||
|---|---|
|
||||
| `lib/debug.ts` | `debugProps()`-helper; retourneert `{}` in productie |
|
||||
| `stores/debug-store.ts` | Zustand-store voor `debugMode`-state en `toggleDebugMode` |
|
||||
| `components/shared/status-bar-debug-toggle.tsx` | `{ }`-knop — synchroniseert localStorage en `body.debug-mode` |
|
||||
| `app/globals.css` | `body.debug-mode [data-debug-id]` — outline + hover-tooltip |
|
||||
14
lib/debug.ts
Normal file
14
lib/debug.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type DebugProps = {
|
||||
'data-debug-id': string
|
||||
}
|
||||
|
||||
export function debugProps(
|
||||
id: string,
|
||||
_component?: string,
|
||||
_file?: string
|
||||
): DebugProps | Record<string, never> {
|
||||
if (process.env.NODE_ENV === 'production') return {}
|
||||
return {
|
||||
'data-debug-id': id,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue