@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,383 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
|
2
|
+
import { Link } from "@tanstack/react-router"
|
|
3
|
+
import { Archive, ArchiveRestore, Plus, Trash2 } from "lucide-react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
import { EditableTableCell } from "@/components/editable-table-cell"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { Card } from "@/components/ui/card"
|
|
8
|
+
import {
|
|
9
|
+
Table,
|
|
10
|
+
TableBody,
|
|
11
|
+
TableCell,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableHeader,
|
|
14
|
+
TableRow,
|
|
15
|
+
} from "@/components/ui/table"
|
|
16
|
+
import { apiClient } from "@/lib/api-client"
|
|
17
|
+
import { normalizePath } from "@/lib/path-utils"
|
|
18
|
+
import type { DocFile, DocFileMd, DocRelation, DocSchemaFieldType } from "@/lib/types"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* テーブルカラムの型定義
|
|
22
|
+
*/
|
|
23
|
+
type DocTableColumn = {
|
|
24
|
+
key: string
|
|
25
|
+
label: string
|
|
26
|
+
type: DocSchemaFieldType
|
|
27
|
+
path: string
|
|
28
|
+
options: string[] | number[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isDocFileMd(file: DocFile): file is DocFileMd {
|
|
32
|
+
return file.type === 'markdown'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Props = {
|
|
36
|
+
files: DocFile[]
|
|
37
|
+
columns: DocTableColumn[]
|
|
38
|
+
directoryPath: string
|
|
39
|
+
onDataChanged?: () => void
|
|
40
|
+
relations?: DocRelation[]
|
|
41
|
+
archivedCount?: number
|
|
42
|
+
showArchived?: boolean
|
|
43
|
+
onToggleArchived?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function DirectoryTableView(props: Props) {
|
|
47
|
+
const queryClient = useQueryClient()
|
|
48
|
+
const [deleteConfirmFiles, setDeleteConfirmFiles] = useState<Set<string>>(
|
|
49
|
+
new Set(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const createFileMutation = useMutation({
|
|
53
|
+
async mutationFn() {
|
|
54
|
+
const endpoint = apiClient.api.files
|
|
55
|
+
const resp = await endpoint.$post({
|
|
56
|
+
json: {
|
|
57
|
+
directoryPath: normalizePath(props.directoryPath),
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return resp.json()
|
|
62
|
+
},
|
|
63
|
+
onSuccess() {
|
|
64
|
+
// ファイルツリーキャッシュを無効化
|
|
65
|
+
queryClient.invalidateQueries({ queryKey: ["file-tree"] })
|
|
66
|
+
|
|
67
|
+
if (!props.onDataChanged) return
|
|
68
|
+
props.onDataChanged()
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const updateCellMutation = useMutation({
|
|
73
|
+
async mutationFn(params: { path: string; field: string; value: unknown }) {
|
|
74
|
+
console.log('Updating cell:', params)
|
|
75
|
+
try {
|
|
76
|
+
const endpoint = apiClient.api.files[":path{.+}"]
|
|
77
|
+
const resp = await endpoint.$put({
|
|
78
|
+
param: { path: params.path },
|
|
79
|
+
json: {
|
|
80
|
+
properties: { [params.field]: params.value },
|
|
81
|
+
content: null,
|
|
82
|
+
title: null,
|
|
83
|
+
description: null,
|
|
84
|
+
isArchived: null,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
console.log('Response status:', resp.status)
|
|
89
|
+
|
|
90
|
+
if (!resp.ok) {
|
|
91
|
+
const errorText = await resp.text()
|
|
92
|
+
console.error('Response error:', errorText)
|
|
93
|
+
throw new Error(`API Error: ${resp.status} - ${errorText}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await resp.json()
|
|
97
|
+
console.log('Update response:', result)
|
|
98
|
+
return result
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Update request failed:', error)
|
|
101
|
+
throw error
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
onSuccess() {
|
|
105
|
+
// キャッシュを無効化
|
|
106
|
+
queryClient.invalidateQueries({ queryKey: ["file-tree"] })
|
|
107
|
+
queryClient.invalidateQueries({ queryKey: ["directory"] })
|
|
108
|
+
|
|
109
|
+
if (!props.onDataChanged) return
|
|
110
|
+
props.onDataChanged()
|
|
111
|
+
},
|
|
112
|
+
onError(error) {
|
|
113
|
+
console.error('Update mutation failed:', error)
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const updateTitleMutation = useMutation({
|
|
118
|
+
async mutationFn(params: { path: string; title: string }) {
|
|
119
|
+
const endpoint = apiClient.api.files[":path{.+}"]
|
|
120
|
+
const resp = await endpoint.$put({
|
|
121
|
+
param: { path: params.path },
|
|
122
|
+
json: {
|
|
123
|
+
title: params.title,
|
|
124
|
+
properties: null,
|
|
125
|
+
content: null,
|
|
126
|
+
description: null,
|
|
127
|
+
isArchived: null,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return resp.json()
|
|
132
|
+
},
|
|
133
|
+
onSuccess() {
|
|
134
|
+
if (!props.onDataChanged) return
|
|
135
|
+
props.onDataChanged()
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const deleteFileMutation = useMutation({
|
|
140
|
+
async mutationFn(filePath: string) {
|
|
141
|
+
// パスを正規化
|
|
142
|
+
const normalizedPath = normalizePath(filePath)
|
|
143
|
+
|
|
144
|
+
// apiClientを使用して削除
|
|
145
|
+
const endpoint = apiClient.api.files[":path{.+}"]
|
|
146
|
+
const resp = await endpoint.$delete({
|
|
147
|
+
param: { path: normalizedPath },
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (!resp.ok) {
|
|
151
|
+
const errorText = await resp.text()
|
|
152
|
+
throw new Error(`Failed to delete file: ${errorText}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return resp.json()
|
|
156
|
+
},
|
|
157
|
+
onSuccess() {
|
|
158
|
+
// ファイルツリーキャッシュを無効化
|
|
159
|
+
queryClient.invalidateQueries({ queryKey: ["file-tree"] })
|
|
160
|
+
|
|
161
|
+
if (!props.onDataChanged) return
|
|
162
|
+
props.onDataChanged()
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const archiveFileMutation = useMutation({
|
|
167
|
+
async mutationFn(filePath: string) {
|
|
168
|
+
// パスを正規化
|
|
169
|
+
const normalizedPath = normalizePath(filePath)
|
|
170
|
+
|
|
171
|
+
const endpoint = apiClient.api.files[":path{.+}"]
|
|
172
|
+
const resp = await endpoint.$put({
|
|
173
|
+
param: { path: normalizedPath },
|
|
174
|
+
json: {
|
|
175
|
+
properties: null,
|
|
176
|
+
content: null,
|
|
177
|
+
title: null,
|
|
178
|
+
description: null,
|
|
179
|
+
isArchived: true,
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return resp.json()
|
|
184
|
+
},
|
|
185
|
+
onSuccess() {
|
|
186
|
+
// ファイルツリーキャッシュを無効化
|
|
187
|
+
queryClient.invalidateQueries({ queryKey: ["file-tree"] })
|
|
188
|
+
|
|
189
|
+
if (!props.onDataChanged) return
|
|
190
|
+
props.onDataChanged()
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const restoreFileMutation = useMutation({
|
|
195
|
+
async mutationFn(filePath: string) {
|
|
196
|
+
// パスを正規化
|
|
197
|
+
const normalizedPath = normalizePath(filePath)
|
|
198
|
+
|
|
199
|
+
const endpoint = apiClient.api.files[":path{.+}"]
|
|
200
|
+
const resp = await endpoint.$put({
|
|
201
|
+
param: { path: normalizedPath },
|
|
202
|
+
json: {
|
|
203
|
+
properties: null,
|
|
204
|
+
content: null,
|
|
205
|
+
title: null,
|
|
206
|
+
description: null,
|
|
207
|
+
isArchived: false,
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return resp.json()
|
|
212
|
+
},
|
|
213
|
+
onSuccess() {
|
|
214
|
+
// ファイルツリーキャッシュを無効化
|
|
215
|
+
queryClient.invalidateQueries({ queryKey: ["file-tree"] })
|
|
216
|
+
|
|
217
|
+
if (!props.onDataChanged) return
|
|
218
|
+
props.onDataChanged()
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
const handleDeleteClick = (filePath: string) => {
|
|
224
|
+
if (deleteConfirmFiles.has(filePath)) {
|
|
225
|
+
// 2回目のクリック - ファイルを削除
|
|
226
|
+
deleteFileMutation.mutate(filePath)
|
|
227
|
+
setDeleteConfirmFiles(new Set())
|
|
228
|
+
} else {
|
|
229
|
+
// 1回目のクリック - 確認状態にする
|
|
230
|
+
setDeleteConfirmFiles(new Set([filePath]))
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const handleArchiveClick = (filePath: string) => {
|
|
235
|
+
archiveFileMutation.mutate(filePath)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const handleRestoreClick = (filePath: string) => {
|
|
239
|
+
restoreFileMutation.mutate(filePath)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<Card className="gap-0 overflow-x-scroll rounded-md p-0">
|
|
244
|
+
<Table>
|
|
245
|
+
<TableHeader>
|
|
246
|
+
<TableRow>
|
|
247
|
+
<TableHead className="w-40">ファイル名</TableHead>
|
|
248
|
+
<TableHead className="min-w-64">タイトル</TableHead>
|
|
249
|
+
{props.columns.map((column) => (
|
|
250
|
+
<TableHead key={column.key}>{column.label}</TableHead>
|
|
251
|
+
))}
|
|
252
|
+
<TableHead className="w-16" />
|
|
253
|
+
</TableRow>
|
|
254
|
+
</TableHeader>
|
|
255
|
+
<TableBody>
|
|
256
|
+
{props.files.filter(isDocFileMd).map((fileData) => {
|
|
257
|
+
const isArchived = 'isArchived' in fileData && fileData.isArchived
|
|
258
|
+
return (
|
|
259
|
+
<TableRow
|
|
260
|
+
key={fileData.path.path}
|
|
261
|
+
className={isArchived ? "opacity-60" : ""}
|
|
262
|
+
>
|
|
263
|
+
<TableCell className="font-medium">
|
|
264
|
+
<Link
|
|
265
|
+
to="/$"
|
|
266
|
+
params={{ _splat: normalizePath(fileData.path.path) }}
|
|
267
|
+
className="text-blue-600 hover:underline"
|
|
268
|
+
>
|
|
269
|
+
{fileData.path.name}
|
|
270
|
+
</Link>
|
|
271
|
+
</TableCell>
|
|
272
|
+
<TableCell className="p-0">
|
|
273
|
+
<EditableTableCell
|
|
274
|
+
value={fileData.content.title || ""}
|
|
275
|
+
type="string"
|
|
276
|
+
onUpdate={(newValue) => {
|
|
277
|
+
return updateTitleMutation.mutate({
|
|
278
|
+
path: fileData.path.path,
|
|
279
|
+
title: String(newValue),
|
|
280
|
+
})
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
283
|
+
</TableCell>
|
|
284
|
+
{props.columns.map((column) => {
|
|
285
|
+
// リレーション情報を取得
|
|
286
|
+
const relationData = props.relations?.find(
|
|
287
|
+
(rel) => rel.path === column.path,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const cellValue = fileData.content.frontMatter?.[column.key]
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<TableCell key={column.key} className="p-1">
|
|
294
|
+
<EditableTableCell
|
|
295
|
+
value={cellValue}
|
|
296
|
+
type={column.type}
|
|
297
|
+
path={column.path}
|
|
298
|
+
relationOptions={relationData?.files}
|
|
299
|
+
selectOptions={column.options}
|
|
300
|
+
onUpdate={(newValue) => {
|
|
301
|
+
return updateCellMutation.mutate({
|
|
302
|
+
path: fileData.path.path,
|
|
303
|
+
field: column.key,
|
|
304
|
+
value: newValue,
|
|
305
|
+
})
|
|
306
|
+
}}
|
|
307
|
+
/>
|
|
308
|
+
</TableCell>
|
|
309
|
+
)
|
|
310
|
+
})}
|
|
311
|
+
<TableCell className="p-2">
|
|
312
|
+
<div className="flex justify-end gap-1">
|
|
313
|
+
{!isArchived && (
|
|
314
|
+
<Button
|
|
315
|
+
size="sm"
|
|
316
|
+
variant="ghost"
|
|
317
|
+
onClick={() => handleArchiveClick(fileData.path.path)}
|
|
318
|
+
disabled={archiveFileMutation.isPending}
|
|
319
|
+
className="h-8 w-8 p-0"
|
|
320
|
+
title="アーカイブする"
|
|
321
|
+
>
|
|
322
|
+
<Archive className="h-4 w-4" />
|
|
323
|
+
</Button>
|
|
324
|
+
)}
|
|
325
|
+
{isArchived && (
|
|
326
|
+
<Button
|
|
327
|
+
size="sm"
|
|
328
|
+
variant="ghost"
|
|
329
|
+
onClick={() => handleRestoreClick(fileData.path.path)}
|
|
330
|
+
disabled={restoreFileMutation.isPending}
|
|
331
|
+
className="h-8 w-8 p-0"
|
|
332
|
+
title="復元する"
|
|
333
|
+
>
|
|
334
|
+
<ArchiveRestore className="h-4 w-4" />
|
|
335
|
+
</Button>
|
|
336
|
+
)}
|
|
337
|
+
<Button
|
|
338
|
+
size="sm"
|
|
339
|
+
variant={
|
|
340
|
+
deleteConfirmFiles.has(fileData.path.path)
|
|
341
|
+
? "destructive"
|
|
342
|
+
: "ghost"
|
|
343
|
+
}
|
|
344
|
+
onClick={() => handleDeleteClick(fileData.path.path)}
|
|
345
|
+
disabled={deleteFileMutation.isPending}
|
|
346
|
+
className="h-8 w-8 p-0"
|
|
347
|
+
title="削除する"
|
|
348
|
+
>
|
|
349
|
+
<Trash2 className="h-4 w-4" />
|
|
350
|
+
</Button>
|
|
351
|
+
</div>
|
|
352
|
+
</TableCell>
|
|
353
|
+
</TableRow>
|
|
354
|
+
)
|
|
355
|
+
})}
|
|
356
|
+
</TableBody>
|
|
357
|
+
</Table>
|
|
358
|
+
<div className="border-t p-2 flex gap-2">
|
|
359
|
+
<Button
|
|
360
|
+
size="sm"
|
|
361
|
+
variant="secondary"
|
|
362
|
+
onClick={() => createFileMutation.mutate()}
|
|
363
|
+
disabled={createFileMutation.isPending}
|
|
364
|
+
>
|
|
365
|
+
<Plus className="h-4 w-4" />
|
|
366
|
+
{"新しいファイル"}
|
|
367
|
+
</Button>
|
|
368
|
+
{(props.archivedCount ?? 0) > 0 && (
|
|
369
|
+
<Button
|
|
370
|
+
size="sm"
|
|
371
|
+
variant={props.showArchived ? "default" : "outline"}
|
|
372
|
+
onClick={props.onToggleArchived}
|
|
373
|
+
>
|
|
374
|
+
{props.showArchived
|
|
375
|
+
? `表示中(${props.archivedCount}件)`
|
|
376
|
+
: `非表示(${props.archivedCount}件)`
|
|
377
|
+
}
|
|
378
|
+
</Button>
|
|
379
|
+
)}
|
|
380
|
+
</div>
|
|
381
|
+
</Card>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Select,
|
|
3
|
+
SelectContent,
|
|
4
|
+
SelectItem,
|
|
5
|
+
SelectTrigger,
|
|
6
|
+
SelectValue,
|
|
7
|
+
} from "@/components/ui/select"
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
value: unknown
|
|
11
|
+
onUpdate: (value: unknown) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function BooleanEditableCell(props: Props) {
|
|
15
|
+
return (
|
|
16
|
+
<Select
|
|
17
|
+
value={
|
|
18
|
+
props.value === true
|
|
19
|
+
? "true"
|
|
20
|
+
: props.value === false
|
|
21
|
+
? "false"
|
|
22
|
+
: "undefined"
|
|
23
|
+
}
|
|
24
|
+
onValueChange={(value) => {
|
|
25
|
+
if (value === "true") {
|
|
26
|
+
props.onUpdate(true)
|
|
27
|
+
} else if (value === "false") {
|
|
28
|
+
props.onUpdate(false)
|
|
29
|
+
} else {
|
|
30
|
+
props.onUpdate(undefined)
|
|
31
|
+
}
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<SelectTrigger className="w-full">
|
|
35
|
+
<SelectValue placeholder="選択してください" />
|
|
36
|
+
</SelectTrigger>
|
|
37
|
+
<SelectContent>
|
|
38
|
+
<SelectItem value="undefined">-</SelectItem>
|
|
39
|
+
<SelectItem value="true">true</SelectItem>
|
|
40
|
+
<SelectItem value="false">false</SelectItem>
|
|
41
|
+
</SelectContent>
|
|
42
|
+
</Select>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { Input } from "@/components/ui/input"
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
value: unknown
|
|
6
|
+
onUpdate: (value: unknown) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function MultiBooleanEditableCell(props: Props) {
|
|
10
|
+
const formatValue = (value: unknown): string => {
|
|
11
|
+
if (value === undefined || value === null) {
|
|
12
|
+
return ""
|
|
13
|
+
}
|
|
14
|
+
return Array.isArray(value) ? value.join(", ") : ""
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [editValue, setEditValue] = useState(formatValue(props.value))
|
|
18
|
+
|
|
19
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setEditValue(formatValue(props.value))
|
|
22
|
+
}, [props.value])
|
|
23
|
+
|
|
24
|
+
const parseValue = (text: string): unknown => {
|
|
25
|
+
if (text === "") {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
return text.split(",").map((s) => s.trim().toLowerCase() === "true")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const displayValue = formatValue(props.value)
|
|
32
|
+
|
|
33
|
+
const handleBlur = () => {
|
|
34
|
+
const parsedValue = parseValue(editValue)
|
|
35
|
+
if (parsedValue !== props.value) {
|
|
36
|
+
props.onUpdate(parsedValue)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
41
|
+
if (e.key === "Enter") {
|
|
42
|
+
handleBlur()
|
|
43
|
+
}
|
|
44
|
+
if (e.key === "Escape") {
|
|
45
|
+
setEditValue(displayValue)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Input
|
|
51
|
+
type="text"
|
|
52
|
+
value={editValue}
|
|
53
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
54
|
+
onBlur={handleBlur}
|
|
55
|
+
onKeyDown={handleKeyDown}
|
|
56
|
+
placeholder={displayValue || "-"}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { Input } from "@/components/ui/input"
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
value: unknown
|
|
6
|
+
onUpdate: (value: unknown) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function MultiNumberEditableCell(props: Props) {
|
|
10
|
+
const formatValue = (value: unknown): string => {
|
|
11
|
+
if (value === undefined || value === null) {
|
|
12
|
+
return ""
|
|
13
|
+
}
|
|
14
|
+
return Array.isArray(value) ? value.join(", ") : ""
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [editValue, setEditValue] = useState(formatValue(props.value))
|
|
18
|
+
|
|
19
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setEditValue(formatValue(props.value))
|
|
22
|
+
}, [props.value])
|
|
23
|
+
|
|
24
|
+
const parseValue = (text: string): unknown => {
|
|
25
|
+
if (text === "") {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
return text.split(",").map((s) => {
|
|
29
|
+
const n = Number(s.trim())
|
|
30
|
+
return Number.isNaN(n) ? 0 : n
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const displayValue = formatValue(props.value)
|
|
35
|
+
|
|
36
|
+
const handleBlur = () => {
|
|
37
|
+
const parsedValue = parseValue(editValue)
|
|
38
|
+
if (parsedValue !== props.value) {
|
|
39
|
+
props.onUpdate(parsedValue)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
44
|
+
if (e.key === "Enter") {
|
|
45
|
+
handleBlur()
|
|
46
|
+
}
|
|
47
|
+
if (e.key === "Escape") {
|
|
48
|
+
setEditValue(displayValue)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Input
|
|
54
|
+
type="text"
|
|
55
|
+
value={editValue}
|
|
56
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
57
|
+
onBlur={handleBlur}
|
|
58
|
+
onKeyDown={handleKeyDown}
|
|
59
|
+
placeholder={displayValue || "-"}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MultiRelationSelect } from "@/components/file-view/multi-relation-select"
|
|
2
|
+
import type { DocRelationFile } from "@/lib/types"
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
value: unknown
|
|
6
|
+
onUpdate: (value: unknown) => void
|
|
7
|
+
relationOptions?: DocRelationFile[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function MultiRelationEditableCell(props: Props) {
|
|
11
|
+
// MultiRelationSelectはDocRelationFileを直接受け取る
|
|
12
|
+
return (
|
|
13
|
+
<MultiRelationSelect
|
|
14
|
+
value={Array.isArray(props.value) ? props.value : []}
|
|
15
|
+
relationOptions={props.relationOptions}
|
|
16
|
+
onValueChange={(value) => props.onUpdate(value)}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { X } from "lucide-react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
import { Badge } from "@/components/ui/badge"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
value: number[]
|
|
8
|
+
options: number[]
|
|
9
|
+
onUpdate: (value: number[]) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 複数選択数値セル
|
|
14
|
+
*/
|
|
15
|
+
export function MultiSelectNumberEditableCell(props: Props) {
|
|
16
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
17
|
+
const currentValues = props.value || []
|
|
18
|
+
|
|
19
|
+
const handleToggle = (option: number) => {
|
|
20
|
+
const isSelected = currentValues.includes(option)
|
|
21
|
+
const newValues = isSelected
|
|
22
|
+
? currentValues.filter((v) => v !== option)
|
|
23
|
+
: [...currentValues, option]
|
|
24
|
+
props.onUpdate(newValues)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const handleRemove = (value: number) => {
|
|
28
|
+
const newValues = currentValues.filter((v) => v !== value)
|
|
29
|
+
props.onUpdate(newValues)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="relative">
|
|
34
|
+
<div className="flex min-h-8 flex-wrap gap-1 rounded-md border p-2">
|
|
35
|
+
{currentValues.map((value) => (
|
|
36
|
+
<Badge
|
|
37
|
+
key={value}
|
|
38
|
+
variant="secondary"
|
|
39
|
+
className="flex items-center gap-1"
|
|
40
|
+
>
|
|
41
|
+
{value}
|
|
42
|
+
<Button
|
|
43
|
+
size="sm"
|
|
44
|
+
variant="ghost"
|
|
45
|
+
className="h-4 w-4 p-0"
|
|
46
|
+
onClick={() => handleRemove(value)}
|
|
47
|
+
>
|
|
48
|
+
<X className="h-3 w-3" />
|
|
49
|
+
</Button>
|
|
50
|
+
</Badge>
|
|
51
|
+
))}
|
|
52
|
+
<Button
|
|
53
|
+
variant="ghost"
|
|
54
|
+
size="sm"
|
|
55
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
56
|
+
className="text-muted-foreground"
|
|
57
|
+
>
|
|
58
|
+
追加
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
{isOpen && (
|
|
62
|
+
<div className="absolute z-10 mt-1 w-full rounded-md border bg-white shadow-lg">
|
|
63
|
+
{props.options
|
|
64
|
+
.filter((option) => !currentValues.includes(option))
|
|
65
|
+
.map((option) => (
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
key={option}
|
|
69
|
+
className="w-full px-3 py-2 text-left hover:bg-gray-100"
|
|
70
|
+
onClick={() => {
|
|
71
|
+
handleToggle(option)
|
|
72
|
+
setIsOpen(false)
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{option}
|
|
76
|
+
</button>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|