@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,165 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { CsvFileView } from "@/components/file-view/csv-file-view"
|
|
3
|
+
import { DefaultFileViewer } from "@/components/file-view/default-file-view"
|
|
4
|
+
import { JsonFileEditor } from "@/components/file-view/json-file-editor"
|
|
5
|
+
import { MarkdownFileView } from "@/components/file-view/markdown-file-view"
|
|
6
|
+
import { useDirectoryQuery } from "@/hooks/use-directory-query"
|
|
7
|
+
import { useFilePropertiesMutation } from "@/hooks/use-file-properties-mutation"
|
|
8
|
+
import { useFileQuery } from "@/hooks/use-file-query"
|
|
9
|
+
import { apiClient } from "@/lib/api-client"
|
|
10
|
+
import { normalizePath } from "@/lib/path-utils"
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
filePath: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function FilePageView(props: Props) {
|
|
17
|
+
const fileQuery = useFileQuery(props.filePath)
|
|
18
|
+
|
|
19
|
+
const directoryQuery = useDirectoryQuery(props.filePath)
|
|
20
|
+
|
|
21
|
+
const fileData = fileQuery.data
|
|
22
|
+
|
|
23
|
+
const directorySchema =
|
|
24
|
+
directoryQuery.data?.indexFile?.content?.frontMatter?.schema || {}
|
|
25
|
+
|
|
26
|
+
const relations = directoryQuery.data?.relations || []
|
|
27
|
+
|
|
28
|
+
// contentがオブジェクトの場合はbodyプロパティを使用
|
|
29
|
+
const initialContent = typeof fileData?.content === 'string'
|
|
30
|
+
? fileData.content
|
|
31
|
+
: fileData?.content?.body || ""
|
|
32
|
+
const [currentContent, setCurrentContent] = useState(initialContent)
|
|
33
|
+
|
|
34
|
+
const updateProperties = useFilePropertiesMutation()
|
|
35
|
+
|
|
36
|
+
const onChange = async (newContent: string) => {
|
|
37
|
+
const normalizedPath = normalizePath(props.filePath)
|
|
38
|
+
|
|
39
|
+
const result = await apiClient.api.files[":path{.+}"].$put({
|
|
40
|
+
param: { path: normalizedPath },
|
|
41
|
+
json: {
|
|
42
|
+
title: null,
|
|
43
|
+
properties: null,
|
|
44
|
+
content: newContent,
|
|
45
|
+
description: null,
|
|
46
|
+
isArchived: null,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const data = await result.json()
|
|
51
|
+
if ("content" in data && typeof data.content === "string") {
|
|
52
|
+
setCurrentContent(data.content)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleReload = async () => {
|
|
57
|
+
const result = await fileQuery.refetch()
|
|
58
|
+
if (result.data === undefined) return
|
|
59
|
+
const content = typeof result.data.content === 'string'
|
|
60
|
+
? result.data.content
|
|
61
|
+
: result.data.content?.body || ""
|
|
62
|
+
setCurrentContent(content)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleFrontMatterUpdate = async (key: string, value: unknown) => {
|
|
66
|
+
await updateProperties.mutateAsync({
|
|
67
|
+
path: props.filePath,
|
|
68
|
+
field: key,
|
|
69
|
+
value: value,
|
|
70
|
+
})
|
|
71
|
+
// フロントマター更新後にファイル内容を再取得
|
|
72
|
+
handleReload()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!fileData) {
|
|
76
|
+
return <div>ファイルが見つかりません</div>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// pathオブジェクトから実際のパス文字列を取得
|
|
80
|
+
const filePath = typeof fileData.path === 'string' ? fileData.path : fileData.path?.path
|
|
81
|
+
|
|
82
|
+
if (filePath?.endsWith(".md")) {
|
|
83
|
+
return (
|
|
84
|
+
<main className="p-2">
|
|
85
|
+
<MarkdownFileView
|
|
86
|
+
filePath={props.filePath}
|
|
87
|
+
fileData={{ path: filePath, title: fileData.content.title || null }}
|
|
88
|
+
cwd={directoryQuery.data.cwd}
|
|
89
|
+
content={currentContent}
|
|
90
|
+
onChange={onChange}
|
|
91
|
+
frontMatter={(() => {
|
|
92
|
+
// frontMatterはcontent内にある
|
|
93
|
+
const fm = fileData.content.frontMatter || {}
|
|
94
|
+
// スキーマやアイコンなどの複雑なオブジェクトを除外し、プリミティブ値のみを返す
|
|
95
|
+
const filtered: Record<
|
|
96
|
+
string,
|
|
97
|
+
string | number | boolean | string[] | number[] | boolean[] | null
|
|
98
|
+
> = {}
|
|
99
|
+
for (const [key, value] of Object.entries(fm)) {
|
|
100
|
+
if (key === "schema" || key === "icon") continue
|
|
101
|
+
if (
|
|
102
|
+
typeof value === "string" ||
|
|
103
|
+
typeof value === "number" ||
|
|
104
|
+
typeof value === "boolean" ||
|
|
105
|
+
value === null ||
|
|
106
|
+
(Array.isArray(value) &&
|
|
107
|
+
value.every(
|
|
108
|
+
(v) =>
|
|
109
|
+
typeof v === "string" ||
|
|
110
|
+
typeof v === "number" ||
|
|
111
|
+
typeof v === "boolean",
|
|
112
|
+
))
|
|
113
|
+
) {
|
|
114
|
+
filtered[key] = value as
|
|
115
|
+
| string
|
|
116
|
+
| number
|
|
117
|
+
| boolean
|
|
118
|
+
| string[]
|
|
119
|
+
| number[]
|
|
120
|
+
| boolean[]
|
|
121
|
+
| null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return filtered
|
|
125
|
+
})()}
|
|
126
|
+
onFrontMatterUpdate={handleFrontMatterUpdate}
|
|
127
|
+
onReload={handleReload}
|
|
128
|
+
isLoading={fileQuery.isLoading}
|
|
129
|
+
schema={directorySchema}
|
|
130
|
+
relations={relations}
|
|
131
|
+
/>
|
|
132
|
+
</main>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (filePath?.endsWith(".csv")) {
|
|
137
|
+
return (
|
|
138
|
+
<main className="p-2">
|
|
139
|
+
<CsvFileView
|
|
140
|
+
filePath={props.filePath}
|
|
141
|
+
fileData={{ path: filePath, title: fileData.content.title || null }}
|
|
142
|
+
cwd={directoryQuery.data.cwd}
|
|
143
|
+
content={currentContent}
|
|
144
|
+
onChange={onChange}
|
|
145
|
+
onReload={handleReload}
|
|
146
|
+
isLoading={fileQuery.isLoading}
|
|
147
|
+
/>
|
|
148
|
+
</main>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (filePath?.endsWith(".json")) {
|
|
153
|
+
return (
|
|
154
|
+
<main className="p-2">
|
|
155
|
+
<JsonFileEditor content={currentContent} />
|
|
156
|
+
</main>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<main className="p-2">
|
|
162
|
+
<DefaultFileViewer content={currentContent} />
|
|
163
|
+
</main>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { Input } from "@/components/ui/input"
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "@/components/ui/select"
|
|
10
|
+
import type { DocRelationFile, DocSchemaField } from "@/lib/types"
|
|
11
|
+
import { MultiRelationSelect } from "./multi-relation-select"
|
|
12
|
+
import { SingleRelationSelect } from "./single-relation-select"
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
fieldKey: string
|
|
16
|
+
value: unknown
|
|
17
|
+
originalValue: unknown
|
|
18
|
+
onValueChange: (key: string, value: string) => void
|
|
19
|
+
onBlur: (key: string, value: string) => void
|
|
20
|
+
schemaField?: DocSchemaField
|
|
21
|
+
relationOptions?: DocRelationFile[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Front Matter用の入力フィールドを表示するコンポーネント
|
|
26
|
+
*/
|
|
27
|
+
export function FrontMatterInputField(props: Props) {
|
|
28
|
+
const [localValue, setLocalValue] = useState(() => {
|
|
29
|
+
if (Array.isArray(props.value)) {
|
|
30
|
+
return props.value.join(", ")
|
|
31
|
+
}
|
|
32
|
+
return String(props.value)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (Array.isArray(props.value)) {
|
|
37
|
+
setLocalValue(props.value.join(", "))
|
|
38
|
+
} else {
|
|
39
|
+
setLocalValue(String(props.value))
|
|
40
|
+
}
|
|
41
|
+
}, [props.value])
|
|
42
|
+
|
|
43
|
+
const _getDisplayValue = (value: unknown): string => {
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value.join(", ")
|
|
46
|
+
}
|
|
47
|
+
return String(value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// リレーション型の場合
|
|
51
|
+
if (
|
|
52
|
+
props.schemaField &&
|
|
53
|
+
props.schemaField.type === "relation" &&
|
|
54
|
+
props.schemaField.path
|
|
55
|
+
) {
|
|
56
|
+
return (
|
|
57
|
+
<SingleRelationSelect
|
|
58
|
+
value={(props.value as string) || ""}
|
|
59
|
+
relationOptions={props.relationOptions}
|
|
60
|
+
onValueChange={(value) => props.onValueChange(props.fieldKey, value)}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 配列リレーション型の場合
|
|
66
|
+
if (
|
|
67
|
+
props.schemaField &&
|
|
68
|
+
props.schemaField.type === "multi-relation" &&
|
|
69
|
+
props.schemaField.path
|
|
70
|
+
) {
|
|
71
|
+
// 値を配列に正規化
|
|
72
|
+
let normalizedValue: string[] = []
|
|
73
|
+
if (Array.isArray(props.value)) {
|
|
74
|
+
normalizedValue = props.value.filter(
|
|
75
|
+
(v) => typeof v === "string" && v.trim() !== "",
|
|
76
|
+
)
|
|
77
|
+
} else if (typeof props.value === "string" && props.value.trim() !== "") {
|
|
78
|
+
normalizedValue = props.value
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((v) => v.trim())
|
|
81
|
+
.filter((v) => v !== "")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<MultiRelationSelect
|
|
86
|
+
value={normalizedValue}
|
|
87
|
+
relationOptions={props.relationOptions}
|
|
88
|
+
onValueChange={(value) =>
|
|
89
|
+
props.onValueChange(props.fieldKey, value.join(", "))
|
|
90
|
+
}
|
|
91
|
+
wrap={true}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof props.originalValue === "boolean") {
|
|
97
|
+
return (
|
|
98
|
+
<Select
|
|
99
|
+
value={String(props.value)}
|
|
100
|
+
onValueChange={(value) => props.onValueChange(props.fieldKey, value)}
|
|
101
|
+
>
|
|
102
|
+
<SelectTrigger className="w-full">
|
|
103
|
+
<SelectValue />
|
|
104
|
+
</SelectTrigger>
|
|
105
|
+
<SelectContent>
|
|
106
|
+
<SelectItem value="true">true</SelectItem>
|
|
107
|
+
<SelectItem value="false">false</SelectItem>
|
|
108
|
+
</SelectContent>
|
|
109
|
+
</Select>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof props.originalValue === "number") {
|
|
114
|
+
return (
|
|
115
|
+
<Input
|
|
116
|
+
type="number"
|
|
117
|
+
value={localValue}
|
|
118
|
+
onChange={(e) => setLocalValue(e.target.value)}
|
|
119
|
+
onBlur={(e) => props.onBlur(props.fieldKey, e.target.value)}
|
|
120
|
+
className="flex-1"
|
|
121
|
+
placeholder={`${props.fieldKey}の値を入力`}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(props.originalValue)) {
|
|
127
|
+
const placeholder =
|
|
128
|
+
props.originalValue.length > 0 &&
|
|
129
|
+
typeof props.originalValue[0] === "number"
|
|
130
|
+
? "カンマ区切りで数値を入力 (例: 1, 2, 3)"
|
|
131
|
+
: props.originalValue.length > 0 &&
|
|
132
|
+
typeof props.originalValue[0] === "boolean"
|
|
133
|
+
? "カンマ区切りでtrue/falseを入力 (例: true, false, true)"
|
|
134
|
+
: "カンマ区切りで入力 (例: item1, item2, item3)"
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Input
|
|
138
|
+
value={localValue}
|
|
139
|
+
onChange={(e) => setLocalValue(e.target.value)}
|
|
140
|
+
onBlur={(e) => props.onBlur(props.fieldKey, e.target.value)}
|
|
141
|
+
className="flex-1"
|
|
142
|
+
placeholder={placeholder}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// string型の場合
|
|
148
|
+
return (
|
|
149
|
+
<Input
|
|
150
|
+
value={localValue}
|
|
151
|
+
onChange={(e) => setLocalValue(e.target.value)}
|
|
152
|
+
onBlur={(e) => props.onBlur(props.fieldKey, e.target.value)}
|
|
153
|
+
className="flex-1"
|
|
154
|
+
placeholder={`${props.fieldKey}の値を入力`}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Card } from "@/components/ui/card"
|
|
2
|
+
import { RenderFrontMatterValue } from "./render-front-matter-value"
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
frontMatter: Record<string, string | string[]> | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Front-matterをより見やすく表示するコンポーネント(表示のみ)
|
|
10
|
+
*/
|
|
11
|
+
export function FrontMatterView(props: Props) {
|
|
12
|
+
const frontMatter = props.frontMatter
|
|
13
|
+
// front-matterが空の場合は表示しない
|
|
14
|
+
if (!frontMatter || Object.keys(frontMatter).length === 0) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Card className="gap-0 p-2">
|
|
20
|
+
{Object.entries(frontMatter).map(([key, value]) => (
|
|
21
|
+
<div key={key}>
|
|
22
|
+
<div className="flex gap-x-2">
|
|
23
|
+
<span className="font-bold">{key}</span>
|
|
24
|
+
<RenderFrontMatterValue value={value} />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
))}
|
|
28
|
+
</Card>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
content: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function JsonFileEditor(props: Props) {
|
|
8
|
+
const [formattedJson, setFormattedJson] = useState<string>("")
|
|
9
|
+
const [error, setError] = useState<string | null>(null)
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
try {
|
|
13
|
+
const jsonData = JSON.parse(props.content)
|
|
14
|
+
setFormattedJson(JSON.stringify(jsonData, null, 2))
|
|
15
|
+
setError(null)
|
|
16
|
+
} catch (_e) {
|
|
17
|
+
setError(
|
|
18
|
+
"JSONのパースに失敗しました。有効なJSONではない可能性があります。",
|
|
19
|
+
)
|
|
20
|
+
setFormattedJson(props.content) // JSONのパースに失敗した場合はそのまま表示
|
|
21
|
+
}
|
|
22
|
+
}, [props.content])
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
{error && (
|
|
27
|
+
<div className="mb-4 rounded border border-red-700 bg-red-900/30 p-2 text-red-300 text-sm">
|
|
28
|
+
{error}
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
<div className="overflow-x-auto rounded bg-gray-800 p-4 text-white">
|
|
32
|
+
<pre className="whitespace-pre-wrap font-mono text-green-300">
|
|
33
|
+
{formattedJson}
|
|
34
|
+
</pre>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { marked } from "marked"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { ArchiveRestoreBanner } from "@/components/file-view/archive-restore-banner"
|
|
4
|
+
import { EditableFrontMatterView } from "@/components/file-view/editable-front-matter-view"
|
|
5
|
+
import { FileHeader } from "@/components/file-view/file-header"
|
|
6
|
+
import { Card } from "@/components/ui/card"
|
|
7
|
+
import type { DocRelation, DocSchemaRecord, DocFileMdFrontMatter } from "@/lib/types"
|
|
8
|
+
|
|
9
|
+
import "github-markdown-css"
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
filePath: string
|
|
13
|
+
fileData: {
|
|
14
|
+
path: string
|
|
15
|
+
title: string | null
|
|
16
|
+
}
|
|
17
|
+
cwd: string
|
|
18
|
+
content: string
|
|
19
|
+
onChange(content: string): void
|
|
20
|
+
frontMatter: DocFileMdFrontMatter
|
|
21
|
+
onFrontMatterUpdate: (key: string, value: unknown) => void
|
|
22
|
+
onReload: () => void
|
|
23
|
+
isLoading: boolean
|
|
24
|
+
schema?: DocSchemaRecord
|
|
25
|
+
relations?: DocRelation[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function MarkdownFileView(props: Props): ReactNode {
|
|
29
|
+
const frontMatter = props.frontMatter
|
|
30
|
+
|
|
31
|
+
const hasFrontMatter =
|
|
32
|
+
frontMatter !== null && Object.keys(frontMatter || {}).length > 0
|
|
33
|
+
|
|
34
|
+
const html = marked.parse(props.content)
|
|
35
|
+
|
|
36
|
+
// アーカイブファイルかどうかを判定
|
|
37
|
+
const isArchived = props.filePath.includes("/_/")
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="h-full space-y-2">
|
|
41
|
+
{isArchived && (
|
|
42
|
+
<ArchiveRestoreBanner
|
|
43
|
+
filePath={props.filePath}
|
|
44
|
+
onRestore={props.onReload}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
<FileHeader
|
|
48
|
+
filePath={props.filePath}
|
|
49
|
+
fileData={props.fileData}
|
|
50
|
+
cwd={props.cwd}
|
|
51
|
+
onReload={props.onReload}
|
|
52
|
+
isLoading={props.isLoading}
|
|
53
|
+
/>
|
|
54
|
+
{hasFrontMatter && (
|
|
55
|
+
<EditableFrontMatterView
|
|
56
|
+
frontMatter={frontMatter}
|
|
57
|
+
onUpdate={props.onFrontMatterUpdate}
|
|
58
|
+
schema={props.schema}
|
|
59
|
+
relations={props.relations}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
<Card className="overflow-hidden rounded-md p-0">
|
|
63
|
+
<div
|
|
64
|
+
className="markdown-body p-4"
|
|
65
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
|
|
66
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
67
|
+
/>
|
|
68
|
+
</Card>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { X } from "lucide-react"
|
|
2
|
+
import { Button } from "@/components/ui/button"
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "@/components/ui/select"
|
|
10
|
+
import type { DocRelationFile } from "@/lib/types"
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
value: string[]
|
|
14
|
+
onValueChange: (value: string[]) => void
|
|
15
|
+
relationOptions?: DocRelationFile[]
|
|
16
|
+
wrap?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 複数リレーション用のSelectコンポーネント
|
|
21
|
+
*/
|
|
22
|
+
export function MultiRelationSelect(props: Props) {
|
|
23
|
+
const options = props.relationOptions || []
|
|
24
|
+
const selectedValues = Array.isArray(props.value) ? props.value : []
|
|
25
|
+
|
|
26
|
+
// 選択済みの値を除外した利用可能なオプション
|
|
27
|
+
const availableOptions = options.filter(
|
|
28
|
+
(option) => !selectedValues.includes(option.name),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const addValue = (newValue: string) => {
|
|
32
|
+
if (newValue && !selectedValues.includes(newValue)) {
|
|
33
|
+
const updatedValues = [...selectedValues, newValue]
|
|
34
|
+
props.onValueChange(updatedValues)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const removeValue = (valueToRemove: string) => {
|
|
39
|
+
const updatedValues = selectedValues.filter((val) => val !== valueToRemove)
|
|
40
|
+
props.onValueChange(updatedValues)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// wrapありの場合(ファイル詳細用)
|
|
44
|
+
if (props.wrap) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-2">
|
|
47
|
+
{/* 新しい値を追加するSelect */}
|
|
48
|
+
{availableOptions.length > 0 && (
|
|
49
|
+
<Select onValueChange={addValue}>
|
|
50
|
+
<SelectTrigger className="w-full">
|
|
51
|
+
<SelectValue placeholder="項目を追加" />
|
|
52
|
+
</SelectTrigger>
|
|
53
|
+
<SelectContent>
|
|
54
|
+
{availableOptions.map((option) => (
|
|
55
|
+
<SelectItem key={option.name} value={option.name}>
|
|
56
|
+
{option.label}
|
|
57
|
+
</SelectItem>
|
|
58
|
+
))}
|
|
59
|
+
</SelectContent>
|
|
60
|
+
</Select>
|
|
61
|
+
)}
|
|
62
|
+
{/* 選択済みの値を表示(wrap対応) */}
|
|
63
|
+
<div className="flex flex-wrap gap-2">
|
|
64
|
+
{selectedValues.toReversed().map((value) => {
|
|
65
|
+
const option = options.find((opt) => opt.name === value)
|
|
66
|
+
const label = option?.label || value
|
|
67
|
+
return (
|
|
68
|
+
<Button
|
|
69
|
+
key={value}
|
|
70
|
+
variant={"outline"}
|
|
71
|
+
onClick={() => removeValue(value)}
|
|
72
|
+
className="h-auto break-words text-left"
|
|
73
|
+
>
|
|
74
|
+
<span className="flex-1">{label}</span>
|
|
75
|
+
<X className="ml-2 w-4 flex-shrink-0 text-muted-foreground" />
|
|
76
|
+
</Button>
|
|
77
|
+
)
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
{selectedValues.length === 0 && availableOptions.length === 0 && (
|
|
81
|
+
<div className="text-muted-foreground text-sm">
|
|
82
|
+
利用可能なオプションがありません
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// wrapなしの場合(テーブル用)
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex gap-2">
|
|
92
|
+
{/* 新しい値を追加するSelect */}
|
|
93
|
+
{availableOptions.length > 0 && (
|
|
94
|
+
<Select onValueChange={addValue}>
|
|
95
|
+
<SelectTrigger className="min-w-24">
|
|
96
|
+
<SelectValue placeholder="項目を追加" />
|
|
97
|
+
</SelectTrigger>
|
|
98
|
+
<SelectContent>
|
|
99
|
+
{availableOptions.map((option) => (
|
|
100
|
+
<SelectItem key={option.name} value={option.name}>
|
|
101
|
+
{option.label}
|
|
102
|
+
</SelectItem>
|
|
103
|
+
))}
|
|
104
|
+
</SelectContent>
|
|
105
|
+
</Select>
|
|
106
|
+
)}
|
|
107
|
+
{/* 選択済みの値を表示(横並び) */}
|
|
108
|
+
{selectedValues.toReversed().map((value) => {
|
|
109
|
+
const option = options.find((opt) => opt.name === value)
|
|
110
|
+
const label = option?.label || value
|
|
111
|
+
return (
|
|
112
|
+
<Button
|
|
113
|
+
key={value}
|
|
114
|
+
variant={"outline"}
|
|
115
|
+
onClick={() => removeValue(value)}
|
|
116
|
+
>
|
|
117
|
+
<span>{label}</span>
|
|
118
|
+
<X className="w-4 text-muted-foreground" />
|
|
119
|
+
</Button>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
{selectedValues.length === 0 && availableOptions.length === 0 && (
|
|
123
|
+
<div className="text-muted-foreground text-sm">
|
|
124
|
+
利用可能なオプションがありません
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
value: unknown
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* フロントマターの値を再帰的にレンダリングする
|
|
7
|
+
*/
|
|
8
|
+
export function RenderFrontMatterValue(props: Props) {
|
|
9
|
+
const value = props.value
|
|
10
|
+
|
|
11
|
+
if (value === null || value === undefined) {
|
|
12
|
+
return <span>null</span>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof value === "boolean") {
|
|
16
|
+
return <span>{value.toString()}</span>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof value === "number") {
|
|
20
|
+
return <span>{value}</span>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
return <span>"{value}"</span>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return (
|
|
29
|
+
<div>
|
|
30
|
+
{value.map((item, index) => (
|
|
31
|
+
<div
|
|
32
|
+
key={`item-${index}-${String(item).substring(0, 8)}`}
|
|
33
|
+
className="flex"
|
|
34
|
+
>
|
|
35
|
+
<span className="mr-2">-</span>
|
|
36
|
+
<RenderFrontMatterValue value={item} />
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof value === "object") {
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
{Object.entries(value as Record<string, unknown>).map(([key, val]) => (
|
|
47
|
+
<div key={key} className="flex">
|
|
48
|
+
<span className="mr-2">{key}:</span>
|
|
49
|
+
<RenderFrontMatterValue value={val} />
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return <span>{String(value)}</span>
|
|
57
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Select,
|
|
3
|
+
SelectContent,
|
|
4
|
+
SelectItem,
|
|
5
|
+
SelectTrigger,
|
|
6
|
+
SelectValue,
|
|
7
|
+
} from "@/components/ui/select"
|
|
8
|
+
|
|
9
|
+
import type { DocRelationFile } from "@/lib/types"
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
value: string
|
|
13
|
+
onValueChange: (value: string) => void
|
|
14
|
+
relationOptions?: DocRelationFile[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 単一リレーション用のSelectコンポーネント
|
|
19
|
+
*/
|
|
20
|
+
export function SingleRelationSelect(props: Props) {
|
|
21
|
+
const options = props.relationOptions || []
|
|
22
|
+
|
|
23
|
+
const handleValueChange = (value: string) => {
|
|
24
|
+
props.onValueChange(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Select value={props.value || ""} onValueChange={handleValueChange}>
|
|
29
|
+
<SelectTrigger className="w-full bg-background">
|
|
30
|
+
<SelectValue placeholder="リレーションを選択" />
|
|
31
|
+
</SelectTrigger>
|
|
32
|
+
<SelectContent>
|
|
33
|
+
{options.map((option) => (
|
|
34
|
+
<SelectItem key={option.name} value={option.name}>
|
|
35
|
+
{option.label}
|
|
36
|
+
</SelectItem>
|
|
37
|
+
))}
|
|
38
|
+
</SelectContent>
|
|
39
|
+
</Select>
|
|
40
|
+
)
|
|
41
|
+
}
|