feat(ST-1110.5): unify demo write-button pattern to disabled+tooltip
Convert all !isDemo && <Button> patterns to <DemoTooltip show={isDemo}>
<Button disabled={isDemo}> so demo visitors see app capabilities.
Affects: pbi-list, story-panel, story-dialog, task-list, sprint-backlog,
token-manager, product-list, activate-product-button, leave-product-button,
settings page.
This commit is contained in:
parent
84f0a2add4
commit
5e0308d42e
12 changed files with 180 additions and 134 deletions
|
|
@ -103,7 +103,7 @@ describe('POST /api/auth/pair/claim', () => {
|
|||
expect(mockClearPairCookie).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('demo-user: isDemo doorgezet als vangnet', async () => {
|
||||
it('demo-user: claim geblokkeerd met 403 (ST-1110.4)', async () => {
|
||||
mockReadPairCookie.mockResolvedValue(COOKIE_TOKEN)
|
||||
mockPrisma.loginPairing.updateMany.mockResolvedValue({ count: 1 })
|
||||
mockPrisma.loginPairing.findUnique.mockResolvedValue({
|
||||
|
|
@ -112,8 +112,10 @@ describe('POST /api/auth/pair/claim', () => {
|
|||
})
|
||||
|
||||
const res = await POST(makePost({ pairingId: PAIRING_ID }))
|
||||
expect(res.status).toBe(200)
|
||||
expect(mockSession.isDemo).toBe(true)
|
||||
expect(res.status).toBe(403)
|
||||
const body = await res.json()
|
||||
expect(body.error).toMatch(/demo-modus/i)
|
||||
expect(mockClearPairCookie).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('401 zonder s4m_pair-cookie', async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const { cookieJar } = vi.hoisted(() => ({
|
||||
const { cookieJar, mockGetIronSession } = vi.hoisted(() => ({
|
||||
cookieJar: { set: vi.fn(), get: vi.fn(), delete: vi.fn() },
|
||||
mockGetIronSession: vi.fn().mockResolvedValue({ isDemo: false }),
|
||||
}))
|
||||
|
||||
vi.mock('iron-session', () => ({
|
||||
getIronSession: mockGetIronSession,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/prisma', () => ({
|
||||
|
|
|
|||
|
|
@ -95,10 +95,7 @@ export default async function SettingsPage() {
|
|||
</p>
|
||||
</div>
|
||||
{!session.isDemo && (
|
||||
<Link
|
||||
href="/products/new"
|
||||
className="shrink-0 text-xs text-primary hover:underline font-medium"
|
||||
>
|
||||
<Link href="/products/new" className="shrink-0 text-xs text-primary hover:underline font-medium">
|
||||
+ Nieuw product
|
||||
</Link>
|
||||
)}
|
||||
|
|
@ -149,8 +146,8 @@ export default async function SettingsPage() {
|
|||
label="Maak actief"
|
||||
/>
|
||||
)}
|
||||
{pb.kind === 'member' && !session.isDemo && (
|
||||
<LeaveProductButton productId={pb.id} />
|
||||
{pb.kind === 'member' && (
|
||||
<LeaveProductButton productId={pb.id} isDemo={session.isDemo ?? false} />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { reorderPbisAction, updatePbiPriorityAction } from '@/actions/stories'
|
|||
import { cn } from '@/lib/utils'
|
||||
import { PbiDialog, type PbiDialogState } from './pbi-dialog'
|
||||
import { BacklogCard } from './backlog-card'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { PRIORITY_COLORS } from '@/components/shared/priority-select'
|
||||
import { PBI_STATUS_LABELS, PBI_STATUS_COLORS } from '@/components/shared/pbi-status-select'
|
||||
import type { PbiStatusApi } from '@/lib/task-status'
|
||||
|
|
@ -164,24 +165,30 @@ function SortablePbiRow({
|
|||
{PBI_STATUS_LABELS[pbi.status]}
|
||||
</Badge>
|
||||
}
|
||||
actions={!isDemo ? (
|
||||
actions={
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onEdit() }}
|
||||
className="border border-border rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground hover:bg-surface-container transition-colors"
|
||||
aria-label="Bewerk PBI"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDelete() }}
|
||||
className="text-muted-foreground hover:text-error text-xs"
|
||||
aria-label="Verwijder PBI"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (!isDemo) onEdit() }}
|
||||
className="border border-border rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground hover:bg-surface-container transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Bewerk PBI"
|
||||
disabled={isDemo}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (!isDemo) onDelete() }}
|
||||
className="text-muted-foreground hover:text-error text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Verwijder PBI"
|
||||
disabled={isDemo}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
) : undefined}
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -383,15 +390,16 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
|||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{!isDemo && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}
|
||||
>
|
||||
+ PBI
|
||||
</Button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
@ -400,11 +408,11 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
|
|||
{pbis.length === 0 ? (
|
||||
<div className="p-8 text-center text-muted-foreground text-sm space-y-3">
|
||||
<p>Nog geen PBI's aangemaakt.</p>
|
||||
{!isDemo && (
|
||||
<Button size="sm" variant="outline" onClick={() => setDialogState({ mode: 'create', productId, defaultPriority: 2 })}>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" variant="outline" disabled={isDemo} onClick={() => !isDemo && setDialogState({ mode: 'create', productId, defaultPriority: 2 })}>
|
||||
Maak je eerste PBI aan
|
||||
</Button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Textarea } from '@/components/ui/textarea'
|
|||
import { Badge } from '@/components/ui/badge'
|
||||
import { PrioritySelect, PRIORITY_LABELS, PRIORITY_COLORS } from '@/components/shared/priority-select'
|
||||
import { StoryLog } from '@/components/shared/story-log'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { createStoryAction, updateStoryAction, deleteStoryAction, getStoryLogsAction } from '@/actions/stories'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { Story } from './story-panel'
|
||||
|
|
@ -42,10 +43,10 @@ const STATUS_LABELS: Record<string, string> = {
|
|||
DONE: 'Klaar',
|
||||
}
|
||||
|
||||
function SubmitButton({ label }: { label: string }) {
|
||||
function SubmitButton({ label, disabled }: { label: string; disabled?: boolean }) {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" disabled={pending}>
|
||||
<Button type="submit" disabled={disabled || pending}>
|
||||
{pending ? '…' : label}
|
||||
</Button>
|
||||
)
|
||||
|
|
@ -262,9 +263,9 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
|
|||
)}
|
||||
</div>
|
||||
|
||||
{isEdit && !isDemo && (
|
||||
{isEdit && (
|
||||
<div className="px-5 py-3 border-t border-border shrink-0">
|
||||
{confirmDelete ? (
|
||||
{!isDemo && confirmDelete ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground flex-1">
|
||||
Weet je het zeker? Taken worden ook verwijderd.
|
||||
|
|
@ -277,24 +278,29 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
|
|||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-error hover:bg-error/10"
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
>
|
||||
Story verwijderen
|
||||
</Button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-error hover:bg-error/10"
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setConfirmDelete(true)}
|
||||
>
|
||||
Story verwijderen
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 px-5 py-4 border-t border-border shrink-0 rounded-b-xl bg-muted/50">
|
||||
<DialogClose render={<Button type="button" variant="outline" />}>
|
||||
{isDemo ? 'Sluiten' : 'Annuleren'}
|
||||
Annuleren
|
||||
</DialogClose>
|
||||
{!isDemo && <SubmitButton label={isEdit ? 'Opslaan' : 'Aanmaken'} />}
|
||||
<DemoTooltip show={isDemo}>
|
||||
<SubmitButton label={isEdit ? 'Opslaan' : 'Aanmaken'} disabled={isDemo} />
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { usePlannerStore } from '@/stores/planner-store'
|
|||
import { reorderStoriesAction } from '@/actions/stories'
|
||||
import { StoryDialog, type StoryDialogState } from './story-dialog'
|
||||
import { BacklogCard } from './backlog-card'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type SortMode = 'priority' | 'code' | 'date'
|
||||
|
|
@ -223,14 +224,17 @@ export function StoryPanel({ productId, storiesByPbi, isDemo }: StoryPanelProps)
|
|||
<SelectItem value="DONE">Klaar</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedPbiId && !isDemo && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => setStoryDialogState({ mode: 'create', pbiId: selectedPbiId, productId, defaultPriority: 2 })}
|
||||
>
|
||||
+ Story
|
||||
</Button>
|
||||
{selectedPbiId && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && setStoryDialogState({ mode: 'create', pbiId: selectedPbiId, productId, defaultPriority: 2 })}
|
||||
>
|
||||
+ Story
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
|
@ -244,10 +248,12 @@ export function StoryPanel({ productId, storiesByPbi, isDemo }: StoryPanelProps)
|
|||
) : rawStories.length === 0 ? (
|
||||
<div className="text-center mt-8 space-y-3">
|
||||
<p className="text-sm text-muted-foreground">Nog geen stories voor dit PBI.</p>
|
||||
{!isDemo && selectedPbiId && (
|
||||
<Button size="sm" variant="outline" onClick={() => setStoryDialogState({ mode: 'create', pbiId: selectedPbiId, productId, defaultPriority: 2 })}>
|
||||
Maak je eerste story aan
|
||||
</Button>
|
||||
{selectedPbiId && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" variant="outline" disabled={isDemo} onClick={() => !isDemo && setStoryDialogState({ mode: 'create', pbiId: selectedPbiId, productId, defaultPriority: 2 })}>
|
||||
Maak je eerste story aan
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { toast } from 'sonner'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { CodeBadge } from '@/components/shared/code-badge'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { restoreProductAction } from '@/actions/products'
|
||||
import { setActiveProductAction } from '@/actions/active-product'
|
||||
|
||||
|
|
@ -38,7 +39,6 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
|
|||
}
|
||||
|
||||
function handleActivate(id: string) {
|
||||
if (isDemo) { toast.error('Niet beschikbaar in demo-modus'); return }
|
||||
startTransition(async () => {
|
||||
const result = await setActiveProductAction(id)
|
||||
if (result?.error) toast.error(typeof result.error === 'string' ? result.error : 'Activeren mislukt')
|
||||
|
|
@ -54,11 +54,11 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
|
|||
? 'Geen gearchiveerde producten.'
|
||||
: 'Je hebt nog geen producten aangemaakt.'}
|
||||
</p>
|
||||
{!isDemo && !showArchived && (
|
||||
<Button variant="outline" nativeButton={false} render={<Link href="/products/new" />}>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button variant="outline" nativeButton={false} render={<Link href={isDemo ? '#' : '/products/new'} />} disabled={isDemo}>
|
||||
Maak je eerste product aan
|
||||
</Button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -103,21 +103,27 @@ export function ProductList({ products, isDemo, showArchived = false, activeProd
|
|||
product.id === activeProductId
|
||||
? <Badge className="bg-primary-container text-primary-container-foreground text-xs px-2 py-0">Actief</Badge>
|
||||
: (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleActivate(product.id) }}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
Activeer
|
||||
</button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (!isDemo) handleActivate(product.id) }}
|
||||
className="text-xs text-primary hover:underline disabled:opacity-40 disabled:cursor-not-allowed disabled:no-underline"
|
||||
disabled={isDemo}
|
||||
>
|
||||
Activeer
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
)
|
||||
)}
|
||||
{showArchived && !isDemo && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleRestore(product.id) }}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
Herstellen
|
||||
</button>
|
||||
{showArchived && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); if (!isDemo) handleRestore(product.id) }}
|
||||
className="text-xs text-primary hover:underline disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
disabled={isDemo}
|
||||
>
|
||||
Herstellen
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
import { useState, useTransition } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { leaveProductAction } from '@/actions/products'
|
||||
|
||||
interface LeaveProductButtonProps {
|
||||
productId: string
|
||||
isDemo?: boolean
|
||||
}
|
||||
|
||||
export function LeaveProductButton({ productId }: LeaveProductButtonProps) {
|
||||
export function LeaveProductButton({ productId, isDemo = false }: LeaveProductButtonProps) {
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
|
|
@ -32,13 +34,16 @@ export function LeaveProductButton({ productId }: LeaveProductButtonProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 border-error/40 text-error hover:bg-error/10"
|
||||
onClick={() => setConfirming(true)}
|
||||
>
|
||||
Verlaten
|
||||
</Button>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState, useActionState, useTransition } from 'react'
|
|||
import { useFormStatus } from 'react-dom'
|
||||
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'
|
||||
|
||||
interface Token {
|
||||
|
|
@ -18,12 +19,14 @@ interface TokenManagerProps {
|
|||
isDemo: boolean
|
||||
}
|
||||
|
||||
function CreateSubmitButton() {
|
||||
function CreateSubmitButton({ isDemo }: { isDemo: boolean }) {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending ? 'Aanmaken…' : 'Token aanmaken'}
|
||||
</Button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button type="submit" disabled={isDemo || pending}>
|
||||
{pending ? 'Aanmaken…' : 'Token aanmaken'}
|
||||
</Button>
|
||||
</DemoTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -80,21 +83,19 @@ export function TokenManager({ tokens, isDemo }: TokenManagerProps) {
|
|||
)}
|
||||
|
||||
{/* Create form */}
|
||||
{!isDemo && (
|
||||
<div className="bg-surface-container-low border border-border rounded-xl p-5 space-y-4">
|
||||
<h2 className="text-sm font-medium text-foreground">Nieuw token aanmaken</h2>
|
||||
<form action={formAction} className="flex gap-2">
|
||||
<Input name="label" placeholder="Label (optioneel)" className="flex-1" />
|
||||
<CreateSubmitButton />
|
||||
</form>
|
||||
{typeof state?.error === 'string' && (
|
||||
<p className="text-xs text-error">{state.error}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Maximaal 10 actieve tokens. Je hebt er nu {activeTokens.length}.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-surface-container-low border border-border rounded-xl p-5 space-y-4">
|
||||
<h2 className="text-sm font-medium text-foreground">Nieuw token aanmaken</h2>
|
||||
<form action={formAction} className="flex gap-2">
|
||||
<Input name="label" placeholder="Label (optioneel)" className="flex-1" disabled={isDemo} />
|
||||
<CreateSubmitButton isDemo={isDemo} />
|
||||
</form>
|
||||
{typeof state?.error === 'string' && (
|
||||
<p className="text-xs text-error">{state.error}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Maximaal 10 actieve tokens. Je hebt er nu {activeTokens.length}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Active tokens */}
|
||||
<div className="space-y-2">
|
||||
|
|
@ -111,16 +112,17 @@ export function TokenManager({ tokens, isDemo }: TokenManagerProps) {
|
|||
Aangemaakt {new Date(token.created_at).toLocaleDateString('nl-NL')}
|
||||
</p>
|
||||
</div>
|
||||
{!isDemo && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-error hover:bg-error/10 shrink-0"
|
||||
onClick={() => handleRevoke(token.id)}
|
||||
disabled={isDemo}
|
||||
onClick={() => !isDemo && handleRevoke(token.id)}
|
||||
>
|
||||
Intrekken
|
||||
</Button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useRouter } from 'next/navigation'
|
||||
import { useTransition } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { setActiveProductAction } from '@/actions/active-product'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -18,7 +19,6 @@ export function ActivateProductButton({ productId, isDemo, redirectTo, label = '
|
|||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
function handleActivate() {
|
||||
if (isDemo) { toast.error('Niet beschikbaar in demo-modus'); return }
|
||||
startTransition(async () => {
|
||||
const result = await setActiveProductAction(productId)
|
||||
if (result?.error) toast.error(typeof result.error === 'string' ? result.error : 'Activeren mislukt')
|
||||
|
|
@ -28,12 +28,14 @@ export function ActivateProductButton({ productId, isDemo, redirectTo, label = '
|
|||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleActivate}
|
||||
disabled={isPending}
|
||||
className="text-xs text-primary hover:underline font-medium disabled:opacity-50"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={() => !isDemo && handleActivate()}
|
||||
disabled={isDemo || isPending}
|
||||
className="text-xs text-primary hover:underline font-medium disabled:opacity-50 disabled:no-underline"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</DemoTooltip>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,15 +189,16 @@ function SortableSprintRow({
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</DemoTooltip>
|
||||
{!isDemo && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); onRemove() }}
|
||||
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-error"
|
||||
onClick={e => { e.stopPropagation(); if (!isDemo) onRemove() }}
|
||||
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-error disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Verwijder uit sprint"
|
||||
disabled={isDemo}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -352,14 +353,15 @@ function DraggablePbiStoryRow({
|
|||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{!isDemo && (
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button
|
||||
onClick={onAdd}
|
||||
className="text-xs text-primary hover:underline shrink-0"
|
||||
onClick={() => !isDemo && onAdd()}
|
||||
className="text-xs text-primary hover:underline shrink-0 disabled:opacity-40 disabled:cursor-not-allowed disabled:no-underline"
|
||||
disabled={isDemo}
|
||||
>
|
||||
+ Toevoegen
|
||||
</button>
|
||||
)}
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
createTaskAction, updateTaskStatusAction, updateTaskAction,
|
||||
deleteTaskAction, reorderTasksAction,
|
||||
} from '@/actions/tasks'
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const STATUS_CYCLE: Record<string, 'TO_DO' | 'IN_PROGRESS' | 'DONE'> = {
|
||||
|
|
@ -99,7 +100,7 @@ function SortableTaskRow({
|
|||
PRIORITY_BORDER[task.priority]
|
||||
)}>
|
||||
{!isDemo && (
|
||||
<span {...attributes} {...listeners} className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 text-sm select-none mt-0.5">⠿</span>
|
||||
<span {...attributes} {...listeners} className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 text-sm select-none mt-0.5" aria-hidden="true">⠿</span>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
|
|
@ -114,12 +115,14 @@ function SortableTaskRow({
|
|||
{STATUS_LABELS[task.status]}
|
||||
</Badge>
|
||||
</button>
|
||||
{!isDemo && (
|
||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1 shrink-0">
|
||||
<button onClick={() => setEditing(true)} className="text-xs text-muted-foreground hover:text-foreground">Bewerk</button>
|
||||
<button onClick={onDelete} aria-label="Verwijder taak" className="text-xs text-muted-foreground hover:text-error">×</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1 shrink-0">
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button onClick={() => !isDemo && setEditing(true)} disabled={isDemo} className="text-xs text-muted-foreground hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed">Bewerk</button>
|
||||
</DemoTooltip>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<button onClick={() => !isDemo && onDelete()} disabled={isDemo} aria-label="Verwijder taak" className="text-xs text-muted-foreground hover:text-error disabled:opacity-40 disabled:cursor-not-allowed">×</button>
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -220,9 +223,9 @@ export function TaskList({ storyId, storyCode, sprintId, productId: _productId,
|
|||
actions={
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground">{doneCount}/{orderedTasks.length} klaar</span>
|
||||
{!isDemo && (
|
||||
<Button size="sm" className="h-7 text-xs" onClick={() => setCreating(true)}>+ Taak</Button>
|
||||
)}
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" className="h-7 text-xs" disabled={isDemo} onClick={() => !isDemo && setCreating(true)}>+ Taak</Button>
|
||||
</DemoTooltip>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
@ -235,7 +238,9 @@ export function TaskList({ storyId, storyCode, sprintId, productId: _productId,
|
|||
{orderedTasks.length === 0 && !creating ? (
|
||||
<div className="text-center mt-8 space-y-3">
|
||||
<p className="text-sm text-muted-foreground">Geen taken voor deze story.</p>
|
||||
{!isDemo && <Button size="sm" variant="outline" onClick={() => setCreating(true)}>Maak eerste taak aan</Button>}
|
||||
<DemoTooltip show={isDemo}>
|
||||
<Button size="sm" variant="outline" disabled={isDemo} onClick={() => !isDemo && setCreating(true)}>Maak eerste taak aan</Button>
|
||||
</DemoTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue