@interactive-inc/docs-client 0.0.1
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/.tanstack/tmp/router-generator-Eq20Yx/50b11a0f488e61cf92364a7e585c1448 +123 -0
- package/.tanstack/tmp/router-generator-KQIYSa/aecb094e4891f9475b91de590abc8809 +117 -0
- package/.tanstack/tmp/router-generator-ekW2pI/aecb094e4891f9475b91de590abc8809 +117 -0
- package/app/components/archived-file-list-view.tsx +99 -0
- package/app/components/directory-file-list-view.tsx +32 -0
- package/app/components/directory-file-tree-node.tsx +96 -0
- package/app/components/directory-layout-sidebar.tsx +181 -0
- package/app/components/directory-page-view.tsx +208 -0
- package/app/components/directory-table-view.tsx +383 -0
- package/app/components/editable-table-cell/boolean-editable-cell.tsx +44 -0
- package/app/components/editable-table-cell/multi-boolean-editable-cell.tsx +59 -0
- package/app/components/editable-table-cell/multi-number-editable-cell.tsx +62 -0
- package/app/components/editable-table-cell/multi-relation-editable-cell.tsx +19 -0
- package/app/components/editable-table-cell/multi-select-number-editable-cell.tsx +82 -0
- package/app/components/editable-table-cell/multi-select-text-editable-cell.tsx +82 -0
- package/app/components/editable-table-cell/multi-text-editable-cell.tsx +62 -0
- package/app/components/editable-table-cell/number-editable-cell.tsx +54 -0
- package/app/components/editable-table-cell/relation-editable-cell.tsx +20 -0
- package/app/components/editable-table-cell/select-number-editable-cell.tsx +43 -0
- package/app/components/editable-table-cell/select-text-editable-cell.tsx +33 -0
- package/app/components/editable-table-cell/string-editable-cell.tsx +42 -0
- package/app/components/editable-table-cell.tsx +129 -0
- package/app/components/file-card.tsx +120 -0
- package/app/components/file-tree-icon.tsx +26 -0
- package/app/components/file-view/archive-restore-banner.tsx +86 -0
- package/app/components/file-view/csv-file-view.tsx +50 -0
- package/app/components/file-view/default-file-view.tsx +11 -0
- package/app/components/file-view/editable-csv-cell.tsx +37 -0
- package/app/components/file-view/editable-front-matter-view.tsx +105 -0
- package/app/components/file-view/file-csv-columns-view.tsx +181 -0
- package/app/components/file-view/file-csv-table.tsx +142 -0
- package/app/components/file-view/file-header.tsx +115 -0
- package/app/components/file-view/file-page-view.tsx +165 -0
- package/app/components/file-view/front-matter-input-field.tsx +157 -0
- package/app/components/file-view/front-matter-view.tsx +30 -0
- package/app/components/file-view/json-file-editor.tsx +38 -0
- package/app/components/file-view/markdown-file-view.tsx +71 -0
- package/app/components/file-view/multi-relation-select.tsx +129 -0
- package/app/components/file-view/render-front-matter-value.tsx +57 -0
- package/app/components/file-view/single-relation-select.tsx +41 -0
- package/app/components/file-view/sortable-csv-row.tsx +57 -0
- package/app/components/file-view/sortable-header.tsx +30 -0
- package/app/components/file-view/utils.ts +8 -0
- package/app/components/milestones-editor.tsx +95 -0
- package/app/components/page-header.tsx +20 -0
- package/app/components/project-view/feature-item.tsx +139 -0
- package/app/components/project-view/project-page-group.tsx +125 -0
- package/app/components/project-view/project-view.tsx +230 -0
- package/app/components/project-view/unlinked-features-section.tsx +73 -0
- package/app/components/providers.tsx +21 -0
- package/app/components/reload-button.tsx +25 -0
- package/app/components/sidebar-button.tsx +13 -0
- package/app/components/ui/alert.tsx +66 -0
- package/app/components/ui/badge.tsx +46 -0
- package/app/components/ui/button.tsx +59 -0
- package/app/components/ui/card.tsx +92 -0
- package/app/components/ui/dialog.tsx +133 -0
- package/app/components/ui/emoji-picker.tsx +133 -0
- package/app/components/ui/icons.tsx +3 -0
- package/app/components/ui/input.tsx +21 -0
- package/app/components/ui/select.tsx +183 -0
- package/app/components/ui/separator.tsx +26 -0
- package/app/components/ui/sheet.tsx +137 -0
- package/app/components/ui/sidebar.tsx +724 -0
- package/app/components/ui/simple-sidebar.tsx +50 -0
- package/app/components/ui/skeleton.tsx +13 -0
- package/app/components/ui/sonner.tsx +23 -0
- package/app/components/ui/table.tsx +114 -0
- package/app/components/ui/tabs.tsx +53 -0
- package/app/components/ui/textarea.tsx +18 -0
- package/app/components/ui/tooltip.tsx +59 -0
- package/app/components/vscode-button.tsx +23 -0
- package/app/hooks/use-directory-query.ts +22 -0
- package/app/hooks/use-file-properties-mutation.ts +38 -0
- package/app/hooks/use-file-query.ts +21 -0
- package/app/hooks/use-mobile.ts +19 -0
- package/app/main.css +122 -0
- package/app/main.tsx +38 -0
- package/app/routes/$.index.tsx +25 -0
- package/app/routes/$.tsx +20 -0
- package/app/routes/__root.tsx +20 -0
- package/app/routes/_index.tsx +17 -0
- package/app/routes/apps.client.$.tsx +21 -0
- package/build/assets/index.css +1 -0
- package/build/assets/index.js +298 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +23 -0
- package/components.json +21 -0
- package/package.json +54 -0
- package/tsconfig.index.json +17 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useSortable } from "@dnd-kit/sortable"
|
|
2
|
+
import { CSS } from "@dnd-kit/utilities"
|
|
3
|
+
import { GripVertical } from "lucide-react"
|
|
4
|
+
import { TableCell, TableRow } from "@/components/ui/table"
|
|
5
|
+
import { EditableCsvCell } from "./editable-csv-cell"
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
id: string
|
|
9
|
+
row: string[]
|
|
10
|
+
rowIndex: number
|
|
11
|
+
onCellUpdate: (rowIndex: number, columnIndex: number, value: string) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SortableCsvRow(props: Props) {
|
|
15
|
+
const sortable = useSortable({
|
|
16
|
+
id: props.id,
|
|
17
|
+
attributes: {
|
|
18
|
+
role: "button",
|
|
19
|
+
tabIndex: 0,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const style = {
|
|
24
|
+
transform: CSS.Transform.toString(sortable.transform),
|
|
25
|
+
opacity: sortable.isDragging ? 0.5 : 1,
|
|
26
|
+
touchAction: "none",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleCellChange(
|
|
30
|
+
rowIndex: number,
|
|
31
|
+
columnIndex: number,
|
|
32
|
+
value: string,
|
|
33
|
+
) {
|
|
34
|
+
props.onCellUpdate(rowIndex, columnIndex, value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<TableRow
|
|
39
|
+
ref={sortable.setNodeRef}
|
|
40
|
+
style={style}
|
|
41
|
+
className="hover:opacity-60"
|
|
42
|
+
>
|
|
43
|
+
<TableCell {...sortable.attributes} {...sortable.listeners}>
|
|
44
|
+
<GripVertical className="size-4" />
|
|
45
|
+
</TableCell>
|
|
46
|
+
{props.row.map((cell: string, cellIndex: number) => (
|
|
47
|
+
<EditableCsvCell
|
|
48
|
+
key={`cell-${props.rowIndex}-${cellIndex}-${cell}`}
|
|
49
|
+
content={cell}
|
|
50
|
+
rowIndex={props.rowIndex}
|
|
51
|
+
columnIndex={cellIndex}
|
|
52
|
+
onCellChange={handleCellChange}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</TableRow>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useSortable } from "@dnd-kit/sortable"
|
|
2
|
+
import { CSS } from "@dnd-kit/utilities"
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
id: string
|
|
6
|
+
index: number
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CsvSortableHeader({ id, index, children }: Props) {
|
|
11
|
+
const { attributes, listeners, setNodeRef, transform, transition } =
|
|
12
|
+
useSortable({ id })
|
|
13
|
+
|
|
14
|
+
const style = {
|
|
15
|
+
transform: CSS.Transform.toString(transform),
|
|
16
|
+
transition,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<th
|
|
21
|
+
ref={setNodeRef}
|
|
22
|
+
style={style}
|
|
23
|
+
{...attributes}
|
|
24
|
+
{...listeners}
|
|
25
|
+
className="cursor-grab border border-gray-600 p-2 text-sm"
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</th>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useSuspenseQuery } from "@tanstack/react-query"
|
|
2
|
+
import { Card } from "@/components/ui/card"
|
|
3
|
+
import { apiClient } from "@/lib/api-client"
|
|
4
|
+
import type { DocFile, DocFileMd } from "@/lib/types"
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
project: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function MilestonesEditor(props: Props) {
|
|
11
|
+
const { data: milestonesData } = useSuspenseQuery({
|
|
12
|
+
queryKey: ["directories", `products/${props.project}/milestones`],
|
|
13
|
+
queryFn: async () => {
|
|
14
|
+
const response = await apiClient.api.directories[":path{.+}"].$get({
|
|
15
|
+
param: { path: `products/${props.project}/milestones` },
|
|
16
|
+
})
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new Error("Failed to fetch milestones")
|
|
19
|
+
}
|
|
20
|
+
return response.json()
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const { data: featuresData } = useSuspenseQuery({
|
|
25
|
+
queryKey: ["directories", `products/${props.project}/features`],
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
const response = await apiClient.api.directories[":path{.+}"].$get({
|
|
28
|
+
param: { path: `products/${props.project}/features` },
|
|
29
|
+
})
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error("Failed to fetch features")
|
|
32
|
+
}
|
|
33
|
+
return response.json()
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function isDocFileMd(file: DocFile): file is DocFileMd {
|
|
38
|
+
return file.type === 'markdown'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasMarkdownContent(file: { content: unknown }): file is { content: { title: string; description: string; frontMatter: Record<string, unknown> } } {
|
|
42
|
+
return typeof file.content === 'object' && file.content !== null && 'frontMatter' in file.content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const milestones = (milestonesData.files || []).filter(isDocFileMd)
|
|
46
|
+
const features = (featuresData.files || []).filter(isDocFileMd)
|
|
47
|
+
|
|
48
|
+
function getFeaturesByMilestone(milestoneId: string) {
|
|
49
|
+
return features.filter(
|
|
50
|
+
(feature) => {
|
|
51
|
+
if (!hasMarkdownContent(feature)) return false
|
|
52
|
+
const frontMatter = feature.content.frontMatter
|
|
53
|
+
return typeof frontMatter === 'object' && frontMatter !== null && 'milestone' in frontMatter && frontMatter.milestone === milestoneId
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="grid gap-2">
|
|
60
|
+
{milestones.map((milestone) => {
|
|
61
|
+
const milestoneFeatures = getFeaturesByMilestone(milestone.path.name)
|
|
62
|
+
return (
|
|
63
|
+
<Card key={milestone.path.name} className="gap-2 overflow-hidden p-2">
|
|
64
|
+
<div className="space-y-1">
|
|
65
|
+
<h2 className="font-semibold text-xl">
|
|
66
|
+
{hasMarkdownContent(milestone) ? milestone.content.title || milestone.path.name : milestone.path.name}
|
|
67
|
+
</h2>
|
|
68
|
+
<p className="text-muted-foreground text-sm">
|
|
69
|
+
{hasMarkdownContent(milestone) ? milestone.content.description : ''}
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
{milestoneFeatures.length > 0 ? (
|
|
73
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
74
|
+
{milestoneFeatures.map((feature) => (
|
|
75
|
+
<div key={feature.path.name} className="rounded border p-2">
|
|
76
|
+
<h3 className="font-medium">
|
|
77
|
+
{hasMarkdownContent(feature) ? feature.content.title || feature.path.name : feature.path.name}
|
|
78
|
+
</h3>
|
|
79
|
+
<p className="text-muted-foreground text-sm">
|
|
80
|
+
{hasMarkdownContent(feature) ? feature.content.description : ''}
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
) : (
|
|
86
|
+
<p className="py-4 text-center text-muted-foreground">
|
|
87
|
+
このマイルストーンに関連する機能はありません
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
</Card>
|
|
91
|
+
)
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { SidebarButton } from "@/components/sidebar-button"
|
|
3
|
+
import { Input } from "@/components/ui/input"
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
filePath: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function PageHeader(props: Props): ReactNode {
|
|
10
|
+
const paths = props.filePath.split("/")
|
|
11
|
+
|
|
12
|
+
const fileName = paths[paths.length - 1]
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-1 items-center gap-2">
|
|
16
|
+
<SidebarButton />
|
|
17
|
+
<Input value={fileName} readOnly />
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { CheckCircle, X } from "lucide-react"
|
|
2
|
+
import { SingleRelationSelect } from "@/components/file-view/single-relation-select"
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
import type { DocFile, DocFileMd, DocRelationFile } from "@/lib/types"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function isDocFileMd(file: DocFile): file is DocFileMd {
|
|
8
|
+
return file.type === 'markdown'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Priority = "high" | "medium" | "low"
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
feature: DocFile
|
|
15
|
+
milestoneOptions?: DocRelationFile[]
|
|
16
|
+
onMilestoneUpdate?: (featurePath: string, milestone: string) => void
|
|
17
|
+
onPropertyUpdate?: (
|
|
18
|
+
featurePath: string,
|
|
19
|
+
field: string,
|
|
20
|
+
value: unknown,
|
|
21
|
+
) => void
|
|
22
|
+
onFeatureRemove?: (featurePath: string) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function FeatureItem(props: Props) {
|
|
26
|
+
if (!isDocFileMd(props.feature)) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const frontMatter = props.feature.content.frontMatter as Record<string, unknown>
|
|
31
|
+
|
|
32
|
+
const isDone = (frontMatter?.["is-done"] as boolean) === true
|
|
33
|
+
|
|
34
|
+
const priorityNumber = (frontMatter?.priority as number) || 0
|
|
35
|
+
|
|
36
|
+
const priority =
|
|
37
|
+
priorityNumber === 2 ? "high" : priorityNumber === 1 ? "medium" : "low"
|
|
38
|
+
|
|
39
|
+
const milestone = frontMatter?.milestone as string
|
|
40
|
+
|
|
41
|
+
const getCurrentValue = () => {
|
|
42
|
+
const matchingOption = props.milestoneOptions?.find(
|
|
43
|
+
(option) => option.label === milestone,
|
|
44
|
+
)
|
|
45
|
+
return matchingOption?.name || ""
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleMilestoneChange = (value: string) => {
|
|
49
|
+
if (!value) {
|
|
50
|
+
props.onMilestoneUpdate?.(props.feature.path.path, "")
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const selectedOption = props.milestoneOptions?.find(
|
|
55
|
+
(option) => option.name === value,
|
|
56
|
+
)
|
|
57
|
+
const labelToSave = selectedOption?.label || value
|
|
58
|
+
props.onMilestoneUpdate?.(props.feature.path.path, labelToSave)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleDoneToggle = () => {
|
|
62
|
+
props.onPropertyUpdate?.(props.feature.path.path, "is-done", !isDone)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handlePriorityClick = () => {
|
|
66
|
+
const currentValue = (frontMatter?.priority as number) || 0
|
|
67
|
+
const nextValue = (currentValue + 1) % 3
|
|
68
|
+
props.onPropertyUpdate?.(props.feature.path.path, "priority", nextValue)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const getPriorityColor = (priority: Priority): string => {
|
|
72
|
+
switch (priority) {
|
|
73
|
+
case "high":
|
|
74
|
+
return "border-red-500"
|
|
75
|
+
case "medium":
|
|
76
|
+
return "border-yellow-500"
|
|
77
|
+
case "low":
|
|
78
|
+
return "border-green-500"
|
|
79
|
+
default:
|
|
80
|
+
return "border-gray-500"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const getPriorityLabel = (priority: Priority): string => {
|
|
85
|
+
if (priority === "high") return "高"
|
|
86
|
+
if (priority === "medium") return "中"
|
|
87
|
+
return "低"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className={cn("space-y-2 rounded-md border p-2", isDone && "opacity-75")}
|
|
93
|
+
>
|
|
94
|
+
<div className="flex items-center gap-3">
|
|
95
|
+
<Button
|
|
96
|
+
variant={isDone ? "default" : "secondary"}
|
|
97
|
+
size="sm"
|
|
98
|
+
onClick={handleDoneToggle}
|
|
99
|
+
>
|
|
100
|
+
<CheckCircle className="size-4" />
|
|
101
|
+
</Button>
|
|
102
|
+
<h3 className={`font-bold ${isDone ? "line-through opacity-80" : ""}`}>
|
|
103
|
+
{props.feature.content.title || props.feature.path.name}
|
|
104
|
+
</h3>
|
|
105
|
+
</div>
|
|
106
|
+
{props.feature.content.description && (
|
|
107
|
+
<p className="text-sm opacity-80">{props.feature.content.description}</p>
|
|
108
|
+
)}
|
|
109
|
+
<div className="flex flex-col items-end justify-between gap-2 lg:flex-row">
|
|
110
|
+
<div className="text-xs opacity-50">{props.feature.path.name}</div>
|
|
111
|
+
<div className="flex gap-2">
|
|
112
|
+
<SingleRelationSelect
|
|
113
|
+
value={getCurrentValue()}
|
|
114
|
+
relationOptions={props.milestoneOptions}
|
|
115
|
+
onValueChange={handleMilestoneChange}
|
|
116
|
+
/>
|
|
117
|
+
<Button
|
|
118
|
+
variant="outline"
|
|
119
|
+
className={cn("w-8", getPriorityColor(priority))}
|
|
120
|
+
onClick={handlePriorityClick}
|
|
121
|
+
>
|
|
122
|
+
{getPriorityLabel(priority)}
|
|
123
|
+
</Button>
|
|
124
|
+
{props.onFeatureRemove && (
|
|
125
|
+
<Button
|
|
126
|
+
variant="outline"
|
|
127
|
+
className="w-8"
|
|
128
|
+
onClick={() => {
|
|
129
|
+
props.onFeatureRemove?.(props.feature.path.path)
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<X className="size-3" />
|
|
133
|
+
</Button>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Card } from "@/components/ui/card"
|
|
2
|
+
import {
|
|
3
|
+
Select,
|
|
4
|
+
SelectContent,
|
|
5
|
+
SelectItem,
|
|
6
|
+
SelectTrigger,
|
|
7
|
+
SelectValue,
|
|
8
|
+
} from "@/components/ui/select"
|
|
9
|
+
import type { DocFile, DocFileMd, DocRelationFile } from "@/lib/types"
|
|
10
|
+
import { FeatureItem } from "./feature-item"
|
|
11
|
+
|
|
12
|
+
function isDocFileMd(file: DocFile): file is DocFileMd {
|
|
13
|
+
return file.type === 'markdown'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Priority = "high" | "medium" | "low"
|
|
17
|
+
|
|
18
|
+
type PageGroup = {
|
|
19
|
+
page: DocFile
|
|
20
|
+
features: DocFile[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
group: PageGroup
|
|
25
|
+
milestoneOptions?: DocRelationFile[]
|
|
26
|
+
onMilestoneUpdate?: (featurePath: string, milestone: string) => void
|
|
27
|
+
onPropertyUpdate?: (
|
|
28
|
+
featurePath: string,
|
|
29
|
+
field: string,
|
|
30
|
+
value: unknown,
|
|
31
|
+
) => void
|
|
32
|
+
allFeatures?: DocFile[]
|
|
33
|
+
onFeatureAdd?: (pagePath: string, featurePath: string) => void
|
|
34
|
+
onFeatureRemove?: (pagePath: string, featurePath: string) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ProjectPageGroup(props: Props) {
|
|
38
|
+
const sortedFeatures = props.group.features
|
|
39
|
+
.filter(isDocFileMd)
|
|
40
|
+
.sort((a, b) => {
|
|
41
|
+
const priorityOrder: Record<Priority, number> = {
|
|
42
|
+
high: 0,
|
|
43
|
+
medium: 1,
|
|
44
|
+
low: 2,
|
|
45
|
+
}
|
|
46
|
+
const aPriority =
|
|
47
|
+
((a.content.frontMatter as Record<string, unknown>)?.priority as Priority) ||
|
|
48
|
+
"low"
|
|
49
|
+
const bPriority =
|
|
50
|
+
((b.content.frontMatter as Record<string, unknown>)?.priority as Priority) ||
|
|
51
|
+
"low"
|
|
52
|
+
return (priorityOrder[aPriority] || 2) - (priorityOrder[bPriority] || 2)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// このページに関連付けられていない機能を取得
|
|
56
|
+
const linkedFeaturePaths = props.group.features.map((f) => f.path.path)
|
|
57
|
+
const availableFeatures = (props.allFeatures || [])
|
|
58
|
+
.filter(isDocFileMd)
|
|
59
|
+
.filter((feature) => !linkedFeaturePaths.includes(feature.path.path))
|
|
60
|
+
|
|
61
|
+
const handleFeatureAdd = (featureValue: string) => {
|
|
62
|
+
if (featureValue && props.onFeatureAdd) {
|
|
63
|
+
props.onFeatureAdd(props.group.page.path.path, featureValue)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Card className="w-full rounded-md p-2">
|
|
69
|
+
<div className="flex flex-col gap-2 lg:flex-row">
|
|
70
|
+
{/* 左側: ページ情報 */}
|
|
71
|
+
<div className="space-y-2 lg:w-1/3">
|
|
72
|
+
<h2 className="font-semibold text-lg">
|
|
73
|
+
{isDocFileMd(props.group.page) ? props.group.page.content.title || props.group.page.path.name : props.group.page.path.name}
|
|
74
|
+
</h2>
|
|
75
|
+
<div className="text-xs opacity-50">{props.group.page.path.name}</div>
|
|
76
|
+
{isDocFileMd(props.group.page) && props.group.page.content.description && (
|
|
77
|
+
<p className="text-sm opacity-50">{props.group.page.content.description}</p>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
{/* 右側: 関連機能一覧 */}
|
|
81
|
+
<div className="flex-1">
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
{sortedFeatures.length > 0 ? (
|
|
84
|
+
sortedFeatures.map((feature) => (
|
|
85
|
+
<FeatureItem
|
|
86
|
+
key={feature.path.name}
|
|
87
|
+
feature={feature}
|
|
88
|
+
milestoneOptions={props.milestoneOptions}
|
|
89
|
+
onMilestoneUpdate={props.onMilestoneUpdate}
|
|
90
|
+
onPropertyUpdate={props.onPropertyUpdate}
|
|
91
|
+
onFeatureRemove={(featurePath) =>
|
|
92
|
+
props.onFeatureRemove?.(
|
|
93
|
+
props.group.page.path.path,
|
|
94
|
+
featurePath,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
))
|
|
99
|
+
) : (
|
|
100
|
+
<p className="text-sm italic opacity-50">
|
|
101
|
+
このページに関連する機能はありません
|
|
102
|
+
</p>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* 機能追加用Select */}
|
|
106
|
+
{availableFeatures.length > 0 && (
|
|
107
|
+
<Select value="" onValueChange={handleFeatureAdd}>
|
|
108
|
+
<SelectTrigger className="w-full">
|
|
109
|
+
<SelectValue placeholder="機能を追加..." />
|
|
110
|
+
</SelectTrigger>
|
|
111
|
+
<SelectContent>
|
|
112
|
+
{availableFeatures.map((feature) => (
|
|
113
|
+
<SelectItem key={feature.path.path} value={feature.path.path || ""}>
|
|
114
|
+
{feature.content.title || feature.path.name}
|
|
115
|
+
</SelectItem>
|
|
116
|
+
))}
|
|
117
|
+
</SelectContent>
|
|
118
|
+
</Select>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</Card>
|
|
124
|
+
)
|
|
125
|
+
}
|