@reqord/web 0.1.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/LICENSE +661 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +7 -0
- package/package.json +59 -0
- package/postcss.config.mjs +7 -0
- package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
- package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
- package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
- package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
- package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
- package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
- package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
- package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
- package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
- package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
- package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
- package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
- package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
- package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
- package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
- package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
- package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
- package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
- package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
- package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
- package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
- package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
- package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
- package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
- package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
- package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
- package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
- package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
- package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
- package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
- package/src/__tests__/lib/dashboard-data.test.ts +334 -0
- package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
- package/src/__tests__/lib/gantt-data.test.ts +299 -0
- package/src/__tests__/lib/graph-data.test.ts +309 -0
- package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
- package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
- package/src/__tests__/lib/reqord-root.test.ts +31 -0
- package/src/__tests__/lib/specification-file.test.ts +63 -0
- package/src/__tests__/lib/tasks-data.test.ts +104 -0
- package/src/app/dashboard/loading.tsx +21 -0
- package/src/app/dashboard/page.tsx +50 -0
- package/src/app/error.tsx +22 -0
- package/src/app/feedback/loading.tsx +13 -0
- package/src/app/feedback/page.tsx +48 -0
- package/src/app/globals.css +2 -0
- package/src/app/graph/page.tsx +32 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/page.tsx +5 -0
- package/src/app/requirements/[id]/edit/page.tsx +40 -0
- package/src/app/requirements/[id]/loading.tsx +14 -0
- package/src/app/requirements/[id]/not-found.tsx +18 -0
- package/src/app/requirements/[id]/page.tsx +43 -0
- package/src/app/requirements/loading.tsx +13 -0
- package/src/app/requirements/new/page.tsx +14 -0
- package/src/app/requirements/page.tsx +35 -0
- package/src/app/specifications/[id]/loading.tsx +14 -0
- package/src/app/specifications/[id]/not-found.tsx +18 -0
- package/src/app/specifications/[id]/page.tsx +52 -0
- package/src/app/specifications/loading.tsx +13 -0
- package/src/app/specifications/page.tsx +42 -0
- package/src/components/dashboard/critical-path-display.tsx +76 -0
- package/src/components/dashboard/progress-bar.tsx +45 -0
- package/src/components/dashboard/progress-section.tsx +57 -0
- package/src/components/dashboard/project-health.tsx +35 -0
- package/src/components/dashboard/status-card.tsx +27 -0
- package/src/components/dashboard/status-cards.tsx +28 -0
- package/src/components/dashboard/warning-alert.tsx +33 -0
- package/src/components/dashboard/warning-alerts.tsx +24 -0
- package/src/components/feedback/feedback-badge.tsx +48 -0
- package/src/components/feedback/feedback-client-view.tsx +38 -0
- package/src/components/feedback/feedback-filters.tsx +86 -0
- package/src/components/feedback/feedback-linked-items.tsx +93 -0
- package/src/components/feedback/feedback-list.tsx +40 -0
- package/src/components/feedback/feedback-table.tsx +115 -0
- package/src/components/gantt/gantt-bar.tsx +65 -0
- package/src/components/gantt/gantt-chart.tsx +88 -0
- package/src/components/gantt/gantt-constants.ts +15 -0
- package/src/components/gantt/gantt-critical-path.tsx +38 -0
- package/src/components/gantt/gantt-group.tsx +25 -0
- package/src/components/gantt/gantt-header.tsx +47 -0
- package/src/components/gantt/gantt-legend.tsx +26 -0
- package/src/components/graph/dag-layout.ts +131 -0
- package/src/components/graph/dependency-graph.tsx +88 -0
- package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
- package/src/components/graph/drilldown-graph.tsx +45 -0
- package/src/components/graph/edge-styles.ts +16 -0
- package/src/components/graph/graph-loader.tsx +25 -0
- package/src/components/graph/graph-page-client.tsx +98 -0
- package/src/components/graph/issue-node.tsx +46 -0
- package/src/components/graph/multi-level-graph.tsx +91 -0
- package/src/components/graph/requirement-node.tsx +69 -0
- package/src/components/graph/specification-node.tsx +39 -0
- package/src/components/requirement/delete-button.tsx +46 -0
- package/src/components/requirement/dependency-editor.tsx +79 -0
- package/src/components/requirement/markdown-editor.tsx +47 -0
- package/src/components/requirement/markdown-renderer.tsx +12 -0
- package/src/components/requirement/requirement-detail.tsx +228 -0
- package/src/components/requirement/requirement-form.tsx +390 -0
- package/src/components/requirement/requirement-table.tsx +203 -0
- package/src/components/requirement/requirement-tabs.tsx +65 -0
- package/src/components/requirement/success-criteria-editor.tsx +53 -0
- package/src/components/specification/spec-detail.tsx +103 -0
- package/src/components/specification/spec-tabs.tsx +66 -0
- package/src/components/specification/specification-table.tsx +193 -0
- package/src/components/specification/tab-coverage.tsx +52 -0
- package/src/components/specification/tab-design.tsx +16 -0
- package/src/components/specification/tab-history.tsx +61 -0
- package/src/components/specification/tab-issues.tsx +111 -0
- package/src/components/specification/tab-research.tsx +16 -0
- package/src/components/ui/badge.tsx +64 -0
- package/src/components/ui/nav.tsx +49 -0
- package/src/components/ui/tabs.tsx +39 -0
- package/src/lib/actions.ts +222 -0
- package/src/lib/dashboard-data.ts +224 -0
- package/src/lib/data.ts +21 -0
- package/src/lib/drilldown-graph-data.ts +98 -0
- package/src/lib/feedback-data.ts +33 -0
- package/src/lib/feedback-repository.ts +6 -0
- package/src/lib/file-system.ts +167 -0
- package/src/lib/gantt-data.ts +168 -0
- package/src/lib/get-repository.ts +43 -0
- package/src/lib/graph-data.ts +161 -0
- package/src/lib/id-generator.ts +23 -0
- package/src/lib/local-feedback-repository.ts +36 -0
- package/src/lib/local-repository.ts +78 -0
- package/src/lib/local-specification-repository.ts +61 -0
- package/src/lib/repository.ts +11 -0
- package/src/lib/reqord-root.ts +33 -0
- package/src/lib/specification-data.ts +28 -0
- package/src/lib/specification-file.ts +12 -0
- package/src/lib/specification-repository.ts +8 -0
- package/src/lib/tasks-data.ts +32 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import type { CriticalPathItem } from "@/lib/dashboard-data";
|
|
5
|
+
|
|
6
|
+
type CriticalPathDisplayProps = {
|
|
7
|
+
items: CriticalPathItem[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const INITIAL_DISPLAY_COUNT = 10;
|
|
11
|
+
|
|
12
|
+
export function CriticalPathDisplay({ items }: CriticalPathDisplayProps) {
|
|
13
|
+
const openItems = items.filter((item) => item.status !== "closed");
|
|
14
|
+
const closedItems = items.filter((item) => item.status === "closed");
|
|
15
|
+
const [showClosed, setShowClosed] = useState(false);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
19
|
+
<h2 className="mb-4 text-lg font-semibold text-gray-900">
|
|
20
|
+
Critical Path
|
|
21
|
+
<span className="ml-2 text-sm font-normal text-gray-500">
|
|
22
|
+
{openItems.length} open / {closedItems.length} closed
|
|
23
|
+
</span>
|
|
24
|
+
</h2>
|
|
25
|
+
{/* Show only open items when they exist; closed items are accessible via the expand button below */}
|
|
26
|
+
<div className="space-y-3">
|
|
27
|
+
{(openItems.length > 0
|
|
28
|
+
? openItems
|
|
29
|
+
: (showClosed ? closedItems : closedItems.slice(0, INITIAL_DISPLAY_COUNT))
|
|
30
|
+
).map((item) => {
|
|
31
|
+
const isCompleted = item.status === "closed";
|
|
32
|
+
const isPending =
|
|
33
|
+
item.status === "open" || item.status === "in_progress";
|
|
34
|
+
|
|
35
|
+
let textClass = "";
|
|
36
|
+
if (isCompleted) {
|
|
37
|
+
textClass = "line-through";
|
|
38
|
+
} else if (isPending) {
|
|
39
|
+
textClass = "font-bold";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
key={item.issueNumber}
|
|
45
|
+
data-testid="critical-path-item"
|
|
46
|
+
className={`flex items-start gap-3 rounded-md border border-gray-200 p-3 ${textClass}`}
|
|
47
|
+
>
|
|
48
|
+
<div className="flex-1">
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<span className="text-sm font-medium text-gray-900">
|
|
51
|
+
#{item.issueNumber}
|
|
52
|
+
</span>
|
|
53
|
+
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
|
|
54
|
+
{item.priority}
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
<p className="mt-1 text-sm text-gray-700">{item.title}</p>
|
|
58
|
+
<p className="mt-1 text-xs text-gray-500">{item.status}</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</div>
|
|
64
|
+
{openItems.length === 0 && closedItems.length > INITIAL_DISPLAY_COUNT && (
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => setShowClosed(!showClosed)}
|
|
67
|
+
className="mt-3 text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
|
68
|
+
>
|
|
69
|
+
{showClosed
|
|
70
|
+
? "Show less"
|
|
71
|
+
: `Show all ${closedItems.length} closed items`}
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
type ProgressBarProps = {
|
|
6
|
+
label: string;
|
|
7
|
+
current: number;
|
|
8
|
+
total: number;
|
|
9
|
+
percentage: number;
|
|
10
|
+
color: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const COLOR_CLASSES: Record<string, string> = {
|
|
14
|
+
blue: "bg-blue-500",
|
|
15
|
+
purple: "bg-purple-500",
|
|
16
|
+
green: "bg-green-500",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function ProgressBar({
|
|
20
|
+
label,
|
|
21
|
+
current,
|
|
22
|
+
total,
|
|
23
|
+
percentage,
|
|
24
|
+
color,
|
|
25
|
+
}: ProgressBarProps) {
|
|
26
|
+
const bgClass = COLOR_CLASSES[color] ?? "bg-gray-500";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
<div className="flex items-center justify-between">
|
|
31
|
+
<span className="text-sm font-medium text-gray-700">{label}</span>
|
|
32
|
+
<span className="text-sm text-gray-600">
|
|
33
|
+
{current}/{total} ({percentage}%)
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="h-2 overflow-hidden rounded-full bg-gray-200">
|
|
37
|
+
<div
|
|
38
|
+
data-testid="progress-bar-fill"
|
|
39
|
+
className={`h-full ${bgClass} transition-all duration-300`}
|
|
40
|
+
style={{ width: `${percentage}%` }}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ProgressBar } from "./progress-bar";
|
|
3
|
+
import type { CategorySummary, IssueSummary } from "@/lib/dashboard-data";
|
|
4
|
+
|
|
5
|
+
type ProgressSectionProps = {
|
|
6
|
+
requirements: CategorySummary;
|
|
7
|
+
specifications: CategorySummary;
|
|
8
|
+
issues: IssueSummary;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function ProgressSection({
|
|
12
|
+
requirements,
|
|
13
|
+
specifications,
|
|
14
|
+
issues,
|
|
15
|
+
}: ProgressSectionProps) {
|
|
16
|
+
const reqApproved = Math.round(requirements.total * requirements.approvalRate);
|
|
17
|
+
const reqPercentage = Math.round(requirements.approvalRate * 100);
|
|
18
|
+
|
|
19
|
+
const specApproved = Math.round(
|
|
20
|
+
specifications.total * specifications.approvalRate
|
|
21
|
+
);
|
|
22
|
+
const specPercentage = Math.round(specifications.approvalRate * 100);
|
|
23
|
+
|
|
24
|
+
const issuesPercentage = Math.round(issues.completionRate * 100);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
28
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
29
|
+
<ProgressBar
|
|
30
|
+
label="Requirements"
|
|
31
|
+
current={reqApproved}
|
|
32
|
+
total={requirements.total}
|
|
33
|
+
percentage={reqPercentage}
|
|
34
|
+
color="blue"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
38
|
+
<ProgressBar
|
|
39
|
+
label="Specifications"
|
|
40
|
+
current={specApproved}
|
|
41
|
+
total={specifications.total}
|
|
42
|
+
percentage={specPercentage}
|
|
43
|
+
color="purple"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
47
|
+
<ProgressBar
|
|
48
|
+
label="Issues"
|
|
49
|
+
current={issues.completed}
|
|
50
|
+
total={issues.total}
|
|
51
|
+
percentage={issuesPercentage}
|
|
52
|
+
color="green"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
type ProjectHealthProps = {
|
|
6
|
+
score: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function ProjectHealth({ score }: ProjectHealthProps) {
|
|
10
|
+
const roundedScore = Math.round(score);
|
|
11
|
+
|
|
12
|
+
let colorClass = "text-red-500";
|
|
13
|
+
if (roundedScore >= 80) {
|
|
14
|
+
colorClass = "text-green-500";
|
|
15
|
+
} else if (roundedScore >= 50) {
|
|
16
|
+
colorClass = "text-yellow-500";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
21
|
+
<h2 className="mb-4 text-lg font-semibold text-gray-900">
|
|
22
|
+
Project Health
|
|
23
|
+
</h2>
|
|
24
|
+
<div className="flex items-baseline justify-center">
|
|
25
|
+
<span
|
|
26
|
+
data-testid="health-score"
|
|
27
|
+
className={`text-6xl font-bold ${colorClass}`}
|
|
28
|
+
>
|
|
29
|
+
{roundedScore}
|
|
30
|
+
</span>
|
|
31
|
+
<span className="ml-2 text-2xl text-gray-500">/ 100</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { StatusBreakdown } from "@/lib/dashboard-data";
|
|
3
|
+
|
|
4
|
+
type StatusCardProps = {
|
|
5
|
+
title: string;
|
|
6
|
+
total: number;
|
|
7
|
+
breakdown: StatusBreakdown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function StatusCard({ title, total, breakdown }: StatusCardProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
13
|
+
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
|
14
|
+
<p className="mt-2 text-3xl font-bold text-gray-900">{total}</p>
|
|
15
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
16
|
+
{Object.entries(breakdown).map(([status, count]) => (
|
|
17
|
+
<span
|
|
18
|
+
key={status}
|
|
19
|
+
className="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-800"
|
|
20
|
+
>
|
|
21
|
+
{status}: {count}
|
|
22
|
+
</span>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StatusCard } from "./status-card";
|
|
3
|
+
import type { CategorySummary } from "@/lib/dashboard-data";
|
|
4
|
+
|
|
5
|
+
type StatusCardsProps = {
|
|
6
|
+
requirements: CategorySummary;
|
|
7
|
+
specifications: CategorySummary;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function StatusCards({
|
|
11
|
+
requirements,
|
|
12
|
+
specifications,
|
|
13
|
+
}: StatusCardsProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
16
|
+
<StatusCard
|
|
17
|
+
title="Requirements"
|
|
18
|
+
total={requirements.total}
|
|
19
|
+
breakdown={requirements.breakdown}
|
|
20
|
+
/>
|
|
21
|
+
<StatusCard
|
|
22
|
+
title="Specifications"
|
|
23
|
+
total={specifications.total}
|
|
24
|
+
breakdown={specifications.breakdown}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Warning } from "@/lib/dashboard-data";
|
|
3
|
+
|
|
4
|
+
type WarningAlertProps = {
|
|
5
|
+
warning: Warning;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function WarningAlert({ warning }: WarningAlertProps) {
|
|
9
|
+
let borderColor = "border-blue-500";
|
|
10
|
+
let bgColor = "bg-blue-50";
|
|
11
|
+
|
|
12
|
+
if (warning.severity === "error") {
|
|
13
|
+
borderColor = "border-red-500";
|
|
14
|
+
bgColor = "bg-red-50";
|
|
15
|
+
} else if (warning.severity === "warning") {
|
|
16
|
+
borderColor = "border-yellow-500";
|
|
17
|
+
bgColor = "bg-yellow-50";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
data-testid="warning-alert"
|
|
23
|
+
className={`rounded-md border-l-4 p-4 ${borderColor} ${bgColor}`}
|
|
24
|
+
>
|
|
25
|
+
<div className="flex items-start">
|
|
26
|
+
<div className="flex-1">
|
|
27
|
+
<p className="text-sm font-medium text-gray-900">{warning.message}</p>
|
|
28
|
+
<p className="mt-1 text-xs text-gray-600">{warning.relatedId}</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { WarningAlert } from "./warning-alert";
|
|
3
|
+
import type { Warning } from "@/lib/dashboard-data";
|
|
4
|
+
|
|
5
|
+
type WarningAlertsProps = {
|
|
6
|
+
warnings: Warning[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function WarningAlerts({ warnings }: WarningAlertsProps) {
|
|
10
|
+
if (warnings.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="rounded-lg border border-gray-200 bg-white p-6">
|
|
16
|
+
<h2 className="mb-4 text-lg font-semibold text-gray-900">Warnings</h2>
|
|
17
|
+
<div className="space-y-3">
|
|
18
|
+
{warnings.map((warning, index) => (
|
|
19
|
+
<WarningAlert key={index} warning={warning} />
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const SEVERITY_STYLES: Record<string, string> = {
|
|
4
|
+
critical: "bg-red-500 text-white",
|
|
5
|
+
high: "bg-orange-500 text-white",
|
|
6
|
+
medium: "bg-yellow-500 text-white",
|
|
7
|
+
low: "bg-gray-400 text-white",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const TYPE_STYLES: Record<string, { bg: string; label: string }> = {
|
|
11
|
+
bug: { bg: "bg-red-100 text-red-800", label: "Bug" },
|
|
12
|
+
improvement: { bg: "bg-blue-100 text-blue-800", label: "Improvement" },
|
|
13
|
+
"requirement-gap": { bg: "bg-amber-100 text-amber-800", label: "Requirement Gap" },
|
|
14
|
+
"spec-mismatch": { bg: "bg-purple-100 text-purple-800", label: "Spec Mismatch" },
|
|
15
|
+
security: { bg: "bg-red-100 text-red-800", label: "Security" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function FeedbackBadge({
|
|
19
|
+
type,
|
|
20
|
+
severity,
|
|
21
|
+
}: {
|
|
22
|
+
type?: string;
|
|
23
|
+
severity?: string;
|
|
24
|
+
}) {
|
|
25
|
+
const typeStyle = type ? TYPE_STYLES[type] : undefined;
|
|
26
|
+
const severityStyle = severity ? SEVERITY_STYLES[severity] : undefined;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<span className="inline-flex items-center gap-1.5">
|
|
30
|
+
{typeStyle && (
|
|
31
|
+
<span
|
|
32
|
+
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${typeStyle.bg}`}
|
|
33
|
+
data-testid={`feedback-badge-${type}`}
|
|
34
|
+
>
|
|
35
|
+
{typeStyle.label}
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
{severityStyle && (
|
|
39
|
+
<span
|
|
40
|
+
className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium ${severityStyle}`}
|
|
41
|
+
data-testid="feedback-severity"
|
|
42
|
+
>
|
|
43
|
+
{severity}
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useMemo } from "react";
|
|
4
|
+
import type { FeedbackEntry } from "@reqord/shared";
|
|
5
|
+
import { FeedbackFilters, type FeedbackFilterState } from "./feedback-filters";
|
|
6
|
+
import { FeedbackTable } from "./feedback-table";
|
|
7
|
+
|
|
8
|
+
export function FeedbackClientView({
|
|
9
|
+
feedbacks,
|
|
10
|
+
requirementTitles,
|
|
11
|
+
specificationTitles,
|
|
12
|
+
}: {
|
|
13
|
+
feedbacks: FeedbackEntry[];
|
|
14
|
+
requirementTitles: Record<string, string>;
|
|
15
|
+
specificationTitles: Record<string, string>;
|
|
16
|
+
}) {
|
|
17
|
+
const [filters, setFilters] = useState<FeedbackFilterState>({});
|
|
18
|
+
|
|
19
|
+
const filteredFeedbacks = useMemo(() => {
|
|
20
|
+
return feedbacks.filter((fb) => {
|
|
21
|
+
if (filters.type && fb.type !== filters.type) return false;
|
|
22
|
+
if (filters.severity && fb.severity !== filters.severity) return false;
|
|
23
|
+
if (filters.status && fb.status !== filters.status) return false;
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
}, [feedbacks, filters]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-4" data-testid="feedback-client-view">
|
|
30
|
+
<FeedbackFilters activeFilters={filters} onFilterChange={setFilters} />
|
|
31
|
+
<FeedbackTable
|
|
32
|
+
feedbacks={filteredFeedbacks}
|
|
33
|
+
requirementTitles={requirementTitles}
|
|
34
|
+
specificationTitles={specificationTitles}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
export interface FeedbackFilterState {
|
|
6
|
+
type?: string;
|
|
7
|
+
severity?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TYPE_OPTIONS = ["all", "bug", "improvement", "requirement-gap", "spec-mismatch", "security"];
|
|
12
|
+
const SEVERITY_OPTIONS = ["all", "critical", "high", "medium", "low"];
|
|
13
|
+
const STATUS_OPTIONS = ["all", "open", "closed"];
|
|
14
|
+
|
|
15
|
+
function SegmentedButtons({
|
|
16
|
+
label,
|
|
17
|
+
options,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
}: {
|
|
21
|
+
label: string;
|
|
22
|
+
options: string[];
|
|
23
|
+
value: string;
|
|
24
|
+
onChange: (v: string) => void;
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex items-center gap-2">
|
|
28
|
+
<span className="text-xs font-medium text-gray-500 w-16">{label}</span>
|
|
29
|
+
<div className="flex gap-1">
|
|
30
|
+
{options.map((opt) => (
|
|
31
|
+
<button
|
|
32
|
+
key={opt}
|
|
33
|
+
onClick={() => onChange(opt)}
|
|
34
|
+
aria-label={`${label} ${opt}`}
|
|
35
|
+
aria-pressed={value === opt}
|
|
36
|
+
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
37
|
+
value === opt
|
|
38
|
+
? "bg-blue-600 text-white"
|
|
39
|
+
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
|
40
|
+
}`}
|
|
41
|
+
data-testid={`filter-${label.toLowerCase()}-${opt}`}
|
|
42
|
+
>
|
|
43
|
+
{opt}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function FeedbackFilters({
|
|
52
|
+
activeFilters,
|
|
53
|
+
onFilterChange,
|
|
54
|
+
}: {
|
|
55
|
+
activeFilters: FeedbackFilterState;
|
|
56
|
+
onFilterChange: (filters: FeedbackFilterState) => void;
|
|
57
|
+
}) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex flex-wrap gap-4 rounded-lg border border-gray-200 bg-white p-4" data-testid="feedback-filters">
|
|
60
|
+
<SegmentedButtons
|
|
61
|
+
label="Type"
|
|
62
|
+
options={TYPE_OPTIONS}
|
|
63
|
+
value={activeFilters.type ?? "all"}
|
|
64
|
+
onChange={(v) =>
|
|
65
|
+
onFilterChange({ ...activeFilters, type: v === "all" ? undefined : v })
|
|
66
|
+
}
|
|
67
|
+
/>
|
|
68
|
+
<SegmentedButtons
|
|
69
|
+
label="Severity"
|
|
70
|
+
options={SEVERITY_OPTIONS}
|
|
71
|
+
value={activeFilters.severity ?? "all"}
|
|
72
|
+
onChange={(v) =>
|
|
73
|
+
onFilterChange({ ...activeFilters, severity: v === "all" ? undefined : v })
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
<SegmentedButtons
|
|
77
|
+
label="Status"
|
|
78
|
+
options={STATUS_OPTIONS}
|
|
79
|
+
value={activeFilters.status ?? "all"}
|
|
80
|
+
onChange={(v) =>
|
|
81
|
+
onFilterChange({ ...activeFilters, status: v === "all" ? undefined : v })
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import type { FeedbackLinkedTo } from "@reqord/shared";
|
|
4
|
+
|
|
5
|
+
export function FeedbackLinkedItems({
|
|
6
|
+
linkedTo,
|
|
7
|
+
requirementTitles,
|
|
8
|
+
specificationTitles,
|
|
9
|
+
}: {
|
|
10
|
+
linkedTo: FeedbackLinkedTo;
|
|
11
|
+
requirementTitles: Record<string, string>;
|
|
12
|
+
specificationTitles: Record<string, string>;
|
|
13
|
+
}) {
|
|
14
|
+
const hasAny =
|
|
15
|
+
linkedTo.requirements.length > 0 ||
|
|
16
|
+
linkedTo.createdRequirements.length > 0 ||
|
|
17
|
+
linkedTo.specifications.length > 0 ||
|
|
18
|
+
linkedTo.createdSpecifications.length > 0 ||
|
|
19
|
+
(linkedTo.resolved &&
|
|
20
|
+
(linkedTo.resolved.requirements.length > 0 ||
|
|
21
|
+
linkedTo.resolved.specifications.length > 0));
|
|
22
|
+
|
|
23
|
+
if (!hasAny) return null;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex flex-wrap gap-1" data-testid="feedback-linked-items">
|
|
27
|
+
{linkedTo.requirements.map((id) => (
|
|
28
|
+
<Link
|
|
29
|
+
key={`req-${id}`}
|
|
30
|
+
href={`/requirements/${id}`}
|
|
31
|
+
className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs text-blue-700 hover:bg-blue-100"
|
|
32
|
+
data-testid="linked-requirement"
|
|
33
|
+
>
|
|
34
|
+
{requirementTitles[id] ?? id}
|
|
35
|
+
</Link>
|
|
36
|
+
))}
|
|
37
|
+
{linkedTo.createdRequirements.map((id) => (
|
|
38
|
+
<Link
|
|
39
|
+
key={`created-req-${id}`}
|
|
40
|
+
href={`/requirements/${id}`}
|
|
41
|
+
className="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-0.5 text-xs text-green-700 hover:bg-green-100"
|
|
42
|
+
data-testid="created-requirement"
|
|
43
|
+
>
|
|
44
|
+
{requirementTitles[id] ?? id}
|
|
45
|
+
<span className="rounded bg-green-200 px-1 text-green-800">created</span>
|
|
46
|
+
</Link>
|
|
47
|
+
))}
|
|
48
|
+
{linkedTo.specifications.map((id) => (
|
|
49
|
+
<Link
|
|
50
|
+
key={`spec-${id}`}
|
|
51
|
+
href={`/specifications/${id}`}
|
|
52
|
+
className="inline-flex items-center rounded-full bg-purple-50 px-2 py-0.5 text-xs text-purple-700 hover:bg-purple-100"
|
|
53
|
+
data-testid="linked-specification"
|
|
54
|
+
>
|
|
55
|
+
{specificationTitles[id] ?? id}
|
|
56
|
+
</Link>
|
|
57
|
+
))}
|
|
58
|
+
{linkedTo.createdSpecifications.map((id) => (
|
|
59
|
+
<Link
|
|
60
|
+
key={`created-spec-${id}`}
|
|
61
|
+
href={`/specifications/${id}`}
|
|
62
|
+
className="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-0.5 text-xs text-emerald-700 hover:bg-emerald-100"
|
|
63
|
+
data-testid="created-specification"
|
|
64
|
+
>
|
|
65
|
+
{specificationTitles[id] ?? id}
|
|
66
|
+
<span className="rounded bg-emerald-200 px-1 text-emerald-800">created</span>
|
|
67
|
+
</Link>
|
|
68
|
+
))}
|
|
69
|
+
{linkedTo.resolved?.requirements.map((id) => (
|
|
70
|
+
<Link
|
|
71
|
+
key={`resolved-req-${id}`}
|
|
72
|
+
href={`/requirements/${id}`}
|
|
73
|
+
className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-200"
|
|
74
|
+
data-testid="resolved-requirement"
|
|
75
|
+
>
|
|
76
|
+
{requirementTitles[id] ?? id}
|
|
77
|
+
<span className="rounded bg-gray-300 px-1 text-gray-700">resolved</span>
|
|
78
|
+
</Link>
|
|
79
|
+
))}
|
|
80
|
+
{linkedTo.resolved?.specifications.map((id) => (
|
|
81
|
+
<Link
|
|
82
|
+
key={`resolved-spec-${id}`}
|
|
83
|
+
href={`/specifications/${id}`}
|
|
84
|
+
className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-200"
|
|
85
|
+
data-testid="resolved-specification"
|
|
86
|
+
>
|
|
87
|
+
{specificationTitles[id] ?? id}
|
|
88
|
+
<span className="rounded bg-gray-300 px-1 text-gray-700">resolved</span>
|
|
89
|
+
</Link>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { FeedbackEntry } from "@reqord/shared";
|
|
3
|
+
import { FeedbackBadge } from "./feedback-badge";
|
|
4
|
+
|
|
5
|
+
export function FeedbackList({ feedbacks }: { feedbacks: FeedbackEntry[] }) {
|
|
6
|
+
if (feedbacks.length === 0) return null;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4" data-testid="feedback-list">
|
|
10
|
+
<h2 className="mb-3 text-sm font-semibold text-gray-700">
|
|
11
|
+
Unresolved Feedback ({feedbacks.length})
|
|
12
|
+
</h2>
|
|
13
|
+
<div className="space-y-3">
|
|
14
|
+
{feedbacks.map((feedback) => (
|
|
15
|
+
<div
|
|
16
|
+
key={feedback.githubIssue}
|
|
17
|
+
className="rounded-md border border-gray-100 bg-gray-50 p-3"
|
|
18
|
+
data-testid="feedback-item"
|
|
19
|
+
>
|
|
20
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
21
|
+
<span className="shrink-0 font-mono text-sm text-blue-600" data-testid="feedback-issue">
|
|
22
|
+
#{feedback.githubIssue}
|
|
23
|
+
</span>
|
|
24
|
+
{feedback.title && (
|
|
25
|
+
<span className="truncate text-sm text-gray-700" data-testid="feedback-title">{feedback.title}</span>
|
|
26
|
+
)}
|
|
27
|
+
<FeedbackBadge
|
|
28
|
+
type={feedback.type}
|
|
29
|
+
severity={feedback.severity}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<p className="mt-1 text-xs text-gray-400">
|
|
33
|
+
{new Date(feedback.syncedAt).toLocaleDateString("ja-JP")}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|