@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,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
type TabCoverageProps = {
|
|
6
|
+
successCriteria: string[] | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function TabCoverage({ successCriteria }: TabCoverageProps) {
|
|
10
|
+
if (!successCriteria || successCriteria.length === 0) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="py-8 text-center text-gray-500">
|
|
13
|
+
No success criteria available
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="overflow-x-auto">
|
|
20
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
21
|
+
<thead className="bg-gray-50">
|
|
22
|
+
<tr>
|
|
23
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-16">
|
|
24
|
+
#
|
|
25
|
+
</th>
|
|
26
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
27
|
+
Criteria
|
|
28
|
+
</th>
|
|
29
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32">
|
|
30
|
+
Status
|
|
31
|
+
</th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
35
|
+
{successCriteria.map((criterion, index) => (
|
|
36
|
+
<tr key={index} className={index % 2 === 1 ? "bg-gray-50" : ""}>
|
|
37
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
|
38
|
+
{index + 1}
|
|
39
|
+
</td>
|
|
40
|
+
<td className="px-4 py-3 text-sm text-gray-900">
|
|
41
|
+
{criterion}
|
|
42
|
+
</td>
|
|
43
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
44
|
+
Unchecked
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
))}
|
|
48
|
+
</tbody>
|
|
49
|
+
</table>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { MarkdownRenderer } from "@/components/requirement/markdown-renderer";
|
|
5
|
+
|
|
6
|
+
export function TabDesign({ content }: { content: string | null }) {
|
|
7
|
+
if (content === null) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="py-8 text-center text-gray-500">
|
|
10
|
+
Design document not available
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return <MarkdownRenderer content={content} />;
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import type { VersionHistoryEntry } from "@reqord/shared";
|
|
5
|
+
import { StatusBadge } from "@/components/ui/badge";
|
|
6
|
+
|
|
7
|
+
type TabHistoryProps = {
|
|
8
|
+
versionHistory: VersionHistoryEntry[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function TabHistory({ versionHistory }: TabHistoryProps) {
|
|
12
|
+
if (!versionHistory || versionHistory.length === 0) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="py-8 text-center text-gray-500">
|
|
15
|
+
No version history yet
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="relative pl-8 border-l-2 border-gray-200">
|
|
22
|
+
{versionHistory.map((entry, index) => {
|
|
23
|
+
const isLatest = index === 0;
|
|
24
|
+
const date = new Date(entry.changedAt);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div key={entry.version} className="relative mb-8 last:mb-0">
|
|
28
|
+
{/* Timeline dot */}
|
|
29
|
+
<div
|
|
30
|
+
className={`absolute -left-[33px] w-3 h-3 rounded-full ${
|
|
31
|
+
isLatest
|
|
32
|
+
? "bg-blue-500 ring-2 ring-blue-500"
|
|
33
|
+
: "bg-gray-400"
|
|
34
|
+
}`}
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
{/* Content */}
|
|
38
|
+
<div className="space-y-2">
|
|
39
|
+
<div className="flex items-center gap-3">
|
|
40
|
+
<span className="font-mono font-semibold text-gray-900">
|
|
41
|
+
{entry.version}
|
|
42
|
+
</span>
|
|
43
|
+
<StatusBadge status={entry.status} />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<p className="text-sm text-gray-500">
|
|
47
|
+
{date.toLocaleDateString("ja-JP", {
|
|
48
|
+
year: "numeric",
|
|
49
|
+
month: "long",
|
|
50
|
+
day: "numeric",
|
|
51
|
+
})}
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<p className="text-sm text-gray-700">{entry.summary}</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
export type IssueItem = {
|
|
6
|
+
number: number;
|
|
7
|
+
title: string;
|
|
8
|
+
url: string;
|
|
9
|
+
priority: string;
|
|
10
|
+
status: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type TabIssuesProps = {
|
|
14
|
+
issues: IssueItem[] | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function getPriorityColor(priority: string): string {
|
|
18
|
+
switch (priority) {
|
|
19
|
+
case "P0":
|
|
20
|
+
return "bg-red-100 text-red-800";
|
|
21
|
+
case "P1":
|
|
22
|
+
return "bg-orange-100 text-orange-800";
|
|
23
|
+
case "P2":
|
|
24
|
+
return "bg-yellow-100 text-yellow-800";
|
|
25
|
+
case "P3":
|
|
26
|
+
return "bg-gray-100 text-gray-800";
|
|
27
|
+
default:
|
|
28
|
+
return "bg-gray-100 text-gray-800";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getStatusColor(status: string): string {
|
|
33
|
+
switch (status) {
|
|
34
|
+
case "open":
|
|
35
|
+
return "bg-gray-100 text-gray-800";
|
|
36
|
+
case "in_progress":
|
|
37
|
+
return "bg-blue-100 text-blue-800";
|
|
38
|
+
case "closed":
|
|
39
|
+
return "bg-green-100 text-green-800";
|
|
40
|
+
default:
|
|
41
|
+
return "bg-gray-100 text-gray-800";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function TabIssues({ issues }: TabIssuesProps) {
|
|
46
|
+
if (!issues || issues.length === 0) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="py-8 text-center text-gray-500">
|
|
49
|
+
No issues generated yet
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="overflow-x-auto">
|
|
56
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
57
|
+
<thead className="bg-gray-50">
|
|
58
|
+
<tr>
|
|
59
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-20">
|
|
60
|
+
#
|
|
61
|
+
</th>
|
|
62
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
63
|
+
Title
|
|
64
|
+
</th>
|
|
65
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-24">
|
|
66
|
+
Priority
|
|
67
|
+
</th>
|
|
68
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32">
|
|
69
|
+
Status
|
|
70
|
+
</th>
|
|
71
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-24">
|
|
72
|
+
Link
|
|
73
|
+
</th>
|
|
74
|
+
</tr>
|
|
75
|
+
</thead>
|
|
76
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
77
|
+
{issues.map((issue) => (
|
|
78
|
+
<tr key={issue.number}>
|
|
79
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
|
80
|
+
{issue.number}
|
|
81
|
+
</td>
|
|
82
|
+
<td className="px-4 py-3 text-sm text-gray-900">
|
|
83
|
+
{issue.title}
|
|
84
|
+
</td>
|
|
85
|
+
<td className="px-4 py-3 whitespace-nowrap">
|
|
86
|
+
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getPriorityColor(issue.priority)}`}>
|
|
87
|
+
{issue.priority}
|
|
88
|
+
</span>
|
|
89
|
+
</td>
|
|
90
|
+
<td className="px-4 py-3 whitespace-nowrap">
|
|
91
|
+
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(issue.status)}`}>
|
|
92
|
+
{issue.status}
|
|
93
|
+
</span>
|
|
94
|
+
</td>
|
|
95
|
+
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
|
96
|
+
<a
|
|
97
|
+
href={issue.url}
|
|
98
|
+
target="_blank"
|
|
99
|
+
rel="noopener noreferrer"
|
|
100
|
+
className="text-blue-600 hover:underline"
|
|
101
|
+
>
|
|
102
|
+
View
|
|
103
|
+
</a>
|
|
104
|
+
</td>
|
|
105
|
+
</tr>
|
|
106
|
+
))}
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { MarkdownRenderer } from "@/components/requirement/markdown-renderer";
|
|
5
|
+
|
|
6
|
+
export function TabResearch({ content }: { content: string | null }) {
|
|
7
|
+
if (content === null) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="py-8 text-center text-gray-500">
|
|
10
|
+
Research document not available
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return <MarkdownRenderer content={content} />;
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Status, Priority, Complexity } from "@reqord/shared";
|
|
3
|
+
|
|
4
|
+
const STATUS_STYLES: Record<Status, string> = {
|
|
5
|
+
draft: "bg-gray-100 text-gray-700",
|
|
6
|
+
approved: "bg-green-100 text-green-800",
|
|
7
|
+
implemented: "bg-blue-100 text-blue-800",
|
|
8
|
+
deprecated: "bg-red-100 text-red-700",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const STATUS_LABELS: Record<Status, string> = {
|
|
12
|
+
draft: "Draft",
|
|
13
|
+
approved: "Approved",
|
|
14
|
+
implemented: "Implemented",
|
|
15
|
+
deprecated: "Deprecated",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PRIORITY_STYLES: Record<Priority, string> = {
|
|
19
|
+
low: "bg-blue-100 text-blue-700",
|
|
20
|
+
medium: "bg-orange-100 text-orange-700",
|
|
21
|
+
high: "bg-red-100 text-red-700",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PRIORITY_LABELS: Record<Priority, string> = {
|
|
25
|
+
low: "Low",
|
|
26
|
+
medium: "Medium",
|
|
27
|
+
high: "High",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const COMPLEXITY_STYLES: Record<Complexity, string> = {
|
|
31
|
+
small: "bg-emerald-100 text-emerald-700",
|
|
32
|
+
medium: "bg-amber-100 text-amber-700",
|
|
33
|
+
large: "bg-orange-100 text-orange-700",
|
|
34
|
+
xlarge: "bg-red-100 text-red-700",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const COMPLEXITY_LABELS: Record<Complexity, string> = {
|
|
38
|
+
small: "S",
|
|
39
|
+
medium: "M",
|
|
40
|
+
large: "L",
|
|
41
|
+
xlarge: "XL",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function Badge({ className, children }: { className: string; children: React.ReactNode }) {
|
|
45
|
+
return (
|
|
46
|
+
<span
|
|
47
|
+
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${className}`}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function StatusBadge({ status }: { status: Status }) {
|
|
55
|
+
return <Badge className={STATUS_STYLES[status]}>{STATUS_LABELS[status]}</Badge>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function PriorityBadge({ priority }: { priority: Priority }) {
|
|
59
|
+
return <Badge className={PRIORITY_STYLES[priority]}>{PRIORITY_LABELS[priority]}</Badge>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ComplexityBadge({ complexity }: { complexity: Complexity }) {
|
|
63
|
+
return <Badge className={COMPLEXITY_STYLES[complexity]}>{COMPLEXITY_LABELS[complexity]}</Badge>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export function Nav() {
|
|
4
|
+
return (
|
|
5
|
+
<nav className="border-b border-gray-200 bg-white">
|
|
6
|
+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
7
|
+
<div className="flex h-14 items-center justify-between">
|
|
8
|
+
<div className="flex items-center gap-8">
|
|
9
|
+
<Link href="/requirements" className="text-lg font-bold text-gray-900">
|
|
10
|
+
Reqord
|
|
11
|
+
</Link>
|
|
12
|
+
<div className="flex gap-4">
|
|
13
|
+
<Link
|
|
14
|
+
href="/dashboard"
|
|
15
|
+
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
|
16
|
+
>
|
|
17
|
+
Dashboard
|
|
18
|
+
</Link>
|
|
19
|
+
<Link
|
|
20
|
+
href="/requirements"
|
|
21
|
+
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
|
22
|
+
>
|
|
23
|
+
Requirements
|
|
24
|
+
</Link>
|
|
25
|
+
<Link
|
|
26
|
+
href="/specifications"
|
|
27
|
+
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
|
28
|
+
>
|
|
29
|
+
Specifications
|
|
30
|
+
</Link>
|
|
31
|
+
<Link
|
|
32
|
+
href="/feedback"
|
|
33
|
+
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
|
34
|
+
>
|
|
35
|
+
Feedback
|
|
36
|
+
</Link>
|
|
37
|
+
<Link
|
|
38
|
+
href="/graph"
|
|
39
|
+
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
|
40
|
+
>
|
|
41
|
+
Graph
|
|
42
|
+
</Link>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</nav>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
type Tab = { id: string; label: string };
|
|
6
|
+
|
|
7
|
+
type TabsProps = {
|
|
8
|
+
tabs: Tab[];
|
|
9
|
+
activeTab: string;
|
|
10
|
+
onTabChange: (id: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Tabs({ tabs, activeTab, onTabChange }: TabsProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="border-b border-gray-200">
|
|
16
|
+
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
|
17
|
+
{tabs.map((tab) => {
|
|
18
|
+
const isActive = activeTab === tab.id;
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
key={tab.id}
|
|
22
|
+
onClick={() => onTabChange(tab.id)}
|
|
23
|
+
className={`
|
|
24
|
+
whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium
|
|
25
|
+
${
|
|
26
|
+
isActive
|
|
27
|
+
? "border-blue-500 text-blue-600"
|
|
28
|
+
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
|
29
|
+
}
|
|
30
|
+
`}
|
|
31
|
+
>
|
|
32
|
+
{tab.label}
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
})}
|
|
36
|
+
</nav>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { revalidatePath } from "next/cache";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
import { RequirementSchema } from "@reqord/shared";
|
|
6
|
+
import { getRepository } from "./get-repository";
|
|
7
|
+
|
|
8
|
+
export type ActionResult = {
|
|
9
|
+
success: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function createRequirement(formData: FormData): Promise<ActionResult> {
|
|
14
|
+
const repo = getRepository();
|
|
15
|
+
|
|
16
|
+
const title = formData.get("title") as string;
|
|
17
|
+
const status = formData.get("status") as string;
|
|
18
|
+
const priority = formData.get("priority") as string;
|
|
19
|
+
const formatType = formData.get("formatType") as string;
|
|
20
|
+
const estimatedComplexity = formData.get("estimatedComplexity") as string | null;
|
|
21
|
+
const estimatedHours = formData.get("estimatedHours") as string | null;
|
|
22
|
+
const description = formData.get("description") as string | null;
|
|
23
|
+
const successCriteriaRaw = formData.get("successCriteria") as string | null;
|
|
24
|
+
const blockedByRaw = formData.get("blockedBy") as string | null;
|
|
25
|
+
const blocksRaw = formData.get("blocks") as string | null;
|
|
26
|
+
const relatedToRaw = formData.get("relatedTo") as string | null;
|
|
27
|
+
|
|
28
|
+
// User story fields
|
|
29
|
+
const userStoryAs = formData.get("userStoryAs") as string | null;
|
|
30
|
+
const userStoryIWant = formData.get("userStoryIWant") as string | null;
|
|
31
|
+
const userStorySoThat = formData.get("userStorySoThat") as string | null;
|
|
32
|
+
|
|
33
|
+
// EARS fields
|
|
34
|
+
const earsType = formData.get("earsType") as string | null;
|
|
35
|
+
const earsTrigger = formData.get("earsTrigger") as string | null;
|
|
36
|
+
const earsCondition = formData.get("earsCondition") as string | null;
|
|
37
|
+
const earsAction = formData.get("earsAction") as string | null;
|
|
38
|
+
const earsResponse = formData.get("earsResponse") as string | null;
|
|
39
|
+
|
|
40
|
+
const id = await repo.generateNextId();
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
|
|
43
|
+
const successCriteria = successCriteriaRaw
|
|
44
|
+
? JSON.parse(successCriteriaRaw) as string[]
|
|
45
|
+
: [];
|
|
46
|
+
const blockedBy = blockedByRaw ? JSON.parse(blockedByRaw) as string[] : [];
|
|
47
|
+
const blocks = blocksRaw ? JSON.parse(blocksRaw) as string[] : [];
|
|
48
|
+
const relatedTo = relatedToRaw ? JSON.parse(relatedToRaw) as string[] : [];
|
|
49
|
+
|
|
50
|
+
let format: Record<string, unknown>;
|
|
51
|
+
switch (formatType) {
|
|
52
|
+
case "user-story":
|
|
53
|
+
format = {
|
|
54
|
+
type: "user-story",
|
|
55
|
+
userStory: {
|
|
56
|
+
as: userStoryAs ?? "",
|
|
57
|
+
iWant: userStoryIWant ?? "",
|
|
58
|
+
soThat: userStorySoThat ?? "",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
break;
|
|
62
|
+
case "ears":
|
|
63
|
+
format = {
|
|
64
|
+
type: "ears",
|
|
65
|
+
ears: {
|
|
66
|
+
type: earsType ?? "",
|
|
67
|
+
trigger: earsTrigger || undefined,
|
|
68
|
+
condition: earsCondition || undefined,
|
|
69
|
+
action: earsAction ?? "",
|
|
70
|
+
response: earsResponse || undefined,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
format = { type: "free-form" };
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const raw = {
|
|
80
|
+
id,
|
|
81
|
+
version: "1.0.0",
|
|
82
|
+
title,
|
|
83
|
+
status,
|
|
84
|
+
priority,
|
|
85
|
+
createdAt: now,
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
versionHistory: [],
|
|
88
|
+
files: { description: `${id}/description.md` },
|
|
89
|
+
successCriteria,
|
|
90
|
+
format,
|
|
91
|
+
dependencies: { blockedBy, blocks, relatedTo },
|
|
92
|
+
estimatedComplexity: estimatedComplexity || undefined,
|
|
93
|
+
estimatedHours: estimatedHours ? Number(estimatedHours) : undefined,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = RequirementSchema.safeParse(raw);
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
return { success: false, error: result.error.message };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await repo.save(result.data);
|
|
102
|
+
|
|
103
|
+
if (description?.trim()) {
|
|
104
|
+
await repo.saveDescription(id, description);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
revalidatePath("/requirements");
|
|
108
|
+
redirect(`/requirements/${id}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function updateRequirement(formData: FormData): Promise<ActionResult> {
|
|
112
|
+
const repo = getRepository();
|
|
113
|
+
|
|
114
|
+
const id = formData.get("id") as string;
|
|
115
|
+
const existing = await repo.findById(id);
|
|
116
|
+
if (!existing) {
|
|
117
|
+
return { success: false, error: `Requirement ${id} not found` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const title = formData.get("title") as string;
|
|
121
|
+
const status = formData.get("status") as string;
|
|
122
|
+
const priority = formData.get("priority") as string;
|
|
123
|
+
const formatType = formData.get("formatType") as string;
|
|
124
|
+
const estimatedComplexity = formData.get("estimatedComplexity") as string | null;
|
|
125
|
+
const estimatedHours = formData.get("estimatedHours") as string | null;
|
|
126
|
+
const description = formData.get("description") as string | null;
|
|
127
|
+
const successCriteriaRaw = formData.get("successCriteria") as string | null;
|
|
128
|
+
const blockedByRaw = formData.get("blockedBy") as string | null;
|
|
129
|
+
const blocksRaw = formData.get("blocks") as string | null;
|
|
130
|
+
const relatedToRaw = formData.get("relatedTo") as string | null;
|
|
131
|
+
|
|
132
|
+
// User story fields
|
|
133
|
+
const userStoryAs = formData.get("userStoryAs") as string | null;
|
|
134
|
+
const userStoryIWant = formData.get("userStoryIWant") as string | null;
|
|
135
|
+
const userStorySoThat = formData.get("userStorySoThat") as string | null;
|
|
136
|
+
|
|
137
|
+
// EARS fields
|
|
138
|
+
const earsType = formData.get("earsType") as string | null;
|
|
139
|
+
const earsTrigger = formData.get("earsTrigger") as string | null;
|
|
140
|
+
const earsCondition = formData.get("earsCondition") as string | null;
|
|
141
|
+
const earsAction = formData.get("earsAction") as string | null;
|
|
142
|
+
const earsResponse = formData.get("earsResponse") as string | null;
|
|
143
|
+
|
|
144
|
+
const now = new Date().toISOString();
|
|
145
|
+
const successCriteria = successCriteriaRaw
|
|
146
|
+
? JSON.parse(successCriteriaRaw) as string[]
|
|
147
|
+
: [];
|
|
148
|
+
const blockedBy = blockedByRaw ? JSON.parse(blockedByRaw) as string[] : [];
|
|
149
|
+
const blocks = blocksRaw ? JSON.parse(blocksRaw) as string[] : [];
|
|
150
|
+
const relatedTo = relatedToRaw ? JSON.parse(relatedToRaw) as string[] : [];
|
|
151
|
+
|
|
152
|
+
let format: Record<string, unknown>;
|
|
153
|
+
switch (formatType) {
|
|
154
|
+
case "user-story":
|
|
155
|
+
format = {
|
|
156
|
+
type: "user-story",
|
|
157
|
+
userStory: {
|
|
158
|
+
as: userStoryAs ?? "",
|
|
159
|
+
iWant: userStoryIWant ?? "",
|
|
160
|
+
soThat: userStorySoThat ?? "",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
break;
|
|
164
|
+
case "ears":
|
|
165
|
+
format = {
|
|
166
|
+
type: "ears",
|
|
167
|
+
ears: {
|
|
168
|
+
type: earsType ?? "",
|
|
169
|
+
trigger: earsTrigger || undefined,
|
|
170
|
+
condition: earsCondition || undefined,
|
|
171
|
+
action: earsAction ?? "",
|
|
172
|
+
response: earsResponse || undefined,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
format = { type: "free-form" };
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const raw = {
|
|
182
|
+
...existing,
|
|
183
|
+
title,
|
|
184
|
+
status,
|
|
185
|
+
priority,
|
|
186
|
+
updatedAt: now,
|
|
187
|
+
successCriteria,
|
|
188
|
+
format,
|
|
189
|
+
dependencies: { blockedBy, blocks, relatedTo },
|
|
190
|
+
estimatedComplexity: estimatedComplexity || undefined,
|
|
191
|
+
estimatedHours: estimatedHours ? Number(estimatedHours) : undefined,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const result = RequirementSchema.safeParse(raw);
|
|
195
|
+
if (!result.success) {
|
|
196
|
+
return { success: false, error: result.error.message };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await repo.save(result.data);
|
|
200
|
+
|
|
201
|
+
if (description != null) {
|
|
202
|
+
await repo.saveDescription(id, description);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
revalidatePath("/requirements");
|
|
206
|
+
revalidatePath(`/requirements/${id}`);
|
|
207
|
+
redirect(`/requirements/${id}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function deleteRequirement(id: string): Promise<ActionResult> {
|
|
211
|
+
const repo = getRepository();
|
|
212
|
+
|
|
213
|
+
const existing = await repo.findById(id);
|
|
214
|
+
if (!existing) {
|
|
215
|
+
return { success: false, error: `Requirement ${id} not found` };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await repo.deleteById(id);
|
|
219
|
+
|
|
220
|
+
revalidatePath("/requirements");
|
|
221
|
+
redirect("/requirements");
|
|
222
|
+
}
|