anton-bakker-feedback 0.4.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -0
- package/dist/chunks/ingest-transport-DAA_pJNb.js +245 -0
- package/dist/chunks/ingest-transport-DAA_pJNb.js.map +1 -0
- package/dist/chunks/normalisers-Cbd-i4Jh.js +97 -0
- package/dist/chunks/normalisers-Cbd-i4Jh.js.map +1 -0
- package/dist/chunks/{observability-CfwHKObh.js → observability-DUK725zv.js} +3 -3
- package/dist/chunks/{observability-CfwHKObh.js.map → observability-DUK725zv.js.map} +1 -1
- package/dist/chunks/{submitResolver-DsfkrUHY.js → submitResolver-wfr4kbEM.js} +60 -39
- package/dist/chunks/submitResolver-wfr4kbEM.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/ingest-transport.d.ts +97 -0
- package/dist/core/ingest-transport.d.ts.map +1 -1
- package/dist/core/normalisers.d.ts +29 -0
- package/dist/core/normalisers.d.ts.map +1 -0
- package/dist/core/observability.d.ts.map +1 -1
- package/dist/core/state-metrics.d.ts +19 -0
- package/dist/core/state-metrics.d.ts.map +1 -0
- package/dist/core/transforms.d.ts.map +1 -1
- package/dist/core/types.d.ts +93 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +16 -98
- package/dist/index.js.map +1 -1
- package/dist/react/admin/ActivityTimeline.d.ts +9 -0
- package/dist/react/admin/ActivityTimeline.d.ts.map +1 -0
- package/dist/react/admin/AnnotationComposer.d.ts +30 -0
- package/dist/react/admin/AnnotationComposer.d.ts.map +1 -0
- package/dist/react/admin/FeedbackListPage.d.ts.map +1 -1
- package/dist/react/admin/FeedbackUpdatePage.d.ts +6 -0
- package/dist/react/admin/FeedbackUpdatePage.d.ts.map +1 -1
- package/dist/react/admin/LifecycleControls.d.ts +28 -0
- package/dist/react/admin/LifecycleControls.d.ts.map +1 -0
- package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts +2 -0
- package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts.map +1 -0
- package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts +2 -0
- package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts.map +1 -0
- package/dist/react/admin/__tests__/LifecycleControls.test.d.ts +2 -0
- package/dist/react/admin/__tests__/LifecycleControls.test.d.ts.map +1 -0
- package/dist/react/admin/hooks/can.d.ts +47 -0
- package/dist/react/admin/hooks/can.d.ts.map +1 -0
- package/dist/react/admin/hooks/useStatusTransition.d.ts +16 -2
- package/dist/react/admin/hooks/useStatusTransition.d.ts.map +1 -1
- package/dist/react/admin/index.d.ts +14 -1
- package/dist/react/admin/index.d.ts.map +1 -1
- package/dist/react/admin/index.js +312 -29
- package/dist/react/admin/index.js.map +1 -1
- package/dist/react/admin/types.d.ts +13 -0
- package/dist/react/admin/types.d.ts.map +1 -1
- package/dist/react/resolver/submitResolver.d.ts +10 -7
- package/dist/react/resolver/submitResolver.d.ts.map +1 -1
- package/dist/react.js +1 -1
- package/dist/server/index.d.ts +4 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/listHandler.d.ts +15 -8
- package/dist/server/listHandler.d.ts.map +1 -1
- package/dist/server.js +22 -170
- package/dist/server.js.map +1 -1
- package/package.json +13 -1
- package/dist/chunks/submitResolver-DsfkrUHY.js.map +0 -1
- package/dist/server/FeedbackService.d.ts +0 -46
- package/dist/server/FeedbackService.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/react/admin/FeedbackListPage.tsx","../../../src/react/admin/FeedbackDetailPage.tsx","../../../src/react/admin/hooks/useStatusTransition.ts","../../../src/react/admin/FeedbackUpdatePage.tsx"],"sourcesContent":["/**\n * FeedbackListPage.tsx\n * Location: src/react/admin/FeedbackListPage.tsx\n *\n * Admin list page for browsing and filtering feedback items.\n * Thin wrapper around ListPage from anton-bakker-tailwind-components\n * for visual consistency with sagrosoft20.\n */\nimport * as React from 'react';\nimport { ListPage, Badge } from 'anton-bakker-tailwind-components';\nimport type { SimpleColumn, FilterBarProps } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType } from '../../core/types.js';\nimport type { FeedbackListPageProps } from './types.js';\n\nconst STATUS_VARIANT: Record<FeedbackStatus, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n open: 'warning',\n approved: 'default',\n deferred: 'secondary',\n resolved: 'success',\n wontfix: 'destructive',\n};\n\nconst TYPE_VARIANT: Record<FeedbackType, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n bug: 'destructive',\n feature: 'default',\n ux: 'warning',\n other: 'secondary',\n};\n\nfunction StatusBadge({ status }: { status: FeedbackStatus }) {\n return <Badge variant={STATUS_VARIANT[status] ?? 'secondary'}>{status}</Badge>;\n}\n\nfunction TypeBadge({ type }: { type: FeedbackType }) {\n return <Badge variant={TYPE_VARIANT[type] ?? 'secondary'}>{type}</Badge>;\n}\n\nconst FeedbackListPage = React.forwardRef<HTMLDivElement, FeedbackListPageProps>(\n ({ items, loading = false, onNavigate, className, emptyStateMessage, currentUser, ...rest }, ref) => {\n const [search, setSearch] = React.useState('');\n const [statusFilter, setStatusFilter] = React.useState('');\n const [typeFilter, setTypeFilter] = React.useState('');\n\n const filteredItems = React.useMemo(() => {\n let result = items;\n if (statusFilter) result = result.filter((i) => i.status === statusFilter);\n if (typeFilter) result = result.filter((i) => i.type === typeFilter);\n if (search) {\n const q = search.toLowerCase();\n result = result.filter((i) => i.content?.toLowerCase().includes(q) || i.id.toLowerCase().includes(q));\n }\n return result;\n }, [items, statusFilter, typeFilter, search]);\n\n const columns: SimpleColumn<StoredFeedbackItem>[] = React.useMemo(() => [\n { header: 'Content', accessor: (row) => row.content?.slice(0, 60) || row.id, sortable: true },\n { header: 'Type', accessor: (row) => <TypeBadge type={row.type} />, sortable: true },\n { header: 'Status', accessor: (row) => <StatusBadge status={row.status} />, sortable: true },\n { header: 'Priority', accessor: 'priority', sortable: true },\n { header: 'Reporters', accessor: 'reporterCount', sortable: true },\n {\n header: 'Date',\n accessor: (row) => row.submittedAt ? new Date(row.submittedAt).toLocaleDateString() : '—',\n sortable: true,\n },\n ], []);\n\n const filterBar: FilterBarProps | undefined = (statusFilter || typeFilter) ? {\n filters: [\n ...(statusFilter ? [{ field: 'status', label: `Status: ${statusFilter}`, value: statusFilter }] : []),\n ...(typeFilter ? [{ field: 'type', label: `Type: ${typeFilter}`, value: typeFilter }] : []),\n ],\n onFilterRemove: (field: string) => {\n if (field === 'status') setStatusFilter('');\n if (field === 'type') setTypeFilter('');\n },\n onFiltersClear: () => { setStatusFilter(''); setTypeFilter(''); },\n } : undefined;\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <select\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by status\"\n >\n <option value=\"\">All statuses</option>\n <option value=\"open\">Open</option>\n <option value=\"approved\">Approved</option>\n <option value=\"deferred\">Deferred</option>\n <option value=\"resolved\">Resolved</option>\n <option value=\"wontfix\">Won't fix</option>\n </select>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by type\"\n >\n <option value=\"\">All types</option>\n <option value=\"bug\">Bug</option>\n <option value=\"feature\">Feature</option>\n <option value=\"ux\">UX</option>\n <option value=\"other\">Other</option>\n </select>\n </div>\n );\n\n const listProps = {\n title: 'Feedback' as const,\n columns,\n items: filteredItems,\n loading,\n onRowClick: (row: StoredFeedbackItem) => onNavigate(`/feedback/${row.id}`),\n search,\n onSearchChange: setSearch,\n searchPlaceholder: 'Search feedback...',\n headerActions,\n testId: 'feedback-list-page',\n ...(filterBar ? { filterBar } : {}),\n ...(emptyStateMessage ? { emptyState: emptyStateMessage } : {}),\n ...(className ? { className } : {}),\n };\n\n return <ListPage<StoredFeedbackItem> ref={ref} {...listProps} />;\n },\n);\nFeedbackListPage.displayName = 'FeedbackListPage';\n\nexport { FeedbackListPage, StatusBadge, TypeBadge };\n","/**\n * FeedbackDetailPage.tsx\n * Location: src/react/admin/FeedbackDetailPage.tsx\n *\n * Admin detail page for viewing a single feedback item.\n * Renders structured content (bug reports, feature requests) with all fields,\n * screenshot, console errors, and dialogue history.\n */\nimport * as React from 'react';\nimport { DetailPage, DetailSection, DetailRow, Badge, Button } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem } from '../../core/types.js';\nimport type { FeedbackDetailPageProps } from './types.js';\nimport { StatusBadge, TypeBadge } from './FeedbackListPage.js';\n\n/**\n * Parse structured content (produced by serializeManualForm or AI dialogue)\n * into labeled key-value pairs.\n * Format: \"Label: value\" lines, with optional [Bug Report] or [Feature Request] header.\n */\nfunction parseStructuredContent(content: string): { header: string | undefined; fields: { label: string; value: string }[]; raw: string } {\n const lines = content.split('\\n');\n let header: string | undefined;\n const fields: { label: string; value: string }[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check for header line like [Bug Report] or [Feature Request]\n const headerMatch = trimmed.match(/^\\[(.+)\\]$/);\n if (headerMatch && headerMatch[1]) {\n header = headerMatch[1];\n continue;\n }\n\n // Check for \"Label: value\" pattern\n const fieldMatch = trimmed.match(/^([A-Z][A-Za-z\\s]+):\\s*(.+)$/);\n if (fieldMatch && fieldMatch[1] && fieldMatch[2]) {\n fields.push({ label: fieldMatch[1], value: fieldMatch[2] });\n }\n }\n\n return { header, fields, raw: content };\n}\n\nconst FeedbackDetailPage = React.forwardRef<HTMLDivElement, FeedbackDetailPageProps>(\n ({ feedbackId, fetchItem, getScreenshotUrl, onNavigate, currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [error, setError] = React.useState<string | null>(null);\n const [screenshotUrl, setScreenshotUrl] = React.useState<string | null>(null);\n\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n setError(null);\n fetchItem(feedbackId)\n .then((data) => { if (!cancelled) setItem(data); })\n .catch((err) => { if (!cancelled) setError(err?.message ?? 'Failed to load item'); })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n // Load screenshot URL when item has one\n React.useEffect(() => {\n if (item?.hasScreenshot && item.screenshotS3Key && getScreenshotUrl) {\n getScreenshotUrl(item.screenshotS3Key)\n .then(setScreenshotUrl)\n .catch(() => setScreenshotUrl(null));\n }\n }, [item?.hasScreenshot, item?.screenshotS3Key, getScreenshotUrl]);\n\n if (error) {\n return (\n <div ref={ref} className={className}>\n <DetailPage title=\"Feedback\" headerActions={<Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>}>\n <div className=\"p-6 text-destructive\">{error}</div>\n </DetailPage>\n </div>\n );\n }\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>\n <Button onClick={() => onNavigate(`/feedback/${feedbackId}/edit`)}>Edit</Button>\n </div>\n );\n\n // Parse structured content\n const parsed = item ? parseStructuredContent(item.content) : null;\n const hasStructuredFields = parsed && parsed.fields.length > 0;\n\n return (\n <div ref={ref} className={className}>\n <DetailPage\n title={item?.content?.split('\\n').find(l => l.match(/^Summary:/))?.replace('Summary: ', '') || parsed?.header || `Feedback ${feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n loading={loading}\n testId=\"feedback-detail-page\"\n >\n {item && (\n <>\n <DetailSection title=\"Overview\">\n <DetailRow label=\"Status\"><StatusBadge status={item.status} /></DetailRow>\n <DetailRow label=\"Type\"><TypeBadge type={item.type} /></DetailRow>\n <DetailRow label=\"Priority\">{item.priority}</DetailRow>\n <DetailRow label=\"Environment\">{item.environment || '—'}</DetailRow>\n <DetailRow label=\"Submitted\">{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : '—'}</DetailRow>\n <DetailRow label=\"Reporters\">{item.reporterCount ?? 1}</DetailRow>\n {item.assignedTo && <DetailRow label=\"Assigned to\">{item.assignedTo}</DetailRow>}\n {item.scope && <DetailRow label=\"Scope\"><Badge variant=\"outline\">{item.scope}</Badge></DetailRow>}\n {item.size && <DetailRow label=\"Size\"><Badge variant=\"outline\">{item.size}</Badge></DetailRow>}\n </DetailSection>\n\n <DetailSection title=\"Content\">\n {hasStructuredFields ? (\n // Render each structured field as a separate row\n parsed.fields.map((field, i) => (\n <DetailRow key={i} label={field.label}>\n <p className=\"whitespace-pre-wrap\">{field.value}</p>\n </DetailRow>\n ))\n ) : (\n // Fallback: render raw content\n <DetailRow label=\"Description\">\n <p className=\"whitespace-pre-wrap\">{item.content}</p>\n </DetailRow>\n )}\n </DetailSection>\n\n {/* Screenshot */}\n {item.hasScreenshot && (\n <DetailSection title=\"Screenshot\">\n <DetailRow label=\"Capture\">\n {screenshotUrl ? (\n <a href={screenshotUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <img\n src={screenshotUrl}\n alt=\"Feedback screenshot\"\n className=\"max-w-full max-h-[400px] rounded border cursor-pointer hover:opacity-90 transition-opacity\"\n />\n </a>\n ) : (\n <span className=\"text-muted-foreground text-sm\">\n {getScreenshotUrl ? 'Loading screenshot…' : 'Screenshot available (no viewer configured)'}\n </span>\n )}\n </DetailRow>\n </DetailSection>\n )}\n\n {/* Console errors */}\n {item.context?.consoleErrors && item.context.consoleErrors.length > 0 && (\n <DetailSection title=\"Console Errors\" collapsible defaultOpen>\n <DetailRow label={`${item.context.consoleErrors.length} error(s)`}>\n <ul className=\"list-none space-y-1\">\n {item.context.consoleErrors.map((err, i) => (\n <li key={i} className=\"font-mono text-xs text-destructive bg-destructive/5 p-2 rounded\">\n {err}\n </li>\n ))}\n </ul>\n </DetailRow>\n </DetailSection>\n )}\n\n {/* Dialogue history */}\n {item.dialogueHistory && item.dialogueHistory.length > 0 && (\n <DetailSection title=\"Dialogue History\" collapsible defaultOpen={false}>\n {item.dialogueHistory.map((entry, i) => (\n <DetailRow key={i} label={entry.role === 'user' ? 'User' : 'Assistant'}>\n <p className=\"whitespace-pre-wrap text-sm\">{entry.content}</p>\n {entry.timestamp && (\n <span className=\"text-xs text-muted-foreground\">\n {new Date(entry.timestamp).toLocaleString()}\n </span>\n )}\n </DetailRow>\n ))}\n </DetailSection>\n )}\n\n {/* Browser context */}\n {item.context && (\n <DetailSection title=\"Browser Context\" collapsible defaultOpen={false}>\n {item.context.url && <DetailRow label=\"URL\">{item.context.url}</DetailRow>}\n {item.context.pageTitle && <DetailRow label=\"Page\">{item.context.pageTitle}</DetailRow>}\n {item.context.userAgent && <DetailRow label=\"User Agent\">{item.context.userAgent}</DetailRow>}\n {item.context.viewport && <DetailRow label=\"Viewport\">{`${item.context.viewport.width}×${item.context.viewport.height}`}</DetailRow>}\n {item.context.locale && <DetailRow label=\"Locale\">{item.context.locale}</DetailRow>}\n {item.context.timezone && <DetailRow label=\"Timezone\">{item.context.timezone}</DetailRow>}\n {item.context.performanceMs != null && <DetailRow label=\"Page Load\">{`${item.context.performanceMs}ms`}</DetailRow>}\n </DetailSection>\n )}\n\n {/* Tags */}\n {item.tags && item.tags.length > 0 && (\n <DetailSection title=\"Tags\">\n <DetailRow label=\"Tags\">\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n </DetailRow>\n </DetailSection>\n )}\n </>\n )}\n </DetailPage>\n </div>\n );\n },\n);\nFeedbackDetailPage.displayName = 'FeedbackDetailPage';\n\nexport { FeedbackDetailPage };\n","/**\n * useStatusTransition — pure hook encoding the FeedbackStatus transition table.\n *\n * Returns the set of valid target statuses reachable from the current status.\n * The current status is excluded from allowedTargets (can't transition to self).\n *\n * No side effects — same inputs always produce same outputs.\n */\nimport type { FeedbackStatus } from '../../../core/types.js';\n\n/**\n * Valid transitions for each FeedbackStatus.\n * open → resolved is deliberately excluded (must go through approved first).\n */\nconst ALLOWED_TRANSITIONS: Record<FeedbackStatus, FeedbackStatus[]> = {\n open: ['approved', 'deferred', 'wontfix'],\n approved: ['open', 'resolved', 'deferred'],\n deferred: ['open', 'approved', 'wontfix'],\n resolved: ['open', 'approved'],\n wontfix: ['open', 'deferred'],\n};\n\nexport interface UseStatusTransitionResult {\n /** Statuses reachable from the current status (current status excluded) */\n allowedTargets: FeedbackStatus[];\n}\n\n/**\n * Returns the allowed target statuses from the given current status.\n *\n * @param currentStatus - The status the item currently has\n * @returns Object with `allowedTargets` — statuses the item can transition to\n *\n * @example\n * const { allowedTargets } = useStatusTransition('open');\n * // allowedTargets → ['approved', 'deferred', 'wontfix']\n */\nexport function useStatusTransition(currentStatus: FeedbackStatus): UseStatusTransitionResult {\n const allowedTargets = ALLOWED_TRANSITIONS[currentStatus] ?? [];\n return { allowedTargets };\n}\n","/**\n * FeedbackUpdatePage.tsx\n * Location: src/react/admin/FeedbackUpdatePage.tsx\n *\n * Admin update page for editing mutable fields of a feedback item.\n * Uses UpdatePage + EnumField + TextField + TextareaField from anton-bakker-tailwind-components.\n */\nimport * as React from 'react';\nimport { UpdatePage, EnumField, TextField, TextareaField, Button, SaveFooter, Badge } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType, FeedbackScope, FeedbackSize } from '../../core/types.js';\nimport type { FeedbackUpdatePageProps } from './types.js';\nimport { useStatusTransition } from './hooks/useStatusTransition.js';\n\nconst STATUS_OPTIONS = [\n { value: 'open', label: 'Open' },\n { value: 'approved', label: 'Approved' },\n { value: 'deferred', label: 'Deferred' },\n { value: 'resolved', label: 'Resolved' },\n { value: 'wontfix', label: \"Won't Fix\" },\n];\n\nconst TYPE_OPTIONS = [\n { value: 'bug', label: 'Bug' },\n { value: 'feature', label: 'Feature' },\n { value: 'ux', label: 'UX Issue' },\n { value: 'other', label: 'Other' },\n];\n\nconst SCOPE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'component', label: 'Component' },\n { value: 'page', label: 'Page' },\n { value: 'workflow', label: 'Workflow' },\n];\n\nconst SIZE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'trivial', label: 'Trivial' },\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n];\n\nconst FeedbackUpdatePage = React.forwardRef<HTMLDivElement, FeedbackUpdatePageProps>(\n ({ feedbackId, fetchItem, updateItem, onNavigate, currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [saving, setSaving] = React.useState(false);\n\n // Form state\n const [status, setStatus] = React.useState<FeedbackStatus>('open');\n const [type, setType] = React.useState<FeedbackType>('bug');\n const [scope, setScope] = React.useState<string>('');\n const [size, setSize] = React.useState<string>('');\n const [assignedTo, setAssignedTo] = React.useState('');\n const [tags, setTags] = React.useState('');\n const [correctionNote, setCorrectionNote] = React.useState('');\n\n // Load item\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n fetchItem(feedbackId)\n .then((data) => {\n if (cancelled) return;\n setItem(data);\n setStatus(data.status);\n setType(data.type);\n setScope(data.scope ?? '');\n setSize(data.size ?? '');\n setAssignedTo(data.assignedTo ?? '');\n setTags(data.tags?.join(', ') ?? '');\n })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n const { allowedTargets } = useStatusTransition(status);\n const statusOptions = STATUS_OPTIONS.filter(\n (opt) => opt.value === status || allowedTargets.includes(opt.value as FeedbackStatus),\n );\n\n // Dirty detection\n const classificationChanged = item && (type !== item.type || scope !== (item.scope ?? '') || size !== (item.size ?? ''));\n const isDirty = item && (\n status !== item.status ||\n type !== item.type ||\n scope !== (item.scope ?? '') ||\n size !== (item.size ?? '') ||\n assignedTo !== (item.assignedTo ?? '') ||\n tags !== (item.tags?.join(', ') ?? '')\n );\n\n const handleSave = async () => {\n if (!item) return;\n setSaving(true);\n try {\n const patch: Partial<StoredFeedbackItem> = { lastModifiedAt: new Date().toISOString() };\n if (status !== item.status) patch.status = status;\n if (type !== item.type) patch.type = type;\n if (scope !== (item.scope ?? '')) (patch as Record<string, unknown>).scope = scope || undefined;\n if (size !== (item.size ?? '')) (patch as Record<string, unknown>).size = size || undefined;\n if (assignedTo !== (item.assignedTo ?? '')) (patch as Record<string, unknown>).assignedTo = assignedTo || undefined;\n const newTags = tags.split(',').map((t) => t.trim()).filter(Boolean);\n if (JSON.stringify(newTags) !== JSON.stringify(item.tags ?? [])) patch.tags = newTags;\n\n if (status === 'resolved' && item.status !== 'resolved') {\n patch.resolvedAt = new Date().toISOString();\n patch.resolvedBy = currentUser?.userId ?? 'unknown';\n }\n\n if (classificationChanged && correctionNote) {\n patch.corrections = [\n ...(item.corrections ?? []),\n {\n field: [type !== item.type && 'type', scope !== (item.scope ?? '') && 'scope', size !== (item.size ?? '') && 'size'].filter(Boolean).join(', '),\n before: { type: item.type, scope: item.scope, size: item.size },\n after: { type, scope: scope || undefined, size: size || undefined },\n correctedBy: currentUser?.userId ?? 'unknown',\n correctedAt: new Date().toISOString(),\n reason: correctionNote,\n },\n ];\n }\n\n await updateItem(feedbackId, patch);\n onNavigate(`/feedback/${feedbackId}`);\n } finally {\n setSaving(false);\n }\n };\n\n const headerActions = (\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>\n Cancel\n </Button>\n );\n\n return (\n <div ref={ref} className={className}>\n <UpdatePage\n title={`Edit: ${item?.content?.slice(0, 40) || feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n dataLoading={loading}\n actions={\n <SaveFooter dirty={!!isDirty} saving={saving}>\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>Cancel</Button>\n <Button\n onClick={handleSave}\n disabled={saving || !isDirty || (!!classificationChanged && !correctionNote)}\n >\n {saving ? 'Saving…' : 'Save changes'}\n </Button>\n </SaveFooter>\n }\n testId=\"feedback-update-page\"\n >\n {item && (\n <div className=\"space-y-6\">\n {/* Read-only context */}\n <div className=\"rounded-lg border border-border p-4 bg-muted/30\">\n <p className=\"text-sm text-muted-foreground mb-2\">Original submission</p>\n <p className=\"whitespace-pre-wrap text-sm\">{item.content}</p>\n <div className=\"flex gap-3 mt-3 text-xs text-muted-foreground\">\n <span>{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : ''}</span>\n <span>{item.environment}</span>\n <span>{item.reporterCount ?? 1} reporter(s)</span>\n </div>\n </div>\n\n {/* Editable fields */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <EnumField\n label=\"Status\"\n options={statusOptions}\n value={status}\n onChange={(v) => setStatus(v as FeedbackStatus)}\n />\n <EnumField\n label=\"Type\"\n options={TYPE_OPTIONS}\n value={type}\n onChange={(v) => setType(v as FeedbackType)}\n />\n <EnumField\n label=\"Scope\"\n options={SCOPE_OPTIONS}\n value={scope}\n onChange={setScope}\n />\n <EnumField\n label=\"Size\"\n options={SIZE_OPTIONS}\n value={size}\n onChange={setSize}\n />\n <TextField\n label=\"Assigned to\"\n value={assignedTo}\n onChange={(e) => setAssignedTo(e.target.value)}\n placeholder={currentUser?.name ?? currentUser?.userId ?? ''}\n {...(currentUser ? { description: `You: ${currentUser.name ?? currentUser.userId}` } : {})}\n />\n <TextField\n label=\"Tags\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n placeholder=\"Comma-separated tags\"\n description=\"Separate multiple tags with commas\"\n />\n </div>\n\n {/* Correction note (required when classification changes) */}\n {classificationChanged && (\n <TextareaField\n label=\"Correction note\"\n value={correctionNote}\n onChange={(e) => setCorrectionNote(e.target.value)}\n required\n {...(!correctionNote ? { error: 'Required when changing type, scope, or size' } : {})}\n placeholder=\"Explain why this classification is being changed...\"\n />\n )}\n\n {/* Existing tags display */}\n {item.tags && item.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n )}\n </div>\n )}\n </UpdatePage>\n </div>\n );\n },\n);\nFeedbackUpdatePage.displayName = 'FeedbackUpdatePage';\n\nexport { FeedbackUpdatePage };\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/react/admin/FeedbackListPage.tsx","../../../src/react/admin/FeedbackDetailPage.tsx","../../../src/react/admin/hooks/useStatusTransition.ts","../../../src/react/admin/FeedbackUpdatePage.tsx","../../../src/react/admin/ActivityTimeline.tsx","../../../src/react/admin/AnnotationComposer.tsx","../../../src/react/admin/LifecycleControls.tsx","../../../src/react/admin/hooks/can.ts"],"sourcesContent":["/**\n * FeedbackListPage.tsx\n * Location: src/react/admin/FeedbackListPage.tsx\n *\n * Admin list page for browsing and filtering feedback items.\n * Thin wrapper around ListPage from anton-bakker-tailwind-components\n * for visual consistency with sagrosoft20.\n */\nimport * as React from 'react';\nimport { ListPage, Badge } from 'anton-bakker-tailwind-components';\nimport type { SimpleColumn, FilterBarProps } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType } from '../../core/types.js';\nimport type { FeedbackListPageProps } from './types.js';\n\nconst STATUS_VARIANT: Record<FeedbackStatus, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n new: 'warning',\n triaged: 'default',\n assigned: 'default',\n in_progress: 'default',\n fixed: 'success',\n pending_retest: 'warning',\n verified: 'success',\n closed: 'success',\n deferred: 'secondary',\n rejected: 'destructive',\n duplicate: 'secondary',\n reopened: 'warning',\n};\n\nconst TYPE_VARIANT: Record<FeedbackType, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n bug: 'destructive',\n feature: 'default',\n ux: 'warning',\n other: 'secondary',\n};\n\nfunction StatusBadge({ status }: { status: FeedbackStatus }) {\n return <Badge variant={STATUS_VARIANT[status] ?? 'secondary'}>{status}</Badge>;\n}\n\nfunction TypeBadge({ type }: { type: FeedbackType }) {\n return <Badge variant={TYPE_VARIANT[type] ?? 'secondary'}>{type}</Badge>;\n}\n\nconst FeedbackListPage = React.forwardRef<HTMLDivElement, FeedbackListPageProps>(\n ({ items, loading = false, onNavigate, className, emptyStateMessage, currentUser: _currentUser }, ref) => {\n const [search, setSearch] = React.useState('');\n const [statusFilter, setStatusFilter] = React.useState('');\n const [typeFilter, setTypeFilter] = React.useState('');\n\n const filteredItems = React.useMemo(() => {\n let result = items;\n if (statusFilter) result = result.filter((i) => i.status === statusFilter);\n if (typeFilter) result = result.filter((i) => i.type === typeFilter);\n if (search) {\n const q = search.toLowerCase();\n result = result.filter((i) => i.content?.toLowerCase().includes(q) || i.id.toLowerCase().includes(q));\n }\n return result;\n }, [items, statusFilter, typeFilter, search]);\n\n const columns: SimpleColumn<StoredFeedbackItem>[] = React.useMemo(() => [\n { header: 'Content', accessor: (row) => row.content?.slice(0, 60) || row.id, sortable: true },\n { header: 'Type', accessor: (row) => <TypeBadge type={row.type} />, sortable: true },\n { header: 'Status', accessor: (row) => <StatusBadge status={row.status} />, sortable: true },\n { header: 'Priority', accessor: 'priority', sortable: true },\n { header: 'Reporters', accessor: 'reporterCount', sortable: true },\n {\n header: 'Date',\n accessor: (row) => row.submittedAt ? new Date(row.submittedAt).toLocaleDateString() : '—',\n sortable: true,\n },\n ], []);\n\n const filterBar: FilterBarProps | undefined = (statusFilter || typeFilter) ? {\n filters: [\n ...(statusFilter ? [{ field: 'status', label: `Status: ${statusFilter}`, value: statusFilter }] : []),\n ...(typeFilter ? [{ field: 'type', label: `Type: ${typeFilter}`, value: typeFilter }] : []),\n ],\n onFilterRemove: (field: string) => {\n if (field === 'status') setStatusFilter('');\n if (field === 'type') setTypeFilter('');\n },\n onFiltersClear: () => { setStatusFilter(''); setTypeFilter(''); },\n } : undefined;\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <select\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by status\"\n >\n <option value=\"\">All statuses</option>\n <option value=\"open\">Open</option>\n <option value=\"approved\">Approved</option>\n <option value=\"deferred\">Deferred</option>\n <option value=\"resolved\">Resolved</option>\n <option value=\"wontfix\">Won't fix</option>\n </select>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by type\"\n >\n <option value=\"\">All types</option>\n <option value=\"bug\">Bug</option>\n <option value=\"feature\">Feature</option>\n <option value=\"ux\">UX</option>\n <option value=\"other\">Other</option>\n </select>\n </div>\n );\n\n const listProps = {\n title: 'Feedback' as const,\n columns,\n items: filteredItems,\n loading,\n onRowClick: (row: StoredFeedbackItem) => onNavigate(`/feedback/${row.id}`),\n search,\n onSearchChange: setSearch,\n searchPlaceholder: 'Search feedback...',\n headerActions,\n testId: 'feedback-list-page',\n ...(filterBar ? { filterBar } : {}),\n ...(emptyStateMessage ? { emptyState: emptyStateMessage } : {}),\n ...(className ? { className } : {}),\n };\n\n return <ListPage<StoredFeedbackItem> ref={ref} {...listProps} />;\n },\n);\nFeedbackListPage.displayName = 'FeedbackListPage';\n\nexport { FeedbackListPage, StatusBadge, TypeBadge };\n","/**\n * FeedbackDetailPage.tsx\n * Location: src/react/admin/FeedbackDetailPage.tsx\n *\n * Admin detail page for viewing a single feedback item.\n * Renders structured content (bug reports, feature requests) with all fields,\n * screenshot, console errors, and dialogue history.\n */\nimport * as React from 'react';\nimport { DetailPage, DetailSection, DetailRow, Badge, Button } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem } from '../../core/types.js';\nimport type { FeedbackDetailPageProps } from './types.js';\nimport { StatusBadge, TypeBadge } from './FeedbackListPage.js';\n\n/**\n * Parse structured content (produced by serializeManualForm or AI dialogue)\n * into labeled key-value pairs.\n * Format: \"Label: value\" lines, with optional [Bug Report] or [Feature Request] header.\n */\nfunction parseStructuredContent(content: string): { header: string | undefined; fields: { label: string; value: string }[]; raw: string } {\n const lines = content.split('\\n');\n let header: string | undefined;\n const fields: { label: string; value: string }[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check for header line like [Bug Report] or [Feature Request]\n const headerMatch = trimmed.match(/^\\[(.+)\\]$/);\n if (headerMatch && headerMatch[1]) {\n header = headerMatch[1];\n continue;\n }\n\n // Check for \"Label: value\" pattern\n const fieldMatch = trimmed.match(/^([A-Z][A-Za-z\\s]+):\\s*(.+)$/);\n if (fieldMatch && fieldMatch[1] && fieldMatch[2]) {\n fields.push({ label: fieldMatch[1], value: fieldMatch[2] });\n }\n }\n\n return { header, fields, raw: content };\n}\n\nconst FeedbackDetailPage = React.forwardRef<HTMLDivElement, FeedbackDetailPageProps>(\n ({ feedbackId, fetchItem, getScreenshotUrl, onNavigate, currentUser: _currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [error, setError] = React.useState<string | null>(null);\n const [screenshotUrl, setScreenshotUrl] = React.useState<string | null>(null);\n\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n setError(null);\n fetchItem(feedbackId)\n .then((data) => { if (!cancelled) setItem(data); })\n .catch((err) => { if (!cancelled) setError(err?.message ?? 'Failed to load item'); })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n // Load screenshot URL when item has one\n React.useEffect(() => {\n if (item?.hasScreenshot && item.screenshotS3Key && getScreenshotUrl) {\n getScreenshotUrl(item.screenshotS3Key)\n .then(setScreenshotUrl)\n .catch(() => setScreenshotUrl(null));\n }\n }, [item?.hasScreenshot, item?.screenshotS3Key, getScreenshotUrl]);\n\n if (error) {\n return (\n <div ref={ref} className={className}>\n <DetailPage title=\"Feedback\" headerActions={<Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>}>\n <div className=\"p-6 text-destructive\">{error}</div>\n </DetailPage>\n </div>\n );\n }\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>\n <Button onClick={() => onNavigate(`/feedback/${feedbackId}/edit`)}>Edit</Button>\n </div>\n );\n\n // Parse structured content\n const parsed = item ? parseStructuredContent(item.content) : null;\n const hasStructuredFields = parsed && parsed.fields.length > 0;\n\n return (\n <div ref={ref} className={className}>\n <DetailPage\n title={item?.content?.split('\\n').find(l => l.match(/^Summary:/))?.replace('Summary: ', '') || parsed?.header || `Feedback ${feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n loading={loading}\n testId=\"feedback-detail-page\"\n >\n {item && (\n <>\n <DetailSection title=\"Overview\">\n <DetailRow label=\"Status\"><StatusBadge status={item.status} /></DetailRow>\n <DetailRow label=\"Type\"><TypeBadge type={item.type} /></DetailRow>\n <DetailRow label=\"Priority\">{item.priority}</DetailRow>\n <DetailRow label=\"Environment\">{item.environment || '—'}</DetailRow>\n <DetailRow label=\"Submitted\">{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : '—'}</DetailRow>\n <DetailRow label=\"Reporters\">{item.reporterCount ?? 1}</DetailRow>\n {item.assignedTo && <DetailRow label=\"Assigned to\">{item.assignedTo}</DetailRow>}\n {item.scope && <DetailRow label=\"Scope\"><Badge variant=\"outline\">{item.scope}</Badge></DetailRow>}\n {item.size && <DetailRow label=\"Size\"><Badge variant=\"outline\">{item.size}</Badge></DetailRow>}\n </DetailSection>\n\n <DetailSection title=\"Content\">\n {hasStructuredFields ? (\n // Render each structured field as a separate row\n parsed.fields.map((field, i) => (\n <DetailRow key={i} label={field.label}>\n <p className=\"whitespace-pre-wrap\">{field.value}</p>\n </DetailRow>\n ))\n ) : (\n // Fallback: render raw content\n <DetailRow label=\"Description\">\n <p className=\"whitespace-pre-wrap\">{item.content}</p>\n </DetailRow>\n )}\n </DetailSection>\n\n {/* Screenshot */}\n {item.hasScreenshot && (\n <DetailSection title=\"Screenshot\">\n <DetailRow label=\"Capture\">\n {screenshotUrl ? (\n <a href={screenshotUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <img\n src={screenshotUrl}\n alt=\"Feedback screenshot\"\n className=\"max-w-full max-h-[400px] rounded border cursor-pointer hover:opacity-90 transition-opacity\"\n />\n </a>\n ) : (\n <span className=\"text-muted-foreground text-sm\">\n {getScreenshotUrl ? 'Loading screenshot…' : 'Screenshot available (no viewer configured)'}\n </span>\n )}\n </DetailRow>\n </DetailSection>\n )}\n\n {/* Console errors */}\n {item.context?.consoleErrors && item.context.consoleErrors.length > 0 && (\n <DetailSection title=\"Console Errors\" collapsible defaultOpen>\n <DetailRow label={`${item.context.consoleErrors.length} error(s)`}>\n <ul className=\"list-none space-y-1\">\n {item.context.consoleErrors.map((err, i) => (\n <li key={i} className=\"font-mono text-xs text-destructive bg-destructive/5 p-2 rounded\">\n {err}\n </li>\n ))}\n </ul>\n </DetailRow>\n </DetailSection>\n )}\n\n {/* Dialogue history */}\n {item.dialogueHistory && item.dialogueHistory.length > 0 && (\n <DetailSection title=\"Dialogue History\" collapsible defaultOpen={false}>\n {item.dialogueHistory.map((entry, i) => (\n <DetailRow key={i} label={entry.role === 'user' ? 'User' : 'Assistant'}>\n <p className=\"whitespace-pre-wrap text-sm\">{entry.content}</p>\n {entry.timestamp && (\n <span className=\"text-xs text-muted-foreground\">\n {new Date(entry.timestamp).toLocaleString()}\n </span>\n )}\n </DetailRow>\n ))}\n </DetailSection>\n )}\n\n {/* Browser context */}\n {item.context && (\n <DetailSection title=\"Browser Context\" collapsible defaultOpen={false}>\n {item.context.url && <DetailRow label=\"URL\">{item.context.url}</DetailRow>}\n {item.context.pageTitle && <DetailRow label=\"Page\">{item.context.pageTitle}</DetailRow>}\n {item.context.userAgent && <DetailRow label=\"User Agent\">{item.context.userAgent}</DetailRow>}\n {item.context.viewport && <DetailRow label=\"Viewport\">{`${item.context.viewport.width}×${item.context.viewport.height}`}</DetailRow>}\n {item.context.locale && <DetailRow label=\"Locale\">{item.context.locale}</DetailRow>}\n {item.context.timezone && <DetailRow label=\"Timezone\">{item.context.timezone}</DetailRow>}\n {item.context.performanceMs != null && <DetailRow label=\"Page Load\">{`${item.context.performanceMs}ms`}</DetailRow>}\n </DetailSection>\n )}\n\n {/* Tags */}\n {item.tags && item.tags.length > 0 && (\n <DetailSection title=\"Tags\">\n <DetailRow label=\"Tags\">\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n </DetailRow>\n </DetailSection>\n )}\n </>\n )}\n </DetailPage>\n </div>\n );\n },\n);\nFeedbackDetailPage.displayName = 'FeedbackDetailPage';\n\nexport { FeedbackDetailPage };\n","/**\n * useStatusTransition — pure hook + transition table for the 12-state QA lifecycle.\n *\n * Source of truth for legal status transitions. Exposes:\n * - QA_LIFECYCLE_TRANSITIONS: the exhaustive readonly transition map (consumers/tests\n * can enumerate legal targets without calling the hook).\n * - canTransitionTo(from, to): pure predicate derived from the map.\n * - useStatusTransition(currentStatus): returns the allowed target statuses.\n *\n * No self-loops are encoded. No side effects — same inputs always produce same outputs.\n * Contract: design/feedback-annotations/02-lifecycle-and-guard/DESIGN.md §WP-6.\n */\nimport type { FeedbackStatus } from '../../../core/types.js';\n\n/**\n * Exhaustive legal transitions for the 12-state QA lifecycle. Readonly so consumers and\n * tests can enumerate legal pairs. No self-loops.\n */\nexport const QA_LIFECYCLE_TRANSITIONS: Readonly<Record<FeedbackStatus, readonly FeedbackStatus[]>> = {\n new: ['triaged'],\n triaged: ['assigned', 'deferred', 'rejected', 'duplicate'],\n assigned: ['in_progress', 'deferred'],\n in_progress: ['fixed', 'deferred'],\n fixed: ['pending_retest'],\n pending_retest: ['verified', 'reopened'],\n verified: ['closed', 'reopened'],\n closed: ['reopened'],\n rejected: ['reopened'],\n duplicate: ['reopened'],\n reopened: ['triaged', 'assigned', 'in_progress'],\n deferred: ['triaged'],\n};\n\n/**\n * Pure predicate: is a transition from `from` to `to` legal in the QA lifecycle?\n * Self-transitions are never legal.\n *\n * @param from - current status\n * @param to - proposed target status\n * @returns true if the transition is encoded in QA_LIFECYCLE_TRANSITIONS\n */\nexport function canTransitionTo(from: FeedbackStatus, to: FeedbackStatus): boolean {\n if (from === to) return false;\n return (QA_LIFECYCLE_TRANSITIONS[from] ?? []).includes(to);\n}\n\nexport interface UseStatusTransitionResult {\n /** Statuses reachable from the current status (current status excluded) */\n allowedTargets: FeedbackStatus[];\n}\n\n/**\n * Returns the allowed target statuses from the given current status.\n *\n * @param currentStatus - The status the item currently has\n * @returns Object with `allowedTargets` — statuses the item can transition to\n *\n * @example\n * const { allowedTargets } = useStatusTransition('new');\n * // allowedTargets → ['triaged']\n */\nexport function useStatusTransition(currentStatus: FeedbackStatus): UseStatusTransitionResult {\n const allowedTargets = [...(QA_LIFECYCLE_TRANSITIONS[currentStatus] ?? [])];\n return { allowedTargets };\n}\n","/**\n * FeedbackUpdatePage.tsx\n * Location: src/react/admin/FeedbackUpdatePage.tsx\n *\n * @deprecated This component is deprecated and will be removed in a future version.\n * Use FeedbackAnnotateClient.appendActivity() and FeedbackAnnotateClient.transitionStatus()\n * from ingest-transport.ts instead. The whole-array corrections[] PATCH has been removed\n * to prevent data loss (sagrosoft20-class silent drop). This shell is kept for backward\n * compatibility during stage-3 consumer migration.\n *\n * Admin update page for editing mutable fields of a feedback item.\n * Uses UpdatePage + EnumField + TextField + TextareaField from anton-bakker-tailwind-components.\n */\nimport * as React from 'react';\nimport { UpdatePage, EnumField, TextField, TextareaField, Button, SaveFooter, Badge } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType } from '../../core/types.js';\nimport type { FeedbackUpdatePageProps } from './types.js';\nimport { useStatusTransition } from './hooks/useStatusTransition.js';\n\nconst STATUS_OPTIONS = [\n { value: 'open', label: 'Open' },\n { value: 'approved', label: 'Approved' },\n { value: 'deferred', label: 'Deferred' },\n { value: 'resolved', label: 'Resolved' },\n { value: 'wontfix', label: \"Won't Fix\" },\n];\n\nconst TYPE_OPTIONS = [\n { value: 'bug', label: 'Bug' },\n { value: 'feature', label: 'Feature' },\n { value: 'ux', label: 'UX Issue' },\n { value: 'other', label: 'Other' },\n];\n\nconst SCOPE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'component', label: 'Component' },\n { value: 'page', label: 'Page' },\n { value: 'workflow', label: 'Workflow' },\n];\n\nconst SIZE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'trivial', label: 'Trivial' },\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n];\n\nconst FeedbackUpdatePage = React.forwardRef<HTMLDivElement, FeedbackUpdatePageProps>(\n ({ feedbackId, fetchItem, updateItem, onNavigate, currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [saving, setSaving] = React.useState(false);\n\n // Form state\n const [status, setStatus] = React.useState<FeedbackStatus>('new');\n const [type, setType] = React.useState<FeedbackType>('bug');\n const [scope, setScope] = React.useState<string>('');\n const [size, setSize] = React.useState<string>('');\n const [assignedTo, setAssignedTo] = React.useState('');\n const [tags, setTags] = React.useState('');\n const [correctionNote, setCorrectionNote] = React.useState('');\n\n // Load item\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n fetchItem(feedbackId)\n .then((data) => {\n if (cancelled) return;\n setItem(data);\n setStatus(data.status);\n setType(data.type);\n setScope(data.scope ?? '');\n setSize(data.size ?? '');\n setAssignedTo(data.assignedTo ?? '');\n setTags(data.tags?.join(', ') ?? '');\n })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n const { allowedTargets } = useStatusTransition(status);\n const statusOptions = STATUS_OPTIONS.filter(\n (opt) => opt.value === status || allowedTargets.includes(opt.value as FeedbackStatus),\n );\n\n // Dirty detection\n const classificationChanged = item && (type !== item.type || scope !== (item.scope ?? '') || size !== (item.size ?? ''));\n const isDirty = item && (\n status !== item.status ||\n type !== item.type ||\n scope !== (item.scope ?? '') ||\n size !== (item.size ?? '') ||\n assignedTo !== (item.assignedTo ?? '') ||\n tags !== (item.tags?.join(', ') ?? '')\n );\n\n const handleSave = async () => {\n if (!item) return;\n if (!updateItem) {\n // updateItem is now optional for backward compatibility.\n // This component is deprecated; consumers should migrate to FeedbackAnnotateClient.\n console.warn('FeedbackUpdatePage: updateItem prop not provided. This component is deprecated.');\n return;\n }\n setSaving(true);\n try {\n const patch: Partial<StoredFeedbackItem> = { lastModifiedAt: new Date().toISOString() };\n if (status !== item.status) patch.status = status;\n if (type !== item.type) patch.type = type;\n if (scope !== (item.scope ?? '')) (patch as Record<string, unknown>).scope = scope || undefined;\n if (size !== (item.size ?? '')) (patch as Record<string, unknown>).size = size || undefined;\n if (assignedTo !== (item.assignedTo ?? '')) (patch as Record<string, unknown>).assignedTo = assignedTo || undefined;\n const newTags = tags.split(',').map((t) => t.trim()).filter(Boolean);\n if (JSON.stringify(newTags) !== JSON.stringify(item.tags ?? [])) patch.tags = newTags;\n\n if (status === 'closed' && item.status !== 'closed') {\n patch.resolvedAt = new Date().toISOString();\n patch.resolvedBy = currentUser?.userId ?? 'unknown';\n }\n\n // The whole-array corrections[] PATCH has been removed to prevent data loss.\n // Stage-3 consumers should migrate to FeedbackAnnotateClient.appendActivity()\n // for recording classification changes as activity entries.\n\n await updateItem(feedbackId, patch);\n onNavigate(`/feedback/${feedbackId}`);\n } finally {\n setSaving(false);\n }\n };\n\n const headerActions = (\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>\n Cancel\n </Button>\n );\n\n return (\n <div ref={ref} className={className}>\n <UpdatePage\n title={`Edit: ${item?.content?.slice(0, 40) || feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n dataLoading={loading}\n actions={\n <SaveFooter dirty={!!isDirty} saving={saving}>\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>Cancel</Button>\n <Button\n onClick={handleSave}\n disabled={saving || !isDirty}\n >\n {saving ? 'Saving…' : 'Save changes'}\n </Button>\n </SaveFooter>\n }\n testId=\"feedback-update-page\"\n >\n {item && (\n <div className=\"space-y-6\">\n {/* Read-only context */}\n <div className=\"rounded-lg border border-border p-4 bg-muted/30\">\n <p className=\"text-sm text-muted-foreground mb-2\">Original submission</p>\n <p className=\"whitespace-pre-wrap text-sm\">{item.content}</p>\n <div className=\"flex gap-3 mt-3 text-xs text-muted-foreground\">\n <span>{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : ''}</span>\n <span>{item.environment}</span>\n <span>{item.reporterCount ?? 1} reporter(s)</span>\n </div>\n </div>\n\n {/* Editable fields */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <EnumField\n label=\"Status\"\n options={statusOptions}\n value={status}\n onChange={(v) => setStatus(v as FeedbackStatus)}\n />\n <EnumField\n label=\"Type\"\n options={TYPE_OPTIONS}\n value={type}\n onChange={(v) => setType(v as FeedbackType)}\n />\n <EnumField\n label=\"Scope\"\n options={SCOPE_OPTIONS}\n value={scope}\n onChange={setScope}\n />\n <EnumField\n label=\"Size\"\n options={SIZE_OPTIONS}\n value={size}\n onChange={setSize}\n />\n <TextField\n label=\"Assigned to\"\n value={assignedTo}\n onChange={(e) => setAssignedTo(e.target.value)}\n placeholder={currentUser?.name ?? currentUser?.userId ?? ''}\n {...(currentUser ? { description: `You: ${currentUser.name ?? currentUser.userId}` } : {})}\n />\n <TextField\n label=\"Tags\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n placeholder=\"Comma-separated tags\"\n description=\"Separate multiple tags with commas\"\n />\n </div>\n\n {/* Correction note (required when classification changes) */}\n {classificationChanged && (\n <TextareaField\n label=\"Correction note\"\n value={correctionNote}\n onChange={(e) => setCorrectionNote(e.target.value)}\n required\n {...(!correctionNote ? { error: 'Required when changing type, scope, or size' } : {})}\n placeholder=\"Explain why this classification is being changed...\"\n />\n )}\n\n {/* Existing tags display */}\n {item.tags && item.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n )}\n </div>\n )}\n </UpdatePage>\n </div>\n );\n },\n);\nFeedbackUpdatePage.displayName = 'FeedbackUpdatePage';\n\nexport { FeedbackUpdatePage };\n","import * as React from 'react';\nimport type { ActivityEntry } from '../../core/types.js';\n\ninterface ActivityTimelineProps {\n activity: ActivityEntry[];\n className?: string;\n}\n\nexport function ActivityTimeline({ activity, className }: ActivityTimelineProps): React.ReactElement {\n if (activity.length === 0) {\n return (\n <div data-testid=\"activity-timeline\" className={className}>\n <div data-testid=\"activity-empty-state\">No activity yet.</div>\n </div>\n );\n }\n\n return (\n <div data-testid=\"activity-timeline\" className={className}>\n {activity.map((entry) => (\n <div key={entry.id} data-testid=\"activity-entry\">\n <span data-testid=\"activity-kind-badge\">{entry.kind.replace(/_/g, ' ')}</span>\n <span data-testid=\"activity-actor\">{entry.actor.displayName}</span>\n <span data-testid=\"activity-timestamp\">{entry.occurredAt}</span>\n {entry.body !== undefined && (\n <span data-testid=\"activity-body\">{entry.body}</span>\n )}\n </div>\n ))}\n </div>\n );\n}\n","/**\n * AnnotationComposer.tsx\n * Location: src/react/admin/AnnotationComposer.tsx\n *\n * Capability-gated comment/note/question/answer form calling appendActivity.\n * Surfaces AnnotateResult.conflictReason inline.\n * Hidden entirely when no annotation capability is allowed.\n */\nimport * as React from 'react';\nimport { Button } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, ActivityEntry } from '../../core/types.js';\nimport type { FeedbackAnnotateClient } from '../../core/ingest-transport.js';\nimport type { CapabilityKey } from './hooks/can.js';\n\nexport interface AnnotationComposerProps {\n feedbackId: string;\n item: StoredFeedbackItem;\n currentUser: { userId: string; name?: string };\n can: (cap: CapabilityKey) => boolean;\n appendActivity: FeedbackAnnotateClient['appendActivity'];\n appSource: string;\n onAppended?: () => void;\n}\n\n/**\n * AnnotationComposer calls appendActivity with a correctly-constructed ActivityEntry\n * (id, actor, occurredAt, recordedAt, source, kind, body, schemaVersion) on submit.\n */\nexport const AnnotationComposer = React.forwardRef<HTMLDivElement, AnnotationComposerProps>(\n (props, ref) => {\n const { feedbackId, currentUser, can, appendActivity, appSource, onAppended } = props;\n\n const canComment = can('feedback:comment');\n const canAnnotate = can('feedback:annotate');\n\n const [body, setBody] = React.useState('');\n const [conflictReason, setConflictReason] = React.useState<string | undefined>(undefined);\n const [submitting, setSubmitting] = React.useState(false);\n\n // Hidden when no annotation capability is allowed\n if (!canComment && !canAnnotate) return null;\n\n const submit = async (kind: ActivityEntry['kind']): Promise<void> => {\n setSubmitting(true);\n setConflictReason(undefined);\n\n const entry: ActivityEntry = {\n id: crypto.randomUUID(),\n actor: { userId: currentUser.userId, displayName: currentUser.name ?? currentUser.userId },\n occurredAt: new Date().toISOString(),\n recordedAt: new Date().toISOString(),\n source: appSource,\n kind,\n body,\n schemaVersion: 1,\n };\n\n const result = await appendActivity(feedbackId, entry);\n\n if (result.success) {\n setBody('');\n setConflictReason(undefined);\n onAppended?.();\n } else {\n setConflictReason(result.conflictReason ?? result.error ?? 'Unknown error');\n }\n setSubmitting(false);\n };\n\n const isDisabled = body.trim().length === 0 || submitting;\n\n return (\n <div ref={ref} data-testid=\"annotation-composer\" className=\"space-y-3\">\n <textarea\n data-testid=\"annotation-textarea\"\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Add a comment, note, question, or answer...\"\n className=\"w-full min-h-[100px] p-3 border border-input rounded-md bg-background text-sm resize-y focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n <div className=\"flex items-center gap-2 flex-wrap\">\n {canComment && (\n <Button\n data-testid=\"annotation-comment-button\"\n disabled={isDisabled}\n onClick={() => { void submit('comment'); }}\n variant=\"default\"\n size=\"sm\"\n >\n Comment\n </Button>\n )}\n {canAnnotate && (\n <>\n <Button\n data-testid=\"annotation-note-button\"\n disabled={isDisabled}\n onClick={() => { void submit('note'); }}\n variant=\"outline\"\n size=\"sm\"\n >\n Note\n </Button>\n <Button\n data-testid=\"annotation-question-button\"\n disabled={isDisabled}\n onClick={() => { void submit('question'); }}\n variant=\"outline\"\n size=\"sm\"\n >\n Question\n </Button>\n <Button\n data-testid=\"annotation-answer-button\"\n disabled={isDisabled}\n onClick={() => { void submit('answer'); }}\n variant=\"outline\"\n size=\"sm\"\n >\n Answer\n </Button>\n </>\n )}\n </div>\n {conflictReason !== undefined && (\n <div data-testid=\"annotation-conflict-reason\" className=\"text-sm text-destructive\">\n {conflictReason}\n </div>\n )}\n </div>\n );\n }\n);\n\nAnnotationComposer.displayName = 'AnnotationComposer';\n","/**\n * LifecycleControls.tsx\n * Location: src/react/admin/LifecycleControls.tsx\n *\n * Legal+authorized transition buttons, assignment, duplicate merge via transitionStatus.\n * Surfaces AnnotateResult.conflictReason inline.\n * Renders only transition buttons for targets where can(requiredCapability) is true.\n */\nimport * as React from 'react';\nimport { Button } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus } from '../../core/types.js';\nimport type { FeedbackAnnotateClient, TransitionStatusOptions } from '../../core/ingest-transport.js';\nimport type { CapabilityKey } from './hooks/can.js';\nimport { useStatusTransition } from './hooks/useStatusTransition.js';\n\nexport interface LifecycleControlsProps {\n feedbackId: string;\n item: StoredFeedbackItem;\n currentUser: { userId: string; name?: string };\n can: (cap: CapabilityKey) => boolean;\n transitionStatus: FeedbackAnnotateClient['transitionStatus'];\n onTransitioned?: () => void;\n}\n\n/**\n * Maps target status to the minimum required capability.\n * For transitions that require a specific capability, this mapping is used to check authorization.\n */\nconst CAPABILITY_MAP: Partial<Record<FeedbackStatus, CapabilityKey>> = {\n triaged: 'feedback:triage',\n assigned: 'feedback:assign',\n in_progress: 'feedback:assign',\n fixed: 'feedback:resolve',\n pending_retest: 'feedback:resolve',\n verified: 'feedback:verify',\n closed: 'feedback:resolve',\n deferred: 'feedback:triage',\n rejected: 'feedback:reject',\n duplicate: 'feedback:reject',\n reopened: 'feedback:reopen',\n};\n\n/**\n * Format a FeedbackStatus enum value to a human-readable label.\n */\nfunction formatStatusLabel(status: FeedbackStatus): string {\n return status\n .split('_')\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n/**\n * LifecycleControls renders only transition buttons for targets where can(requiredCapability) is true.\n */\nexport const LifecycleControls = React.forwardRef<HTMLDivElement, LifecycleControlsProps>(\n (props, ref) => {\n const { feedbackId, item, can, transitionStatus, onTransitioned } = props;\n\n // Get legal transitions from the authoritative useStatusTransition hook\n const { allowedTargets } = useStatusTransition(item.status);\n\n // Filter to only authorized transitions\n const authorizedTargets = allowedTargets.filter((target) => {\n const cap = CAPABILITY_MAP[target];\n return cap !== undefined && can(cap);\n });\n\n const [duplicateInputVisible, setDuplicateInputVisible] = React.useState(false);\n const [duplicateOf, setDuplicateOf] = React.useState('');\n const [conflictReason, setConflictReason] = React.useState<string | undefined>(undefined);\n const [submitting, setSubmitting] = React.useState(false);\n\n // Hidden when no authorized transitions\n if (authorizedTargets.length === 0) return null;\n\n const handleTransition = async (target: FeedbackStatus): Promise<void> => {\n if (target === 'duplicate') {\n if (!duplicateInputVisible) {\n setDuplicateInputVisible(true);\n return;\n }\n if (duplicateOf.trim().length === 0) {\n setConflictReason('Duplicate target ID is required');\n return;\n }\n\n setSubmitting(true);\n setConflictReason(undefined);\n\n const opts: TransitionStatusOptions = {\n currentStatus: item.status,\n duplicateOf,\n duplicateReporters: item.reporters,\n };\n const result = await transitionStatus(feedbackId, 'duplicate', opts);\n if (result.success) {\n setDuplicateInputVisible(false);\n setDuplicateOf('');\n setConflictReason(undefined);\n onTransitioned?.();\n } else {\n setConflictReason(result.conflictReason ?? result.error ?? 'Unknown error');\n }\n setSubmitting(false);\n return;\n }\n\n setSubmitting(true);\n setConflictReason(undefined);\n\n const opts: TransitionStatusOptions = {\n currentStatus: item.status,\n };\n const result = await transitionStatus(feedbackId, target, opts);\n if (result.success) {\n setConflictReason(undefined);\n onTransitioned?.();\n } else {\n setConflictReason(result.conflictReason ?? result.error ?? 'Unknown error');\n }\n setSubmitting(false);\n };\n\n return (\n <div ref={ref} data-testid=\"lifecycle-controls\" className=\"space-y-3\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n {authorizedTargets.map((target) => (\n <Button\n key={target}\n data-testid={`lifecycle-${target}-button`}\n onClick={() => { void handleTransition(target); }}\n disabled={submitting}\n variant={target === 'rejected' || target === 'duplicate' ? 'destructive' : 'default'}\n size=\"sm\"\n >\n {formatStatusLabel(target)}\n </Button>\n ))}\n </div>\n {duplicateInputVisible && (\n <div data-testid=\"duplicate-input-container\" className=\"space-y-2\">\n <input\n data-testid=\"duplicate-of-input\"\n value={duplicateOf}\n onChange={(e) => setDuplicateOf(e.target.value)}\n placeholder=\"Canonical feedback ID (e.g., FB-2026-11-01-abc12345)\"\n className=\"w-full p-2 border border-input rounded-md bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n <div className=\"flex gap-2\">\n <Button\n onClick={() => { void handleTransition('duplicate'); }}\n disabled={submitting || duplicateOf.trim().length === 0}\n size=\"sm\"\n >\n Confirm Duplicate\n </Button>\n <Button\n onClick={() => {\n setDuplicateInputVisible(false);\n setDuplicateOf('');\n setConflictReason(undefined);\n }}\n disabled={submitting}\n variant=\"outline\"\n size=\"sm\"\n >\n Cancel\n </Button>\n </div>\n </div>\n )}\n {conflictReason !== undefined && (\n <div data-testid=\"lifecycle-conflict-reason\" className=\"text-sm text-destructive\">\n {conflictReason}\n </div>\n )}\n </div>\n );\n }\n);\n\nLifecycleControls.displayName = 'LifecycleControls';\n","/**\n * Capability-based authorization guard for feedback operations.\n * Pure Policy Decision Point (PDP) — accepts pre-resolved capabilities from the Policy Enforcement Point (PEP).\n *\n * NO AWS SDK OR REACT IMPORTS — pure functions only.\n */\n\nimport type { StoredFeedbackItem } from '../../../core/types.js';\n\n/**\n * Capability keys for feedback operations.\n * Each key grants permission for a specific action on feedback items.\n */\nexport type CapabilityKey =\n | 'feedback:comment'\n | 'feedback:annotate'\n | 'feedback:triage'\n | 'feedback:assign'\n | 'feedback:resolve'\n | 'feedback:verify'\n | 'feedback:reopen'\n | 'feedback:reject'\n | 'feedback:delete';\n\n/**\n * Record mapping each capability key to itself.\n * Facilitates autocomplete and exhaustiveness checking.\n */\nexport const CAPABILITIES: Record<CapabilityKey, CapabilityKey> = {\n 'feedback:comment': 'feedback:comment',\n 'feedback:annotate': 'feedback:annotate',\n 'feedback:triage': 'feedback:triage',\n 'feedback:assign': 'feedback:assign',\n 'feedback:resolve': 'feedback:resolve',\n 'feedback:verify': 'feedback:verify',\n 'feedback:reopen': 'feedback:reopen',\n 'feedback:reject': 'feedback:reject',\n 'feedback:delete': 'feedback:delete',\n};\n\n/**\n * User identity with resolved capabilities.\n * Consuming apps (PEP) translate their identity system to this format at the system boundary.\n */\nexport interface CapabilityUser {\n userId: string;\n capabilities: Set<CapabilityKey>;\n}\n\n/** Alias for CapabilityUser — both names refer to the same type. */\nexport type UserWithCapabilities = CapabilityUser;\n\n/**\n * Pure capability-based authorization guard.\n * Returns true if the user has the specified capability OR if ownership rules grant it.\n *\n * Ownership rules:\n * - Submitters (userId === item.submittedBy) can comment and annotate without explicit capability\n * - Reporters (userId in item.reporters[].userId) can comment and annotate without explicit capability\n * - All other capabilities require explicit presence in user.capabilities\n *\n * @param capability - The capability being checked\n * @param item - The feedback item\n * @param user - The user with resolved capabilities\n * @returns true if authorized, false otherwise\n *\n * @example\n * const user = { userId: 'alice', capabilities: new Set(['feedback:triage']) };\n * can('feedback:triage', item, user) // → true (explicit capability)\n * can('feedback:comment', item, user) // → false (no capability, not owner)\n *\n * const submitter = { userId: item.submittedBy, capabilities: new Set([]) };\n * can('feedback:comment', item, submitter) // → true (ownership rule)\n * can('feedback:triage', item, submitter) // → false (ownership doesn't grant admin capabilities)\n */\nexport function can(\n capability: CapabilityKey,\n item: StoredFeedbackItem,\n user: CapabilityUser\n): boolean {\n // Base check: explicit capability\n if (user.capabilities.has(capability)) {\n return true;\n }\n\n // Ownership rules: submitters and reporters can comment and annotate\n const isSubmitter = user.userId === item.submittedBy;\n const isReporter = item.reporters.some(r => r.userId === user.userId);\n\n if ((isSubmitter || isReporter) &&\n (capability === 'feedback:comment' || capability === 'feedback:annotate')) {\n return true;\n }\n\n // All other cases: no authorization\n return false;\n}\n"],"names":["can","opts","result"],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { StoredFeedbackItem } from '../../core/types.js';
|
|
2
|
+
import { FeedbackAnnotateClient } from '../../core/ingest-transport.js';
|
|
3
|
+
import { CapabilityKey } from './hooks/can.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Base props shared by all admin page components.
|
|
@@ -26,12 +28,23 @@ export interface FeedbackListPageProps extends AdminPageProps {
|
|
|
26
28
|
* Props for the FeedbackDetailPage component.
|
|
27
29
|
* Consumer provides fetchItem; component handles loading/display via
|
|
28
30
|
* anton-bakker-tailwind-components DetailPage template.
|
|
31
|
+
*
|
|
32
|
+
* Extended with optional IoC props for enriched admin UI (activity timeline, annotation composer, lifecycle controls).
|
|
33
|
+
* All new props are optional — when absent, component renders identically to the legacy behavior.
|
|
29
34
|
*/
|
|
30
35
|
export interface FeedbackDetailPageProps extends AdminPageProps {
|
|
31
36
|
feedbackId: string;
|
|
32
37
|
fetchItem: (id: string) => Promise<StoredFeedbackItem>;
|
|
33
38
|
/** Optional callback to get a displayable URL for the screenshot (e.g. presigned S3 URL) */
|
|
34
39
|
getScreenshotUrl?: (s3Key: string) => Promise<string>;
|
|
40
|
+
/** Optional annotate client for appending activity (enables AnnotationComposer) */
|
|
41
|
+
appendActivity?: FeedbackAnnotateClient['appendActivity'];
|
|
42
|
+
/** Optional annotate client for transitioning status (enables LifecycleControls) */
|
|
43
|
+
transitionStatus?: FeedbackAnnotateClient['transitionStatus'];
|
|
44
|
+
/** Optional capability resolver for authorization checks (cap, item) => boolean */
|
|
45
|
+
can?: (cap: CapabilityKey, item: StoredFeedbackItem) => boolean;
|
|
46
|
+
/** Optional app source identifier for activity entries (default: 'admin-ui') */
|
|
47
|
+
appSource?: string;
|
|
35
48
|
}
|
|
36
49
|
/**
|
|
37
50
|
* Props for the FeedbackUpdatePage component.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/react/admin/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/react/admin/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,mFAAmF;IACnF,cAAc,CAAC,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IAC1D,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IAC9D,mFAAmF;IACnF,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC;IAChE,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IngestTransportConfig } from '../../core/ingest-transport.js';
|
|
2
|
+
import { FeedbackSubmitTransport } from '../../core/transport.js';
|
|
1
3
|
import { FeedbackEnvironment, FeedbackType, FeedbackScope, FeedbackSize } from '../../core/types.js';
|
|
2
4
|
|
|
3
5
|
/** Minimal HTTP request shape (Lambda/Express compatible) */
|
|
@@ -39,21 +41,22 @@ export interface SubmitPayload {
|
|
|
39
41
|
locale?: string | undefined;
|
|
40
42
|
} | undefined;
|
|
41
43
|
}
|
|
42
|
-
export interface SubmitHandlerConfig {
|
|
44
|
+
export interface SubmitHandlerConfig extends Partial<IngestTransportConfig> {
|
|
43
45
|
/** Application name — used in OTel attributes and feedback ID */
|
|
44
46
|
appName: string;
|
|
45
47
|
/** Deployment environment */
|
|
46
48
|
environment?: FeedbackEnvironment;
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Pre-constructed submit transport (tests / custom wiring). Takes precedence over ingest config.
|
|
51
|
+
* When neither a transport nor ingest config is supplied, storage is skipped (Minimal level).
|
|
52
|
+
*/
|
|
53
|
+
transport?: FeedbackSubmitTransport;
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
56
|
* Creates an Express/Lambda-compatible request handler for the submit endpoint.
|
|
54
57
|
*
|
|
55
|
-
* Validates payload → stores
|
|
56
|
-
* Returns 503 with retryable:true when
|
|
58
|
+
* Validates payload → stores via the central ingest API (if configured) → returns 201 with ID.
|
|
59
|
+
* Returns 503 with retryable:true when the ingest write fails.
|
|
57
60
|
* No Bedrock dependency.
|
|
58
61
|
*/
|
|
59
62
|
export declare function createIntakeResolver(config: SubmitHandlerConfig): RequestHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submitResolver.d.ts","sourceRoot":"","sources":["../../../src/react/resolver/submitResolver.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"submitResolver.d.ts","sourceRoot":"","sources":["../../../src/react/resolver/submitResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAyB,KAAK,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACnG,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,YAAY,EAEb,MAAM,qBAAqB,CAAC;AAM7B,6DAA6D;AAC7D,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACxD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wCAAwC;AACxC,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAC;IAC5D,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/B,KACE,OAAO,CAAC,IAAI,CAAC,CAAC;AAMnB,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAClC,IAAI,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,SAAS,CAAC;IACzE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACrC,UAAU,CAAC,EAAE;QACX,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC/B,QAAQ,CAAC,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,SAAS,CAAC;QACzD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAC7B,GAAG,SAAS,CAAC;CACf;AAMD,MAAM,WAAW,mBAAoB,SAAQ,OAAO,CAAC,qBAAqB,CAAC;IACzE,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC;;;OAGG;IACH,SAAS,CAAC,EAAE,uBAAuB,CAAC;CACrC;AA0JD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,mBAAmB,GAAG,cAAc,CAiEhF"}
|
package/dist/react.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React__default, { createContext, lazy, useMemo, useReducer, useEffect, useCallback, Suspense, useContext, useState, useRef } from "react";
|
|
3
3
|
import { v as validateConfig, r as resolveConfig, d as detectLocale, t } from "./chunks/i18n-Cn2yFMbd.js";
|
|
4
|
-
import { c } from "./chunks/submitResolver-
|
|
4
|
+
import { c } from "./chunks/submitResolver-wfr4kbEM.js";
|
|
5
5
|
const FeedbackContext = createContext(null);
|
|
6
6
|
FeedbackContext.displayName = "FeedbackContext";
|
|
7
7
|
const INITIAL_PANEL_STATE = { status: "idle" };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* server/index.ts — Re-exports for the `./server` package entry point.
|
|
3
3
|
*
|
|
4
4
|
* Exports:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
5
|
+
* - createListHandler (HTTP GET /api/feedback/list — reads via the central ingest API)
|
|
6
|
+
* - createIntakeResolver (re-export from submitResolver.ts — writes via the central ingest API)
|
|
7
|
+
*
|
|
8
|
+
* No direct-DynamoDB service is exported — all storage goes through the authenticated ingest API.
|
|
8
9
|
*/
|
|
9
|
-
export { FeedbackService } from './FeedbackService.js';
|
|
10
|
-
export type { FeedbackServiceConfig, ListOptions } from './FeedbackService.js';
|
|
11
10
|
export { createListHandler } from './listHandler.js';
|
|
12
11
|
export type { ListHandlerConfig } from './listHandler.js';
|
|
13
12
|
export { createIntakeResolver } from '../react/resolver/submitResolver.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC"}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
+
import { FeedbackReader, IngestTransportConfig } from '../core/ingest-transport.js';
|
|
1
2
|
import { RequestHandler } from '../react/resolver/submitResolver.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Config for the list handler. Supply the ingest credential (apiUrl + tokenUrl + clientId +
|
|
6
|
+
* clientSecret, or a tokenProvider) so the handler can read via the central API. When no ingest
|
|
7
|
+
* config is given, the handler runs at "Minimal level" and returns `[]` (no storage wired).
|
|
8
|
+
*
|
|
9
|
+
* For tests, an already-constructed `reader` may be injected directly.
|
|
10
|
+
*/
|
|
11
|
+
export interface ListHandlerConfig extends Partial<IngestTransportConfig> {
|
|
12
|
+
/** Pre-constructed reader (tests / custom wiring). Takes precedence over ingest config. */
|
|
13
|
+
reader?: FeedbackReader;
|
|
8
14
|
}
|
|
9
15
|
/**
|
|
10
16
|
* Creates an Express/Lambda-compatible request handler for the list endpoint.
|
|
11
17
|
*
|
|
12
|
-
* Validates query params →
|
|
13
|
-
*
|
|
14
|
-
* Returns
|
|
18
|
+
* Validates query params → reads the app's partition via the ingest API → applies client-side
|
|
19
|
+
* status/limit filtering → returns the paginated result.
|
|
20
|
+
* Returns 400 for invalid status/limit values.
|
|
21
|
+
* Returns 200 with items array (empty [] when no ingest config is wired, or the partition is empty).
|
|
15
22
|
*/
|
|
16
23
|
export declare function createListHandler(config: ListHandlerConfig): RequestHandler;
|
|
17
24
|
//# sourceMappingURL=listHandler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listHandler.d.ts","sourceRoot":"","sources":["../../src/server/listHandler.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"listHandler.d.ts","sourceRoot":"","sources":["../../src/server/listHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAsB,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAClH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAM1E;;;;;;GAMG;AACH,MAAM,WAAW,iBAAkB,SAAQ,OAAO,CAAC,qBAAqB,CAAC;IACvE,2FAA2F;IAC3F,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AA6BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAwD3E"}
|