@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,228 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { Requirement, Specification, FeedbackEntry } from "@reqord/shared";
|
|
3
|
+
import { StatusBadge, PriorityBadge, ComplexityBadge } from "@/components/ui/badge";
|
|
4
|
+
import { FeedbackList } from "@/components/feedback/feedback-list";
|
|
5
|
+
import { DeleteButton } from "./delete-button";
|
|
6
|
+
import { RequirementTabs } from "./requirement-tabs";
|
|
7
|
+
|
|
8
|
+
export function RequirementDetail({
|
|
9
|
+
requirement,
|
|
10
|
+
description,
|
|
11
|
+
specifications = [],
|
|
12
|
+
feedbacks,
|
|
13
|
+
}: {
|
|
14
|
+
requirement: Requirement;
|
|
15
|
+
description: string | null;
|
|
16
|
+
specifications?: Specification[];
|
|
17
|
+
feedbacks?: FeedbackEntry[];
|
|
18
|
+
}) {
|
|
19
|
+
const { dependencies } = requirement;
|
|
20
|
+
const hasDeps =
|
|
21
|
+
dependencies.blockedBy.length > 0 ||
|
|
22
|
+
dependencies.blocks.length > 0 ||
|
|
23
|
+
dependencies.relatedTo.length > 0;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="space-y-6">
|
|
27
|
+
{/* Header */}
|
|
28
|
+
<div className="flex items-start justify-between">
|
|
29
|
+
<div>
|
|
30
|
+
<p className="text-sm text-gray-500">
|
|
31
|
+
<Link href="/requirements" className="hover:text-blue-600">
|
|
32
|
+
Requirements
|
|
33
|
+
</Link>
|
|
34
|
+
{" / "}
|
|
35
|
+
<span className="font-mono">{requirement.id}</span>
|
|
36
|
+
</p>
|
|
37
|
+
<h1 className="mt-1 text-2xl font-bold">{requirement.title}</h1>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<Link
|
|
41
|
+
href={`/requirements/${requirement.id}/edit`}
|
|
42
|
+
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
43
|
+
>
|
|
44
|
+
Edit
|
|
45
|
+
</Link>
|
|
46
|
+
<DeleteButton id={requirement.id} />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Badges */}
|
|
51
|
+
<div className="flex flex-wrap gap-2">
|
|
52
|
+
<StatusBadge status={requirement.status} />
|
|
53
|
+
<PriorityBadge priority={requirement.priority} />
|
|
54
|
+
{requirement.estimatedComplexity ? (
|
|
55
|
+
<ComplexityBadge complexity={requirement.estimatedComplexity} />
|
|
56
|
+
) : null}
|
|
57
|
+
{requirement.estimatedHours ? (
|
|
58
|
+
<span className="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700">
|
|
59
|
+
{requirement.estimatedHours}h
|
|
60
|
+
</span>
|
|
61
|
+
) : null}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Meta */}
|
|
65
|
+
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 bg-white p-4 text-sm sm:grid-cols-4">
|
|
66
|
+
<div>
|
|
67
|
+
<p className="text-gray-500">Version</p>
|
|
68
|
+
<p className="font-medium">{requirement.version}</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<p className="text-gray-500">Format</p>
|
|
72
|
+
<p className="font-medium">{requirement.format.type}</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div>
|
|
75
|
+
<p className="text-gray-500">Created</p>
|
|
76
|
+
<p className="font-medium">
|
|
77
|
+
{new Date(requirement.createdAt).toLocaleDateString("ja-JP")}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<p className="text-gray-500">Updated</p>
|
|
82
|
+
<p className="font-medium">
|
|
83
|
+
{new Date(requirement.updatedAt).toLocaleDateString("ja-JP")}
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Format Details */}
|
|
89
|
+
{requirement.format.type === "user-story" ? (
|
|
90
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
91
|
+
<h2 className="mb-2 text-sm font-semibold text-gray-700">User Story</h2>
|
|
92
|
+
<div className="space-y-1 text-sm">
|
|
93
|
+
<p>
|
|
94
|
+
<span className="font-medium text-gray-500">As a</span>{" "}
|
|
95
|
+
{requirement.format.userStory.as}
|
|
96
|
+
</p>
|
|
97
|
+
<p>
|
|
98
|
+
<span className="font-medium text-gray-500">I want</span>{" "}
|
|
99
|
+
{requirement.format.userStory.iWant}
|
|
100
|
+
</p>
|
|
101
|
+
<p>
|
|
102
|
+
<span className="font-medium text-gray-500">So that</span>{" "}
|
|
103
|
+
{requirement.format.userStory.soThat}
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
) : null}
|
|
108
|
+
|
|
109
|
+
{requirement.format.type === "ears" ? (
|
|
110
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
111
|
+
<h2 className="mb-2 text-sm font-semibold text-gray-700">EARS Format</h2>
|
|
112
|
+
<div className="space-y-1 text-sm">
|
|
113
|
+
<p>
|
|
114
|
+
<span className="font-medium text-gray-500">Type:</span>{" "}
|
|
115
|
+
{requirement.format.ears.type}
|
|
116
|
+
</p>
|
|
117
|
+
{requirement.format.ears.trigger ? (
|
|
118
|
+
<p>
|
|
119
|
+
<span className="font-medium text-gray-500">Trigger:</span>{" "}
|
|
120
|
+
{requirement.format.ears.trigger}
|
|
121
|
+
</p>
|
|
122
|
+
) : null}
|
|
123
|
+
{requirement.format.ears.condition ? (
|
|
124
|
+
<p>
|
|
125
|
+
<span className="font-medium text-gray-500">Condition:</span>{" "}
|
|
126
|
+
{requirement.format.ears.condition}
|
|
127
|
+
</p>
|
|
128
|
+
) : null}
|
|
129
|
+
<p>
|
|
130
|
+
<span className="font-medium text-gray-500">Action:</span>{" "}
|
|
131
|
+
{requirement.format.ears.action}
|
|
132
|
+
</p>
|
|
133
|
+
{requirement.format.ears.response ? (
|
|
134
|
+
<p>
|
|
135
|
+
<span className="font-medium text-gray-500">Response:</span>{" "}
|
|
136
|
+
{requirement.format.ears.response}
|
|
137
|
+
</p>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
) : null}
|
|
142
|
+
|
|
143
|
+
{/* Feedbacks */}
|
|
144
|
+
{feedbacks && feedbacks.length > 0 ? (
|
|
145
|
+
<FeedbackList feedbacks={feedbacks} />
|
|
146
|
+
) : null}
|
|
147
|
+
|
|
148
|
+
{/* Dependencies */}
|
|
149
|
+
{hasDeps ? (
|
|
150
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
151
|
+
<h2 className="mb-2 text-sm font-semibold text-gray-700">Dependencies</h2>
|
|
152
|
+
<div className="space-y-2 text-sm">
|
|
153
|
+
{dependencies.blockedBy.length > 0 ? (
|
|
154
|
+
<div>
|
|
155
|
+
<span className="font-medium text-gray-500">Blocked by: </span>
|
|
156
|
+
{dependencies.blockedBy.map((id) => (
|
|
157
|
+
<Link
|
|
158
|
+
key={id}
|
|
159
|
+
href={`/requirements/${id}`}
|
|
160
|
+
className="mr-2 font-mono text-blue-600 hover:underline"
|
|
161
|
+
>
|
|
162
|
+
{id}
|
|
163
|
+
</Link>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
) : null}
|
|
167
|
+
{dependencies.blocks.length > 0 ? (
|
|
168
|
+
<div>
|
|
169
|
+
<span className="font-medium text-gray-500">Blocks: </span>
|
|
170
|
+
{dependencies.blocks.map((id) => (
|
|
171
|
+
<Link
|
|
172
|
+
key={id}
|
|
173
|
+
href={`/requirements/${id}`}
|
|
174
|
+
className="mr-2 font-mono text-blue-600 hover:underline"
|
|
175
|
+
>
|
|
176
|
+
{id}
|
|
177
|
+
</Link>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
) : null}
|
|
181
|
+
{dependencies.relatedTo.length > 0 ? (
|
|
182
|
+
<div>
|
|
183
|
+
<span className="font-medium text-gray-500">Related to: </span>
|
|
184
|
+
{dependencies.relatedTo.map((id) => (
|
|
185
|
+
<Link
|
|
186
|
+
key={id}
|
|
187
|
+
href={`/requirements/${id}`}
|
|
188
|
+
className="mr-2 font-mono text-blue-600 hover:underline"
|
|
189
|
+
>
|
|
190
|
+
{id}
|
|
191
|
+
</Link>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
) : null}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
) : null}
|
|
198
|
+
|
|
199
|
+
{/* Specifications */}
|
|
200
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
|
201
|
+
<h2 className="mb-2 text-sm font-semibold text-gray-700">Specifications</h2>
|
|
202
|
+
{specifications.length > 0 ? (
|
|
203
|
+
<div className="space-y-1 text-sm">
|
|
204
|
+
{specifications.map((spec) => (
|
|
205
|
+
<div key={spec.id} className="flex items-center gap-2">
|
|
206
|
+
<Link
|
|
207
|
+
href={`/specifications/${spec.id}`}
|
|
208
|
+
className="font-mono text-blue-600 hover:underline"
|
|
209
|
+
>
|
|
210
|
+
{spec.id}
|
|
211
|
+
</Link>
|
|
212
|
+
<StatusBadge status={spec.status} />
|
|
213
|
+
</div>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
) : (
|
|
217
|
+
<p className="text-sm text-gray-500">No specifications</p>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Success Criteria & Description Tabs */}
|
|
222
|
+
<RequirementTabs
|
|
223
|
+
successCriteria={requirement.successCriteria}
|
|
224
|
+
description={description}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useActionState, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import type { Requirement } from "@reqord/shared";
|
|
6
|
+
import { createRequirement, updateRequirement, type ActionResult } from "@/lib/actions";
|
|
7
|
+
import { SuccessCriteriaEditor } from "./success-criteria-editor";
|
|
8
|
+
import { DependencyEditor } from "./dependency-editor";
|
|
9
|
+
import { MarkdownEditor } from "./markdown-editor";
|
|
10
|
+
|
|
11
|
+
type Mode = "create" | "edit";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
mode: Mode;
|
|
15
|
+
requirement?: Requirement;
|
|
16
|
+
description?: string | null;
|
|
17
|
+
allRequirements: Requirement[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const INITIAL_STATE: ActionResult = { success: true };
|
|
21
|
+
|
|
22
|
+
export function RequirementForm({ mode, requirement, description, allRequirements }: Props) {
|
|
23
|
+
const action = mode === "create" ? createRequirement : updateRequirement;
|
|
24
|
+
const [state, formAction, isPending] = useActionState(
|
|
25
|
+
async (_prev: ActionResult, formData: FormData) => action(formData),
|
|
26
|
+
INITIAL_STATE,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const [title, setTitle] = useState(requirement?.title ?? "");
|
|
30
|
+
const [status, setStatus] = useState<string>(requirement?.status ?? "draft");
|
|
31
|
+
const [priority, setPriority] = useState<string>(requirement?.priority ?? "medium");
|
|
32
|
+
const [formatType, setFormatType] = useState<string>(requirement?.format.type ?? "free-form");
|
|
33
|
+
const [estimatedComplexity, setEstimatedComplexity] = useState(
|
|
34
|
+
requirement?.estimatedComplexity ?? "",
|
|
35
|
+
);
|
|
36
|
+
const [estimatedHours, setEstimatedHours] = useState(
|
|
37
|
+
requirement?.estimatedHours?.toString() ?? "",
|
|
38
|
+
);
|
|
39
|
+
const [successCriteria, setSuccessCriteria] = useState<string[]>(
|
|
40
|
+
requirement?.successCriteria ?? [],
|
|
41
|
+
);
|
|
42
|
+
const [blockedBy, setBlockedBy] = useState<string[]>(
|
|
43
|
+
requirement?.dependencies.blockedBy ?? [],
|
|
44
|
+
);
|
|
45
|
+
const [blocks, setBlocks] = useState<string[]>(
|
|
46
|
+
requirement?.dependencies.blocks ?? [],
|
|
47
|
+
);
|
|
48
|
+
const [relatedTo, setRelatedTo] = useState<string[]>(
|
|
49
|
+
requirement?.dependencies.relatedTo ?? [],
|
|
50
|
+
);
|
|
51
|
+
const [descriptionText, setDescriptionText] = useState(description ?? "");
|
|
52
|
+
|
|
53
|
+
// User Story
|
|
54
|
+
const [userStoryAs, setUserStoryAs] = useState(
|
|
55
|
+
requirement?.format.type === "user-story" ? requirement.format.userStory.as : "",
|
|
56
|
+
);
|
|
57
|
+
const [userStoryIWant, setUserStoryIWant] = useState(
|
|
58
|
+
requirement?.format.type === "user-story" ? requirement.format.userStory.iWant : "",
|
|
59
|
+
);
|
|
60
|
+
const [userStorySoThat, setUserStorySoThat] = useState(
|
|
61
|
+
requirement?.format.type === "user-story" ? requirement.format.userStory.soThat : "",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// EARS
|
|
65
|
+
const [earsType, setEarsType] = useState(
|
|
66
|
+
requirement?.format.type === "ears" ? requirement.format.ears.type : "",
|
|
67
|
+
);
|
|
68
|
+
const [earsTrigger, setEarsTrigger] = useState(
|
|
69
|
+
requirement?.format.type === "ears" ? (requirement.format.ears.trigger ?? "") : "",
|
|
70
|
+
);
|
|
71
|
+
const [earsCondition, setEarsCondition] = useState(
|
|
72
|
+
requirement?.format.type === "ears" ? (requirement.format.ears.condition ?? "") : "",
|
|
73
|
+
);
|
|
74
|
+
const [earsAction, setEarsAction] = useState(
|
|
75
|
+
requirement?.format.type === "ears" ? requirement.format.ears.action : "",
|
|
76
|
+
);
|
|
77
|
+
const [earsResponse, setEarsResponse] = useState(
|
|
78
|
+
requirement?.format.type === "ears" ? (requirement.format.ears.response ?? "") : "",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const cancelHref = mode === "edit" ? `/requirements/${requirement!.id}` : "/requirements";
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<form action={formAction} className="space-y-6">
|
|
85
|
+
{mode === "edit" ? <input type="hidden" name="id" value={requirement!.id} /> : null}
|
|
86
|
+
|
|
87
|
+
{/* Hidden JSON fields */}
|
|
88
|
+
<input type="hidden" name="successCriteria" value={JSON.stringify(successCriteria)} />
|
|
89
|
+
<input type="hidden" name="blockedBy" value={JSON.stringify(blockedBy)} />
|
|
90
|
+
<input type="hidden" name="blocks" value={JSON.stringify(blocks)} />
|
|
91
|
+
<input type="hidden" name="relatedTo" value={JSON.stringify(relatedTo)} />
|
|
92
|
+
<input type="hidden" name="description" value={descriptionText} />
|
|
93
|
+
<input type="hidden" name="formatType" value={formatType} />
|
|
94
|
+
|
|
95
|
+
{/* User Story hidden fields */}
|
|
96
|
+
<input type="hidden" name="userStoryAs" value={userStoryAs} />
|
|
97
|
+
<input type="hidden" name="userStoryIWant" value={userStoryIWant} />
|
|
98
|
+
<input type="hidden" name="userStorySoThat" value={userStorySoThat} />
|
|
99
|
+
|
|
100
|
+
{/* EARS hidden fields */}
|
|
101
|
+
<input type="hidden" name="earsType" value={earsType} />
|
|
102
|
+
<input type="hidden" name="earsTrigger" value={earsTrigger} />
|
|
103
|
+
<input type="hidden" name="earsCondition" value={earsCondition} />
|
|
104
|
+
<input type="hidden" name="earsAction" value={earsAction} />
|
|
105
|
+
<input type="hidden" name="earsResponse" value={earsResponse} />
|
|
106
|
+
|
|
107
|
+
{!state.success ? (
|
|
108
|
+
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700">
|
|
109
|
+
{state.error}
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
112
|
+
|
|
113
|
+
{/* Breadcrumb */}
|
|
114
|
+
<p className="text-sm text-gray-500">
|
|
115
|
+
<Link href="/requirements" className="hover:text-blue-600">
|
|
116
|
+
Requirements
|
|
117
|
+
</Link>
|
|
118
|
+
{mode === "edit" ? (
|
|
119
|
+
<>
|
|
120
|
+
{" / "}
|
|
121
|
+
<Link href={`/requirements/${requirement!.id}`} className="hover:text-blue-600">
|
|
122
|
+
{requirement!.id}
|
|
123
|
+
</Link>
|
|
124
|
+
{" / Edit"}
|
|
125
|
+
</>
|
|
126
|
+
) : (
|
|
127
|
+
" / New"
|
|
128
|
+
)}
|
|
129
|
+
</p>
|
|
130
|
+
|
|
131
|
+
<h1 className="text-2xl font-bold">
|
|
132
|
+
{mode === "create" ? "New Requirement" : `Edit ${requirement!.id}`}
|
|
133
|
+
</h1>
|
|
134
|
+
|
|
135
|
+
{/* Title */}
|
|
136
|
+
<div>
|
|
137
|
+
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
|
|
138
|
+
Title
|
|
139
|
+
</label>
|
|
140
|
+
<input
|
|
141
|
+
id="title"
|
|
142
|
+
name="title"
|
|
143
|
+
type="text"
|
|
144
|
+
value={title}
|
|
145
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
146
|
+
required
|
|
147
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Status + Priority + Complexity */}
|
|
152
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
153
|
+
<div>
|
|
154
|
+
<label htmlFor="status" className="block text-sm font-medium text-gray-700">
|
|
155
|
+
Status
|
|
156
|
+
</label>
|
|
157
|
+
<select
|
|
158
|
+
id="status"
|
|
159
|
+
name="status"
|
|
160
|
+
value={status}
|
|
161
|
+
onChange={(e) => setStatus(e.target.value)}
|
|
162
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
|
|
163
|
+
>
|
|
164
|
+
<option value="draft">Draft</option>
|
|
165
|
+
<option value="approved">Approved</option>
|
|
166
|
+
<option value="implemented">Implemented</option>
|
|
167
|
+
<option value="deprecated">Deprecated</option>
|
|
168
|
+
</select>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div>
|
|
172
|
+
<label htmlFor="priority" className="block text-sm font-medium text-gray-700">
|
|
173
|
+
Priority
|
|
174
|
+
</label>
|
|
175
|
+
<select
|
|
176
|
+
id="priority"
|
|
177
|
+
name="priority"
|
|
178
|
+
value={priority}
|
|
179
|
+
onChange={(e) => setPriority(e.target.value)}
|
|
180
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
|
|
181
|
+
>
|
|
182
|
+
<option value="low">Low</option>
|
|
183
|
+
<option value="medium">Medium</option>
|
|
184
|
+
<option value="high">High</option>
|
|
185
|
+
</select>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div>
|
|
189
|
+
<label
|
|
190
|
+
htmlFor="estimatedComplexity"
|
|
191
|
+
className="block text-sm font-medium text-gray-700"
|
|
192
|
+
>
|
|
193
|
+
Complexity
|
|
194
|
+
</label>
|
|
195
|
+
<select
|
|
196
|
+
id="estimatedComplexity"
|
|
197
|
+
name="estimatedComplexity"
|
|
198
|
+
value={estimatedComplexity}
|
|
199
|
+
onChange={(e) => setEstimatedComplexity(e.target.value)}
|
|
200
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm"
|
|
201
|
+
>
|
|
202
|
+
<option value="">-</option>
|
|
203
|
+
<option value="small">Small</option>
|
|
204
|
+
<option value="medium">Medium</option>
|
|
205
|
+
<option value="large">Large</option>
|
|
206
|
+
<option value="xlarge">XLarge</option>
|
|
207
|
+
</select>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Estimated Hours */}
|
|
212
|
+
<div className="max-w-xs">
|
|
213
|
+
<label htmlFor="estimatedHours" className="block text-sm font-medium text-gray-700">
|
|
214
|
+
Estimated Hours
|
|
215
|
+
</label>
|
|
216
|
+
<input
|
|
217
|
+
id="estimatedHours"
|
|
218
|
+
name="estimatedHours"
|
|
219
|
+
type="number"
|
|
220
|
+
step="0.5"
|
|
221
|
+
min="0"
|
|
222
|
+
value={estimatedHours}
|
|
223
|
+
onChange={(e) => setEstimatedHours(e.target.value)}
|
|
224
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
225
|
+
placeholder="Optional"
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Format Type */}
|
|
230
|
+
<div>
|
|
231
|
+
<label className="block text-sm font-medium text-gray-700">Format</label>
|
|
232
|
+
<div className="mt-1 flex gap-4">
|
|
233
|
+
{(["free-form", "user-story", "ears"] as const).map((ft) => (
|
|
234
|
+
<label key={ft} className="flex items-center gap-1.5 text-sm">
|
|
235
|
+
<input
|
|
236
|
+
type="radio"
|
|
237
|
+
name="_formatType"
|
|
238
|
+
value={ft}
|
|
239
|
+
checked={formatType === ft}
|
|
240
|
+
onChange={() => setFormatType(ft)}
|
|
241
|
+
className="border-gray-300"
|
|
242
|
+
/>
|
|
243
|
+
{ft === "free-form" ? "Free Form" : ft === "user-story" ? "User Story" : "EARS"}
|
|
244
|
+
</label>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* User Story Fields */}
|
|
250
|
+
{formatType === "user-story" ? (
|
|
251
|
+
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
252
|
+
<h3 className="text-sm font-medium text-gray-700">User Story</h3>
|
|
253
|
+
<div>
|
|
254
|
+
<label className="block text-sm text-gray-600">As a</label>
|
|
255
|
+
<input
|
|
256
|
+
type="text"
|
|
257
|
+
value={userStoryAs}
|
|
258
|
+
onChange={(e) => setUserStoryAs(e.target.value)}
|
|
259
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
<div>
|
|
263
|
+
<label className="block text-sm text-gray-600">I want</label>
|
|
264
|
+
<input
|
|
265
|
+
type="text"
|
|
266
|
+
value={userStoryIWant}
|
|
267
|
+
onChange={(e) => setUserStoryIWant(e.target.value)}
|
|
268
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
<div>
|
|
272
|
+
<label className="block text-sm text-gray-600">So that</label>
|
|
273
|
+
<input
|
|
274
|
+
type="text"
|
|
275
|
+
value={userStorySoThat}
|
|
276
|
+
onChange={(e) => setUserStorySoThat(e.target.value)}
|
|
277
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
) : null}
|
|
282
|
+
|
|
283
|
+
{/* EARS Fields */}
|
|
284
|
+
{formatType === "ears" ? (
|
|
285
|
+
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
286
|
+
<h3 className="text-sm font-medium text-gray-700">EARS</h3>
|
|
287
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
288
|
+
<div>
|
|
289
|
+
<label className="block text-sm text-gray-600">Type</label>
|
|
290
|
+
<input
|
|
291
|
+
type="text"
|
|
292
|
+
value={earsType}
|
|
293
|
+
onChange={(e) => setEarsType(e.target.value)}
|
|
294
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
<div>
|
|
298
|
+
<label className="block text-sm text-gray-600">Trigger</label>
|
|
299
|
+
<input
|
|
300
|
+
type="text"
|
|
301
|
+
value={earsTrigger}
|
|
302
|
+
onChange={(e) => setEarsTrigger(e.target.value)}
|
|
303
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
304
|
+
placeholder="Optional"
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
<div>
|
|
308
|
+
<label className="block text-sm text-gray-600">Condition</label>
|
|
309
|
+
<input
|
|
310
|
+
type="text"
|
|
311
|
+
value={earsCondition}
|
|
312
|
+
onChange={(e) => setEarsCondition(e.target.value)}
|
|
313
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
314
|
+
placeholder="Optional"
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
<div>
|
|
318
|
+
<label className="block text-sm text-gray-600">Action</label>
|
|
319
|
+
<input
|
|
320
|
+
type="text"
|
|
321
|
+
value={earsAction}
|
|
322
|
+
onChange={(e) => setEarsAction(e.target.value)}
|
|
323
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
<div className="sm:col-span-2">
|
|
327
|
+
<label className="block text-sm text-gray-600">Response</label>
|
|
328
|
+
<input
|
|
329
|
+
type="text"
|
|
330
|
+
value={earsResponse}
|
|
331
|
+
onChange={(e) => setEarsResponse(e.target.value)}
|
|
332
|
+
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
333
|
+
placeholder="Optional"
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
) : null}
|
|
339
|
+
|
|
340
|
+
{/* Success Criteria */}
|
|
341
|
+
<SuccessCriteriaEditor criteria={successCriteria} onChange={setSuccessCriteria} />
|
|
342
|
+
|
|
343
|
+
{/* Dependencies */}
|
|
344
|
+
<div className="space-y-4">
|
|
345
|
+
<h3 className="text-sm font-medium text-gray-700">Dependencies</h3>
|
|
346
|
+
<DependencyEditor
|
|
347
|
+
label="Blocked By"
|
|
348
|
+
selected={blockedBy}
|
|
349
|
+
onChange={setBlockedBy}
|
|
350
|
+
allRequirements={allRequirements}
|
|
351
|
+
excludeId={requirement?.id}
|
|
352
|
+
/>
|
|
353
|
+
<DependencyEditor
|
|
354
|
+
label="Blocks"
|
|
355
|
+
selected={blocks}
|
|
356
|
+
onChange={setBlocks}
|
|
357
|
+
allRequirements={allRequirements}
|
|
358
|
+
excludeId={requirement?.id}
|
|
359
|
+
/>
|
|
360
|
+
<DependencyEditor
|
|
361
|
+
label="Related To"
|
|
362
|
+
selected={relatedTo}
|
|
363
|
+
onChange={setRelatedTo}
|
|
364
|
+
allRequirements={allRequirements}
|
|
365
|
+
excludeId={requirement?.id}
|
|
366
|
+
/>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{/* Description */}
|
|
370
|
+
<MarkdownEditor value={descriptionText} onChange={setDescriptionText} />
|
|
371
|
+
|
|
372
|
+
{/* Actions */}
|
|
373
|
+
<div className="flex items-center gap-3">
|
|
374
|
+
<button
|
|
375
|
+
type="submit"
|
|
376
|
+
disabled={isPending}
|
|
377
|
+
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
378
|
+
>
|
|
379
|
+
{isPending ? "Saving..." : mode === "create" ? "Create" : "Save"}
|
|
380
|
+
</button>
|
|
381
|
+
<Link
|
|
382
|
+
href={cancelHref}
|
|
383
|
+
className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
384
|
+
>
|
|
385
|
+
Cancel
|
|
386
|
+
</Link>
|
|
387
|
+
</div>
|
|
388
|
+
</form>
|
|
389
|
+
);
|
|
390
|
+
}
|