feat(PBI-96/T-1072): /docs/settings page + folder-toggle
- app/(app)/products/[id]/docs/settings/page.tsx: server-route met breadcrumb + intro, geeft isOwner door aan toggle-component. - components/product-docs/product-doc-folder-toggle.tsx: client met 8 checkboxes (één per ProductDocFolder enum-lid). Owner kan toggelen → toggleProductDocFolderAction (optimistic update + rollback bij error). ProductMember (niet-owner) krijgt waarschuwing en disabled checkboxes. DemoTooltip-wrapped, demo kan niets togglen. - Voetnoot legt anti-data-loss uit: "Folders uitzetten verwijdert geen bestaande docs". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a892ff83ca
commit
e32ae45eb7
2 changed files with 198 additions and 0 deletions
56
app/(app)/products/[id]/docs/settings/page.tsx
Normal file
56
app/(app)/products/[id]/docs/settings/page.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Product Docs folder-settings page (PBI-96 / T-1072). Server-component.
|
||||
// Owner-only voor schrijven; ProductMember ziet read-only checkboxes.
|
||||
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getAccessibleProduct } from '@/lib/product-access'
|
||||
import { ProductDocFolderToggle } from '@/components/product-docs/product-doc-folder-toggle'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export default async function ProductDocsSettingsPage({ params }: Props) {
|
||||
const { id } = await params
|
||||
const session = await getSession()
|
||||
if (!session.userId) redirect('/login')
|
||||
|
||||
const product = await getAccessibleProduct(id, session.userId)
|
||||
if (!product) notFound()
|
||||
|
||||
const isOwner = product.user_id === session.userId
|
||||
const isDemo = session.isDemo ?? false
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl mx-auto space-y-4">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Link
|
||||
href={`/products/${id}/docs`}
|
||||
className="inline-flex items-center gap-1 hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-3" />
|
||||
Documentatie
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-foreground font-medium">Folder-instellingen</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Folder-instellingen</h1>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Bepaal welke documentatie-folders zichtbaar zijn voor dit product.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProductDocFolderToggle
|
||||
productId={id}
|
||||
initialEnabledFolders={product.enabled_doc_folders}
|
||||
isOwner={isOwner}
|
||||
isDemo={isDemo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
142
components/product-docs/product-doc-folder-toggle.tsx
Normal file
142
components/product-docs/product-doc-folder-toggle.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
'use client'
|
||||
|
||||
// Folder-toggle UI voor /docs/settings. 8 checkboxes (één per
|
||||
// ProductDocFolder enum-lid). Owner kan toggelen; ProductMember ziet
|
||||
// read-only checkboxes met waarschuwing.
|
||||
//
|
||||
// DemoTooltip-wrapped per checkbox; demo-user kan niets togglen.
|
||||
|
||||
import { useState, useTransition } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { DemoTooltip } from '@/components/shared/demo-tooltip'
|
||||
import { debugProps } from '@/lib/debug'
|
||||
import {
|
||||
PRODUCT_DOC_FOLDERS,
|
||||
type ProductDocFolderApi,
|
||||
} from '@/lib/schemas/product-doc'
|
||||
import { productDocFolderToApi } from '@/lib/product-doc-folder'
|
||||
import { toggleProductDocFolderAction } from '@/actions/product-docs'
|
||||
|
||||
import type { ProductDocFolder } from '@prisma/client'
|
||||
|
||||
const FOLDER_LABELS: Record<ProductDocFolderApi, string> = {
|
||||
adr: 'ADRs — Architecture Decision Records',
|
||||
architecture: 'Architecture — Topische arch-bestanden',
|
||||
patterns: 'Patterns — Herbruikbare code-patronen',
|
||||
plans: 'Plans — Feature- en PBI-plannen',
|
||||
runbooks: 'Runbooks — Operationele procedures',
|
||||
specs: 'Specs — Functionele specificaties',
|
||||
manual: 'Manual — Developer manual',
|
||||
api: 'API — API-contract details',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
productId: string
|
||||
/** Folders die nu enabled zijn (DB-enum). */
|
||||
initialEnabledFolders: ProductDocFolder[]
|
||||
isOwner: boolean
|
||||
isDemo: boolean
|
||||
}
|
||||
|
||||
export function ProductDocFolderToggle({
|
||||
productId,
|
||||
initialEnabledFolders,
|
||||
isOwner,
|
||||
isDemo,
|
||||
}: Props) {
|
||||
const router = useRouter()
|
||||
const initialApiSet = new Set(
|
||||
initialEnabledFolders.map((f) => productDocFolderToApi(f)),
|
||||
)
|
||||
const [enabledSet, setEnabledSet] = useState<Set<ProductDocFolderApi>>(initialApiSet)
|
||||
const [pendingFolder, setPendingFolder] = useState<ProductDocFolderApi | null>(null)
|
||||
const [submitting, startSubmit] = useTransition()
|
||||
|
||||
function toggle(folder: ProductDocFolderApi) {
|
||||
const currentlyEnabled = enabledSet.has(folder)
|
||||
const targetEnabled = !currentlyEnabled
|
||||
|
||||
// Optimistic update
|
||||
const next = new Set(enabledSet)
|
||||
if (targetEnabled) next.add(folder)
|
||||
else next.delete(folder)
|
||||
setEnabledSet(next)
|
||||
setPendingFolder(folder)
|
||||
|
||||
startSubmit(async () => {
|
||||
const r = await toggleProductDocFolderAction({
|
||||
product_id: productId,
|
||||
folder,
|
||||
enabled: targetEnabled,
|
||||
})
|
||||
if ('error' in r) {
|
||||
// Rollback
|
||||
setEnabledSet(enabledSet)
|
||||
toast.error(r.error)
|
||||
} else {
|
||||
toast.success(targetEnabled ? `Folder ${folder} aangezet` : `Folder ${folder} uitgezet`)
|
||||
router.refresh()
|
||||
}
|
||||
setPendingFolder(null)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="space-y-3"
|
||||
{...debugProps(
|
||||
'product-doc-folder-toggle',
|
||||
'ProductDocFolderToggle',
|
||||
'components/product-docs/product-doc-folder-toggle.tsx',
|
||||
)}
|
||||
>
|
||||
{!isOwner && (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
Alleen de eigenaar van dit product kan folders aan- of uitzetten.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{PRODUCT_DOC_FOLDERS.map((folder) => {
|
||||
const checked = enabledSet.has(folder)
|
||||
const isPending = pendingFolder === folder
|
||||
const disabled = !isOwner || isDemo || submitting
|
||||
|
||||
return (
|
||||
<li key={folder}>
|
||||
<DemoTooltip show={isDemo}>
|
||||
<label
|
||||
className={`flex items-start gap-2 rounded-md border border-border p-2 ${
|
||||
disabled ? 'opacity-70' : 'hover:bg-surface-container/60 cursor-pointer'
|
||||
}`}
|
||||
data-debug-id={`product-doc-folder-toggle__row--${folder}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={() => toggle(folder)}
|
||||
className="mt-0.5 accent-primary"
|
||||
/>
|
||||
<div className="flex-1 text-xs">
|
||||
<p className="font-medium">{FOLDER_LABELS[folder]}</p>
|
||||
{isPending && (
|
||||
<p className="text-muted-foreground text-[10px]">Bezig…</p>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</DemoTooltip>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Folders uitzetten verwijdert geen bestaande docs — die blijven leesbaar
|
||||
via directe URL en kunnen worden verwijderd voor cleanup.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue