@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,104 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { TaskEntry, TasksIndex } from "@reqord/shared";
|
|
3
|
+
|
|
4
|
+
// Mock the file-system module
|
|
5
|
+
vi.mock("../../lib/file-system.js", () => ({
|
|
6
|
+
joinPath: vi.fn((...args: string[]) => args.join("/")),
|
|
7
|
+
readYAML: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Mock reqord-root
|
|
11
|
+
vi.mock("../../lib/reqord-root.js", () => ({
|
|
12
|
+
getReqordRoot: vi.fn(() => "/mock/root"),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockTaskEntry: TaskEntry = {
|
|
16
|
+
number: 1,
|
|
17
|
+
title: "Test Task",
|
|
18
|
+
url: "https://github.com/owner/repo/issues/1",
|
|
19
|
+
status: "open",
|
|
20
|
+
linkedTo: { specifications: ["spec-000001"] },
|
|
21
|
+
syncedAt: "2026-02-22T00:00:00.000Z",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("tasks-data", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.resetModules();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("loadTasksYaml", () => {
|
|
30
|
+
it("returns tasks index from loaded tasks.yaml", async () => {
|
|
31
|
+
const { readYAML } = await import("../../lib/file-system.js");
|
|
32
|
+
|
|
33
|
+
const mockIndex: TasksIndex = {
|
|
34
|
+
title: "Tasks",
|
|
35
|
+
tasks: [mockTaskEntry],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
vi.mocked(readYAML).mockResolvedValue(mockIndex);
|
|
39
|
+
|
|
40
|
+
const { loadTasksYaml } = await import("../../lib/tasks-data.js");
|
|
41
|
+
const result = await loadTasksYaml();
|
|
42
|
+
|
|
43
|
+
expect(result.tasks).toHaveLength(1);
|
|
44
|
+
expect(result.tasks[0]).toEqual(mockTaskEntry);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns empty tasks index when tasks.yaml is missing or invalid", async () => {
|
|
48
|
+
const { readYAML } = await import("../../lib/file-system.js");
|
|
49
|
+
|
|
50
|
+
vi.mocked(readYAML).mockRejectedValue(new Error("File not found"));
|
|
51
|
+
|
|
52
|
+
const { loadTasksYaml } = await import("../../lib/tasks-data.js");
|
|
53
|
+
const result = await loadTasksYaml();
|
|
54
|
+
|
|
55
|
+
expect(result.tasks).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("getAllTasks", () => {
|
|
60
|
+
it("returns tasks array from loaded TasksIndex", async () => {
|
|
61
|
+
const { readYAML } = await import("../../lib/file-system.js");
|
|
62
|
+
|
|
63
|
+
const mockIndex: TasksIndex = {
|
|
64
|
+
title: "Tasks",
|
|
65
|
+
tasks: [mockTaskEntry],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
vi.mocked(readYAML).mockResolvedValue(mockIndex);
|
|
69
|
+
|
|
70
|
+
const { getAllTasks } = await import("../../lib/tasks-data.js");
|
|
71
|
+
const result = await getAllTasks();
|
|
72
|
+
|
|
73
|
+
expect(result).toHaveLength(1);
|
|
74
|
+
expect(result[0]).toEqual(mockTaskEntry);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns empty array when tasks.yaml has no tasks", async () => {
|
|
78
|
+
const { readYAML } = await import("../../lib/file-system.js");
|
|
79
|
+
|
|
80
|
+
const mockIndex: TasksIndex = {
|
|
81
|
+
title: "Tasks",
|
|
82
|
+
tasks: [],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
vi.mocked(readYAML).mockResolvedValue(mockIndex);
|
|
86
|
+
|
|
87
|
+
const { getAllTasks } = await import("../../lib/tasks-data.js");
|
|
88
|
+
const result = await getAllTasks();
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns empty array when tasks.yaml is missing or invalid", async () => {
|
|
94
|
+
const { readYAML } = await import("../../lib/file-system.js");
|
|
95
|
+
|
|
96
|
+
vi.mocked(readYAML).mockRejectedValue(new Error("File not found"));
|
|
97
|
+
|
|
98
|
+
const { getAllTasks } = await import("../../lib/tasks-data.js");
|
|
99
|
+
const result = await getAllTasks();
|
|
100
|
+
|
|
101
|
+
expect(result).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 lg:px-8">
|
|
4
|
+
{/* Health Score Skeleton */}
|
|
5
|
+
<div className="h-48 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
|
|
6
|
+
|
|
7
|
+
{/* Progress Section Skeleton */}
|
|
8
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
9
|
+
<div className="h-32 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
|
|
10
|
+
<div className="h-32 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
|
|
11
|
+
<div className="h-32 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
{/* Status Cards Skeleton */}
|
|
15
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
16
|
+
<div className="h-48 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
|
|
17
|
+
<div className="h-48 animate-pulse rounded-lg border border-gray-200 bg-gray-100"></div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { getDashboardData } from "@/lib/dashboard-data";
|
|
3
|
+
import { ProjectHealth } from "@/components/dashboard/project-health";
|
|
4
|
+
import { ProgressSection } from "@/components/dashboard/progress-section";
|
|
5
|
+
import { StatusCards } from "@/components/dashboard/status-cards";
|
|
6
|
+
import { WarningAlerts } from "@/components/dashboard/warning-alerts";
|
|
7
|
+
import { CriticalPathDisplay } from "@/components/dashboard/critical-path-display";
|
|
8
|
+
import Loading from "./loading";
|
|
9
|
+
|
|
10
|
+
async function DashboardContent() {
|
|
11
|
+
const data = await getDashboardData();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 lg:px-8">
|
|
15
|
+
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
|
16
|
+
|
|
17
|
+
{/* Project Health */}
|
|
18
|
+
<ProjectHealth score={data.healthScore} />
|
|
19
|
+
|
|
20
|
+
{/* Progress Section */}
|
|
21
|
+
<ProgressSection
|
|
22
|
+
requirements={data.requirements}
|
|
23
|
+
specifications={data.specifications}
|
|
24
|
+
issues={data.issues}
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
{/* Status Cards */}
|
|
28
|
+
<StatusCards
|
|
29
|
+
requirements={data.requirements}
|
|
30
|
+
specifications={data.specifications}
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
{/* Warnings */}
|
|
34
|
+
{data.warnings.length > 0 && <WarningAlerts warnings={data.warnings} />}
|
|
35
|
+
|
|
36
|
+
{/* Critical Path */}
|
|
37
|
+
{data.criticalPath !== null && (
|
|
38
|
+
<CriticalPathDisplay items={data.criticalPath} />
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function DashboardPage() {
|
|
45
|
+
return (
|
|
46
|
+
<Suspense fallback={<Loading />}>
|
|
47
|
+
<DashboardContent />
|
|
48
|
+
</Suspense>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export default function Error({
|
|
4
|
+
error,
|
|
5
|
+
reset,
|
|
6
|
+
}: {
|
|
7
|
+
error: Error & { digest?: string };
|
|
8
|
+
reset: () => void;
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex flex-col items-center justify-center py-20">
|
|
12
|
+
<h2 className="text-2xl font-bold text-red-600">Something went wrong</h2>
|
|
13
|
+
<p className="mt-2 text-gray-600">{error.message}</p>
|
|
14
|
+
<button
|
|
15
|
+
onClick={reset}
|
|
16
|
+
className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
17
|
+
>
|
|
18
|
+
Try again
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="animate-pulse space-y-4">
|
|
4
|
+
<div className="h-8 w-48 rounded bg-gray-200" />
|
|
5
|
+
<div className="h-10 w-full rounded bg-gray-200" />
|
|
6
|
+
<div className="space-y-2">
|
|
7
|
+
{Array.from({ length: 8 }, (_, i) => (
|
|
8
|
+
<div key={i} className="h-12 w-full rounded bg-gray-200" />
|
|
9
|
+
))}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { getAllFeedbacks } from "@/lib/feedback-data";
|
|
3
|
+
import { getAllRequirements } from "@/lib/data";
|
|
4
|
+
import { getAllSpecifications } from "@/lib/specification-data";
|
|
5
|
+
import { FeedbackClientView } from "@/components/feedback/feedback-client-view";
|
|
6
|
+
import Loading from "./loading";
|
|
7
|
+
|
|
8
|
+
export const metadata = {
|
|
9
|
+
title: "Feedback - Reqord",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const dynamic = "force-dynamic";
|
|
13
|
+
|
|
14
|
+
async function FeedbackContent() {
|
|
15
|
+
const [feedbacks, requirements, specifications] = await Promise.all([
|
|
16
|
+
getAllFeedbacks(),
|
|
17
|
+
getAllRequirements(),
|
|
18
|
+
getAllSpecifications(),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const reqTitleMap: Record<string, string> = Object.fromEntries(
|
|
22
|
+
requirements.map((r) => [r.id, r.title]),
|
|
23
|
+
);
|
|
24
|
+
const specTitleMap: Record<string, string> = Object.fromEntries(
|
|
25
|
+
specifications.map((s) => [s.id, s.title ?? s.id]),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<FeedbackClientView
|
|
30
|
+
feedbacks={feedbacks}
|
|
31
|
+
requirementTitles={reqTitleMap}
|
|
32
|
+
specificationTitles={specTitleMap}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function FeedbackPage() {
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<div className="mb-6">
|
|
41
|
+
<h1 className="text-2xl font-bold">Feedback</h1>
|
|
42
|
+
</div>
|
|
43
|
+
<Suspense fallback={<Loading />}>
|
|
44
|
+
<FeedbackContent />
|
|
45
|
+
</Suspense>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getAllRequirements } from "@/lib/data";
|
|
2
|
+
import { getAllSpecifications } from "@/lib/specification-data";
|
|
3
|
+
import { getAllTasks } from "@/lib/tasks-data";
|
|
4
|
+
import { GraphPageClient } from "@/components/graph/graph-page-client";
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
title: "Dependency Graph - Reqord",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const dynamic = "force-dynamic";
|
|
11
|
+
|
|
12
|
+
export default async function GraphPage() {
|
|
13
|
+
const [requirements, specifications, tasks] = await Promise.all([
|
|
14
|
+
getAllRequirements(),
|
|
15
|
+
getAllSpecifications(),
|
|
16
|
+
getAllTasks(),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const specCountMap: Record<string, number> = {};
|
|
20
|
+
for (const spec of specifications) {
|
|
21
|
+
specCountMap[spec.requirementId] = (specCountMap[spec.requirementId] ?? 0) + 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<GraphPageClient
|
|
26
|
+
requirements={requirements}
|
|
27
|
+
specifications={specifications}
|
|
28
|
+
specCountMap={specCountMap}
|
|
29
|
+
tasks={tasks}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Nav } from "@/components/ui/nav";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Reqord",
|
|
7
|
+
description: "Requirements management UI",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<html lang="ja">
|
|
17
|
+
<body className="bg-gray-50 text-gray-900 antialiased">
|
|
18
|
+
<Nav />
|
|
19
|
+
<main className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
20
|
+
{children}
|
|
21
|
+
</main>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
24
|
+
);
|
|
25
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import {
|
|
3
|
+
getAllRequirements,
|
|
4
|
+
getRequirementById,
|
|
5
|
+
getRequirementDescription,
|
|
6
|
+
} from "@/lib/data";
|
|
7
|
+
import { RequirementForm } from "@/components/requirement/requirement-form";
|
|
8
|
+
|
|
9
|
+
export const dynamic = "force-dynamic";
|
|
10
|
+
|
|
11
|
+
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
|
|
12
|
+
const { id } = await params;
|
|
13
|
+
return { title: `Edit ${id} - Reqord` };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function EditPage({
|
|
17
|
+
params,
|
|
18
|
+
}: {
|
|
19
|
+
params: Promise<{ id: string }>;
|
|
20
|
+
}) {
|
|
21
|
+
const { id } = await params;
|
|
22
|
+
const [requirement, description, allRequirements] = await Promise.all([
|
|
23
|
+
getRequirementById(id),
|
|
24
|
+
getRequirementDescription(id),
|
|
25
|
+
getAllRequirements(),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
if (!requirement) {
|
|
29
|
+
notFound();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<RequirementForm
|
|
34
|
+
mode="edit"
|
|
35
|
+
requirement={requirement}
|
|
36
|
+
description={description}
|
|
37
|
+
allRequirements={allRequirements}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="animate-pulse space-y-4">
|
|
4
|
+
<div className="h-6 w-32 rounded bg-gray-200" />
|
|
5
|
+
<div className="h-8 w-96 rounded bg-gray-200" />
|
|
6
|
+
<div className="flex gap-2">
|
|
7
|
+
<div className="h-6 w-16 rounded-full bg-gray-200" />
|
|
8
|
+
<div className="h-6 w-16 rounded-full bg-gray-200" />
|
|
9
|
+
</div>
|
|
10
|
+
<div className="h-24 w-full rounded bg-gray-200" />
|
|
11
|
+
<div className="h-48 w-full rounded bg-gray-200" />
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex flex-col items-center justify-center py-20">
|
|
6
|
+
<h2 className="text-2xl font-bold text-gray-900">Requirement Not Found</h2>
|
|
7
|
+
<p className="mt-2 text-gray-600">
|
|
8
|
+
The requirement you're looking for does not exist.
|
|
9
|
+
</p>
|
|
10
|
+
<Link
|
|
11
|
+
href="/requirements"
|
|
12
|
+
className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
13
|
+
>
|
|
14
|
+
Back to Requirements
|
|
15
|
+
</Link>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { getRequirementById, getRequirementDescription } from "@/lib/data";
|
|
3
|
+
import { getSpecificationsByRequirementId } from "@/lib/specification-data";
|
|
4
|
+
import { findUnresolvedByArtifactId } from "@/lib/feedback-data";
|
|
5
|
+
import { RequirementDetail } from "@/components/requirement/requirement-detail";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
|
|
9
|
+
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const requirement = await getRequirementById(id);
|
|
12
|
+
if (!requirement) {
|
|
13
|
+
return { title: "Not Found - Reqord" };
|
|
14
|
+
}
|
|
15
|
+
return { title: `${requirement.id}: ${requirement.title} - Reqord` };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default async function RequirementPage({
|
|
19
|
+
params,
|
|
20
|
+
}: {
|
|
21
|
+
params: Promise<{ id: string }>;
|
|
22
|
+
}) {
|
|
23
|
+
const { id } = await params;
|
|
24
|
+
const [requirement, description, specifications, feedbacks] = await Promise.all([
|
|
25
|
+
getRequirementById(id),
|
|
26
|
+
getRequirementDescription(id),
|
|
27
|
+
getSpecificationsByRequirementId(id),
|
|
28
|
+
findUnresolvedByArtifactId(id),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
if (!requirement) {
|
|
32
|
+
notFound();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<RequirementDetail
|
|
37
|
+
requirement={requirement}
|
|
38
|
+
description={description}
|
|
39
|
+
specifications={specifications}
|
|
40
|
+
feedbacks={feedbacks}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="animate-pulse space-y-4">
|
|
4
|
+
<div className="h-8 w-48 rounded bg-gray-200" />
|
|
5
|
+
<div className="h-10 w-full rounded bg-gray-200" />
|
|
6
|
+
<div className="space-y-2">
|
|
7
|
+
{Array.from({ length: 8 }, (_, i) => (
|
|
8
|
+
<div key={i} className="h-12 w-full rounded bg-gray-200" />
|
|
9
|
+
))}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getAllRequirements } from "@/lib/data";
|
|
2
|
+
import { RequirementForm } from "@/components/requirement/requirement-form";
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
title: "New Requirement - Reqord",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
export default async function NewPage() {
|
|
11
|
+
const allRequirements = await getAllRequirements();
|
|
12
|
+
|
|
13
|
+
return <RequirementForm mode="create" allRequirements={allRequirements} />;
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { getAllRequirements } from "@/lib/data";
|
|
4
|
+
import { RequirementTable } from "@/components/requirement/requirement-table";
|
|
5
|
+
import Loading from "./loading";
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
title: "Requirements - Reqord",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
async function RequirementList() {
|
|
14
|
+
const requirements = await getAllRequirements();
|
|
15
|
+
return <RequirementTable requirements={requirements} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function RequirementsPage() {
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<div className="mb-6 flex items-center justify-between">
|
|
22
|
+
<h1 className="text-2xl font-bold">Requirements</h1>
|
|
23
|
+
<Link
|
|
24
|
+
href="/requirements/new"
|
|
25
|
+
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
26
|
+
>
|
|
27
|
+
New Requirement
|
|
28
|
+
</Link>
|
|
29
|
+
</div>
|
|
30
|
+
<Suspense fallback={<Loading />}>
|
|
31
|
+
<RequirementList />
|
|
32
|
+
</Suspense>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="animate-pulse space-y-4">
|
|
4
|
+
<div className="h-6 w-32 rounded bg-gray-200" />
|
|
5
|
+
<div className="h-8 w-96 rounded bg-gray-200" />
|
|
6
|
+
<div className="flex gap-2">
|
|
7
|
+
<div className="h-6 w-16 rounded-full bg-gray-200" />
|
|
8
|
+
<div className="h-6 w-16 rounded-full bg-gray-200" />
|
|
9
|
+
</div>
|
|
10
|
+
<div className="h-24 w-full rounded bg-gray-200" />
|
|
11
|
+
<div className="h-48 w-full rounded bg-gray-200" />
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex flex-col items-center justify-center py-20">
|
|
6
|
+
<h2 className="text-2xl font-bold text-gray-900">Specification Not Found</h2>
|
|
7
|
+
<p className="mt-2 text-gray-600">
|
|
8
|
+
The specification you're looking for does not exist.
|
|
9
|
+
</p>
|
|
10
|
+
<Link
|
|
11
|
+
href="/specifications"
|
|
12
|
+
className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
13
|
+
>
|
|
14
|
+
Back to Specifications
|
|
15
|
+
</Link>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { getSpecificationById } from "@/lib/specification-data";
|
|
3
|
+
import { getRequirementById } from "@/lib/data";
|
|
4
|
+
import { loadSpecFile } from "@/lib/specification-file";
|
|
5
|
+
import { findUnresolvedByArtifactId } from "@/lib/feedback-data";
|
|
6
|
+
import { SpecDetail } from "@/components/specification/spec-detail";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const specification = await getSpecificationById(id);
|
|
13
|
+
if (!specification) {
|
|
14
|
+
return { title: "Not Found - Reqord" };
|
|
15
|
+
}
|
|
16
|
+
const title = specification.title
|
|
17
|
+
? `${specification.id}: ${specification.title} - Reqord`
|
|
18
|
+
: `${specification.id} - Reqord`;
|
|
19
|
+
return { title };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default async function SpecificationPage({
|
|
23
|
+
params,
|
|
24
|
+
}: {
|
|
25
|
+
params: Promise<{ id: string }>;
|
|
26
|
+
}) {
|
|
27
|
+
const { id } = await params;
|
|
28
|
+
const [specification, design, research] = await Promise.all([
|
|
29
|
+
getSpecificationById(id),
|
|
30
|
+
loadSpecFile(id, "design.md"),
|
|
31
|
+
loadSpecFile(id, "research.md"),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
if (!specification) {
|
|
35
|
+
notFound();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [requirement, feedbacks] = await Promise.all([
|
|
39
|
+
getRequirementById(specification.requirementId),
|
|
40
|
+
findUnresolvedByArtifactId(id),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<SpecDetail
|
|
45
|
+
specification={specification}
|
|
46
|
+
design={design}
|
|
47
|
+
research={research}
|
|
48
|
+
requirement={requirement}
|
|
49
|
+
feedbacks={feedbacks}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="animate-pulse space-y-4">
|
|
4
|
+
<div className="h-8 w-48 rounded bg-gray-200" />
|
|
5
|
+
<div className="h-10 w-full rounded bg-gray-200" />
|
|
6
|
+
<div className="space-y-2">
|
|
7
|
+
{Array.from({ length: 8 }, (_, i) => (
|
|
8
|
+
<div key={i} className="h-12 w-full rounded bg-gray-200" />
|
|
9
|
+
))}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { getAllSpecifications } from "@/lib/specification-data";
|
|
3
|
+
import { getAllRequirements } from "@/lib/data";
|
|
4
|
+
import { SpecificationTable } from "@/components/specification/specification-table";
|
|
5
|
+
import Loading from "./loading";
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
title: "Specifications - Reqord",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
async function SpecificationList() {
|
|
14
|
+
const [specifications, requirements] = await Promise.all([
|
|
15
|
+
getAllSpecifications(),
|
|
16
|
+
getAllRequirements(),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const requirementMap = Object.fromEntries(
|
|
20
|
+
requirements.map((r) => [r.id, r.title]),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<SpecificationTable
|
|
25
|
+
specifications={specifications}
|
|
26
|
+
requirementTitleMap={requirementMap}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function SpecificationsPage() {
|
|
32
|
+
return (
|
|
33
|
+
<div>
|
|
34
|
+
<div className="mb-6">
|
|
35
|
+
<h1 className="text-2xl font-bold">Specifications</h1>
|
|
36
|
+
</div>
|
|
37
|
+
<Suspense fallback={<Loading />}>
|
|
38
|
+
<SpecificationList />
|
|
39
|
+
</Suspense>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|