@interactive-inc/docs-client 0.0.1 → 0.0.3
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/bin/docs.js +3 -0
- package/build/assets/index.css +1 -1
- package/build/assets/index.js +53 -53
- package/build/index.html +13 -0
- package/build/index.js +24057 -16
- package/package.json +13 -6
- package/.tanstack/tmp/router-generator-Eq20Yx/50b11a0f488e61cf92364a7e585c1448 +0 -123
- package/.tanstack/tmp/router-generator-KQIYSa/aecb094e4891f9475b91de590abc8809 +0 -117
- package/.tanstack/tmp/router-generator-ekW2pI/aecb094e4891f9475b91de590abc8809 +0 -117
- package/app/components/archived-file-list-view.tsx +0 -99
- package/app/components/directory-file-list-view.tsx +0 -32
- package/app/components/directory-file-tree-node.tsx +0 -96
- package/app/components/directory-layout-sidebar.tsx +0 -181
- package/app/components/directory-page-view.tsx +0 -208
- package/app/components/directory-table-view.tsx +0 -383
- package/app/components/editable-table-cell/boolean-editable-cell.tsx +0 -44
- package/app/components/editable-table-cell/multi-boolean-editable-cell.tsx +0 -59
- package/app/components/editable-table-cell/multi-number-editable-cell.tsx +0 -62
- package/app/components/editable-table-cell/multi-relation-editable-cell.tsx +0 -19
- package/app/components/editable-table-cell/multi-select-number-editable-cell.tsx +0 -82
- package/app/components/editable-table-cell/multi-select-text-editable-cell.tsx +0 -82
- package/app/components/editable-table-cell/multi-text-editable-cell.tsx +0 -62
- package/app/components/editable-table-cell/number-editable-cell.tsx +0 -54
- package/app/components/editable-table-cell/relation-editable-cell.tsx +0 -20
- package/app/components/editable-table-cell/select-number-editable-cell.tsx +0 -43
- package/app/components/editable-table-cell/select-text-editable-cell.tsx +0 -33
- package/app/components/editable-table-cell/string-editable-cell.tsx +0 -42
- package/app/components/editable-table-cell.tsx +0 -129
- package/app/components/file-card.tsx +0 -120
- package/app/components/file-tree-icon.tsx +0 -26
- package/app/components/file-view/archive-restore-banner.tsx +0 -86
- package/app/components/file-view/csv-file-view.tsx +0 -50
- package/app/components/file-view/default-file-view.tsx +0 -11
- package/app/components/file-view/editable-csv-cell.tsx +0 -37
- package/app/components/file-view/editable-front-matter-view.tsx +0 -105
- package/app/components/file-view/file-csv-columns-view.tsx +0 -181
- package/app/components/file-view/file-csv-table.tsx +0 -142
- package/app/components/file-view/file-header.tsx +0 -115
- package/app/components/file-view/file-page-view.tsx +0 -165
- package/app/components/file-view/front-matter-input-field.tsx +0 -157
- package/app/components/file-view/front-matter-view.tsx +0 -30
- package/app/components/file-view/json-file-editor.tsx +0 -38
- package/app/components/file-view/markdown-file-view.tsx +0 -71
- package/app/components/file-view/multi-relation-select.tsx +0 -129
- package/app/components/file-view/render-front-matter-value.tsx +0 -57
- package/app/components/file-view/single-relation-select.tsx +0 -41
- package/app/components/file-view/sortable-csv-row.tsx +0 -57
- package/app/components/file-view/sortable-header.tsx +0 -30
- package/app/components/file-view/utils.ts +0 -8
- package/app/components/milestones-editor.tsx +0 -95
- package/app/components/page-header.tsx +0 -20
- package/app/components/project-view/feature-item.tsx +0 -139
- package/app/components/project-view/project-page-group.tsx +0 -125
- package/app/components/project-view/project-view.tsx +0 -230
- package/app/components/project-view/unlinked-features-section.tsx +0 -73
- package/app/components/providers.tsx +0 -21
- package/app/components/reload-button.tsx +0 -25
- package/app/components/sidebar-button.tsx +0 -13
- package/app/components/ui/alert.tsx +0 -66
- package/app/components/ui/badge.tsx +0 -46
- package/app/components/ui/button.tsx +0 -59
- package/app/components/ui/card.tsx +0 -92
- package/app/components/ui/dialog.tsx +0 -133
- package/app/components/ui/emoji-picker.tsx +0 -133
- package/app/components/ui/icons.tsx +0 -3
- package/app/components/ui/input.tsx +0 -21
- package/app/components/ui/select.tsx +0 -183
- package/app/components/ui/separator.tsx +0 -26
- package/app/components/ui/sheet.tsx +0 -137
- package/app/components/ui/sidebar.tsx +0 -724
- package/app/components/ui/simple-sidebar.tsx +0 -50
- package/app/components/ui/skeleton.tsx +0 -13
- package/app/components/ui/sonner.tsx +0 -23
- package/app/components/ui/table.tsx +0 -114
- package/app/components/ui/tabs.tsx +0 -53
- package/app/components/ui/textarea.tsx +0 -18
- package/app/components/ui/tooltip.tsx +0 -59
- package/app/components/vscode-button.tsx +0 -23
- package/app/hooks/use-directory-query.ts +0 -22
- package/app/hooks/use-file-properties-mutation.ts +0 -38
- package/app/hooks/use-file-query.ts +0 -21
- package/app/hooks/use-mobile.ts +0 -19
- package/app/main.css +0 -122
- package/app/main.tsx +0 -38
- package/app/routes/$.index.tsx +0 -25
- package/app/routes/$.tsx +0 -20
- package/app/routes/__root.tsx +0 -20
- package/app/routes/_index.tsx +0 -17
- package/app/routes/apps.client.$.tsx +0 -21
- package/build/index.d.ts +0 -2
- package/build/index.d.ts.map +0 -1
- package/components.json +0 -21
- package/tsconfig.index.json +0 -17
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { useMutation } from "@tanstack/react-query"
|
|
2
|
-
import { useNavigate } from "@tanstack/react-router"
|
|
3
|
-
import { useState } from "react"
|
|
4
|
-
import { Button } from "@/components/ui/button"
|
|
5
|
-
import { Card } from "@/components/ui/card"
|
|
6
|
-
import { apiClient } from "@/lib/api-client"
|
|
7
|
-
import { normalizePath } from "@/lib/path-utils"
|
|
8
|
-
|
|
9
|
-
type Props = {
|
|
10
|
-
filePath: string
|
|
11
|
-
onRestore?: () => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function ArchiveRestoreBanner(props: Props) {
|
|
15
|
-
const navigate = useNavigate()
|
|
16
|
-
|
|
17
|
-
const [isRestoring, setIsRestoring] = useState(false)
|
|
18
|
-
|
|
19
|
-
const restoreMutation = useMutation({
|
|
20
|
-
async mutationFn() {
|
|
21
|
-
setIsRestoring(true)
|
|
22
|
-
|
|
23
|
-
// パスから docs/ プレフィックスを削除
|
|
24
|
-
const normalizedPath = normalizePath(props.filePath)
|
|
25
|
-
|
|
26
|
-
// ファイルを復元(アーカイブから移動)
|
|
27
|
-
const response = await apiClient.api.files[":path{.+}"].$put({
|
|
28
|
-
param: {
|
|
29
|
-
path: normalizedPath,
|
|
30
|
-
},
|
|
31
|
-
json: {
|
|
32
|
-
title: null,
|
|
33
|
-
properties: null,
|
|
34
|
-
content: null,
|
|
35
|
-
description: null,
|
|
36
|
-
isArchived: false, // アーカイブを解除
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
return response.json()
|
|
41
|
-
},
|
|
42
|
-
onSuccess: async (_data) => {
|
|
43
|
-
// 復元後のパスを構築(_ディレクトリを除外)
|
|
44
|
-
const pathSegments = props.filePath.split("/")
|
|
45
|
-
const fileName = pathSegments.pop()
|
|
46
|
-
const restoredPath = pathSegments
|
|
47
|
-
.filter((segment) => segment !== "_")
|
|
48
|
-
.concat(fileName || "")
|
|
49
|
-
.join("/")
|
|
50
|
-
|
|
51
|
-
// コールバックを実行
|
|
52
|
-
if (props.onRestore) {
|
|
53
|
-
props.onRestore()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 復元後のファイルパスにリダイレクト
|
|
57
|
-
navigate({ to: `/${restoredPath}` })
|
|
58
|
-
},
|
|
59
|
-
onError: (error) => {
|
|
60
|
-
console.error("復元エラー:", error)
|
|
61
|
-
alert("ファイルの復元に失敗しました")
|
|
62
|
-
},
|
|
63
|
-
onSettled: () => {
|
|
64
|
-
setIsRestoring(false)
|
|
65
|
-
},
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<Card className="p-2">
|
|
70
|
-
<div className="flex items-center justify-between gap-2">
|
|
71
|
-
<div className="flex items-center gap-x-2">
|
|
72
|
-
<span className="text-muted-foreground text-sm">
|
|
73
|
-
このファイルはアーカイブされています
|
|
74
|
-
</span>
|
|
75
|
-
</div>
|
|
76
|
-
<Button
|
|
77
|
-
size="sm"
|
|
78
|
-
onClick={() => restoreMutation.mutate()}
|
|
79
|
-
disabled={isRestoring || restoreMutation.isPending}
|
|
80
|
-
>
|
|
81
|
-
{isRestoring ? "復元中..." : "復元する"}
|
|
82
|
-
</Button>
|
|
83
|
-
</div>
|
|
84
|
-
</Card>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { useState } from "react"
|
|
2
|
-
import { FileCsvColumns } from "@/components/file-view/file-csv-columns-view"
|
|
3
|
-
import { FileCsvTable } from "@/components/file-view/file-csv-table"
|
|
4
|
-
import { FileHeader } from "@/components/file-view/file-header"
|
|
5
|
-
import { Button } from "@/components/ui/button"
|
|
6
|
-
|
|
7
|
-
export type Props = {
|
|
8
|
-
filePath: string
|
|
9
|
-
fileData: {
|
|
10
|
-
path: string
|
|
11
|
-
title: string | null
|
|
12
|
-
}
|
|
13
|
-
cwd: string
|
|
14
|
-
content: string
|
|
15
|
-
onChange(content: string): void
|
|
16
|
-
onReload: () => void
|
|
17
|
-
isLoading: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function CsvFileView(props: Props) {
|
|
21
|
-
const [isColumnEditMode, setIsColumnEditMode] = useState(false)
|
|
22
|
-
|
|
23
|
-
const toggleColumnEditMode = () => {
|
|
24
|
-
setIsColumnEditMode(!isColumnEditMode)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<div className="space-y-2">
|
|
29
|
-
<FileHeader
|
|
30
|
-
filePath={props.filePath}
|
|
31
|
-
fileData={props.fileData}
|
|
32
|
-
cwd={props.cwd}
|
|
33
|
-
onReload={props.onReload}
|
|
34
|
-
isLoading={props.isLoading}
|
|
35
|
-
>
|
|
36
|
-
<Button
|
|
37
|
-
onClick={toggleColumnEditMode}
|
|
38
|
-
variant={isColumnEditMode ? "default" : "secondary"}
|
|
39
|
-
>
|
|
40
|
-
{"カラム"}
|
|
41
|
-
</Button>
|
|
42
|
-
</FileHeader>
|
|
43
|
-
{isColumnEditMode ? (
|
|
44
|
-
<FileCsvColumns content={props.content} onChange={props.onChange} />
|
|
45
|
-
) : (
|
|
46
|
-
<FileCsvTable content={props.content} onChange={props.onChange} />
|
|
47
|
-
)}
|
|
48
|
-
</div>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { useRef, useState } from "react"
|
|
2
|
-
import { TableCell } from "@/components/ui/table"
|
|
3
|
-
|
|
4
|
-
type Props = {
|
|
5
|
-
content: string
|
|
6
|
-
rowIndex: number
|
|
7
|
-
columnIndex: number
|
|
8
|
-
onCellChange: (rowIndex: number, columnIndex: number, value: string) => void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function EditableCsvCell(props: Props) {
|
|
12
|
-
const [value, setValue] = useState(props.content)
|
|
13
|
-
const cellRef = useRef<HTMLTableCellElement>(null)
|
|
14
|
-
|
|
15
|
-
function handleBlur() {
|
|
16
|
-
if (value !== props.content) {
|
|
17
|
-
props.onCellChange(props.rowIndex, props.columnIndex, value)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function handleInput(event: React.FormEvent<HTMLTableCellElement>) {
|
|
22
|
-
const newValue = event.currentTarget.textContent || ""
|
|
23
|
-
setValue(newValue)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<TableCell
|
|
28
|
-
ref={cellRef}
|
|
29
|
-
contentEditable
|
|
30
|
-
suppressContentEditableWarning
|
|
31
|
-
onBlur={handleBlur}
|
|
32
|
-
onInput={handleInput}
|
|
33
|
-
>
|
|
34
|
-
{props.content}
|
|
35
|
-
</TableCell>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { Card } from "@/components/ui/card"
|
|
2
|
-
import type { DocRelation, DocSchemaRecord, DocFileMdFrontMatter } from "@/lib/types"
|
|
3
|
-
import { cn } from "@/lib/utils"
|
|
4
|
-
import { FrontMatterInputField } from "./front-matter-input-field"
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
frontMatter: DocFileMdFrontMatter | null
|
|
8
|
-
onUpdate?: (key: string, value: unknown) => void
|
|
9
|
-
schema?: DocSchemaRecord
|
|
10
|
-
relations?: DocRelation[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Front-matterを編集可能に表示するコンポーネント
|
|
15
|
-
*/
|
|
16
|
-
export function EditableFrontMatterView(props: Props) {
|
|
17
|
-
const frontMatter = props.frontMatter
|
|
18
|
-
// front-matterが空の場合は表示しない
|
|
19
|
-
if (!frontMatter || Object.keys(frontMatter).length === 0) {
|
|
20
|
-
return null
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const handleValueChange = (key: string, newValue: string) => {
|
|
24
|
-
if (!props.onUpdate) {
|
|
25
|
-
console.warn("onUpdate is not provided to EditableFrontMatterView")
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const originalValue = frontMatter[key]
|
|
30
|
-
|
|
31
|
-
// 元の型に応じて値を変換
|
|
32
|
-
let convertedValue: unknown = newValue
|
|
33
|
-
|
|
34
|
-
if (typeof originalValue === "boolean") {
|
|
35
|
-
convertedValue = newValue === "true"
|
|
36
|
-
} else if (typeof originalValue === "number") {
|
|
37
|
-
const numValue = Number(newValue)
|
|
38
|
-
convertedValue = Number.isNaN(numValue) ? originalValue : numValue
|
|
39
|
-
} else if (Array.isArray(originalValue)) {
|
|
40
|
-
// 配列の場合はカンマ区切りで分割
|
|
41
|
-
const items = newValue
|
|
42
|
-
.split(",")
|
|
43
|
-
.map((v) => v.trim())
|
|
44
|
-
.filter((v) => v !== "")
|
|
45
|
-
if (originalValue.length > 0 && typeof originalValue[0] === "number") {
|
|
46
|
-
convertedValue = items
|
|
47
|
-
.map((v) => Number(v))
|
|
48
|
-
.filter((v) => !Number.isNaN(v))
|
|
49
|
-
} else if (
|
|
50
|
-
originalValue.length > 0 &&
|
|
51
|
-
typeof originalValue[0] === "boolean"
|
|
52
|
-
) {
|
|
53
|
-
convertedValue = items.map((v) => v === "true")
|
|
54
|
-
} else {
|
|
55
|
-
convertedValue = items
|
|
56
|
-
}
|
|
57
|
-
} else if (originalValue === null && Array.isArray(newValue)) {
|
|
58
|
-
// null値だが新しい値が配列の場合(配列リレーションなど)
|
|
59
|
-
convertedValue = newValue
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
props.onUpdate(key, convertedValue)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const handleBlur = (key: string, value: string) => {
|
|
66
|
-
handleValueChange(key, value)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<Card className="gap-4 rounded-md p-2">
|
|
71
|
-
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
72
|
-
{Object.entries(frontMatter).map(([key, value]) => {
|
|
73
|
-
const schemaField = props.schema?.[key]
|
|
74
|
-
const relationData = props.relations?.find((rel) => {
|
|
75
|
-
return (
|
|
76
|
-
rel.path ===
|
|
77
|
-
(schemaField && "path" in schemaField ? schemaField.path : null)
|
|
78
|
-
)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
// multi-系のフィールドは2カラムスペースを使用
|
|
82
|
-
const isMultiField = schemaField?.type?.startsWith("multi-")
|
|
83
|
-
const columnSpan = isMultiField ? "md:col-span-2" : ""
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<div key={key} className={cn("flex flex-col gap-1", columnSpan)}>
|
|
87
|
-
<span className="font-medium text-sm">{key}</span>
|
|
88
|
-
<div className="flex-1">
|
|
89
|
-
<FrontMatterInputField
|
|
90
|
-
fieldKey={key}
|
|
91
|
-
value={value}
|
|
92
|
-
originalValue={frontMatter[key]}
|
|
93
|
-
onValueChange={handleValueChange}
|
|
94
|
-
onBlur={handleBlur}
|
|
95
|
-
schemaField={schemaField}
|
|
96
|
-
relationOptions={relationData?.files}
|
|
97
|
-
/>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
)
|
|
101
|
-
})}
|
|
102
|
-
</div>
|
|
103
|
-
</Card>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { ArrowDown, ArrowUp, Check, Plus, XCircle } from "lucide-react"
|
|
2
|
-
import { type FormEvent, useEffect, useState } from "react"
|
|
3
|
-
import { Button } from "@/components/ui/button"
|
|
4
|
-
import { Card } from "@/components/ui/card"
|
|
5
|
-
import { Input } from "@/components/ui/input"
|
|
6
|
-
import { OpenCsv } from "@/lib/open-csv/open-csv"
|
|
7
|
-
import { cn } from "@/lib/utils"
|
|
8
|
-
|
|
9
|
-
type Props = {
|
|
10
|
-
content: string
|
|
11
|
-
onChange(content: string): void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function FileCsvColumns(props: Props) {
|
|
15
|
-
const [csvInstance, setCsvInstance] = useState<OpenCsv>(
|
|
16
|
-
new OpenCsv(props.content),
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
const [editingHeader, setEditingHeader] = useState<{
|
|
20
|
-
index: number
|
|
21
|
-
value: string
|
|
22
|
-
} | null>(null)
|
|
23
|
-
|
|
24
|
-
const [newColumnName, setNewColumnName] = useState("")
|
|
25
|
-
|
|
26
|
-
// propsの変更を検知してCSVインスタンスを更新
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
setCsvInstance(new OpenCsv(props.content))
|
|
29
|
-
}, [props.content])
|
|
30
|
-
|
|
31
|
-
// カラム名の編集モードを開始
|
|
32
|
-
const startEditingHeader = (index: number, headerName: string) => {
|
|
33
|
-
setEditingHeader({ index, value: headerName })
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// カラム名の編集を確定
|
|
37
|
-
const confirmHeaderEdit = () => {
|
|
38
|
-
if (editingHeader && editingHeader.value.trim() !== "") {
|
|
39
|
-
const newCsvInstance = csvInstance.renameColumn(
|
|
40
|
-
editingHeader.index,
|
|
41
|
-
editingHeader.value,
|
|
42
|
-
)
|
|
43
|
-
updateCsvAndNotify(newCsvInstance)
|
|
44
|
-
setEditingHeader(null)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// カラムを削除
|
|
49
|
-
const removeColumn = (index: number) => {
|
|
50
|
-
const newCsvInstance = csvInstance.removeColumn(index)
|
|
51
|
-
updateCsvAndNotify(newCsvInstance)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// カラムを上に移動
|
|
55
|
-
const moveColumnUp = (index: number) => {
|
|
56
|
-
if (index > 0) {
|
|
57
|
-
const newCsvInstance = csvInstance.moveColumn(index, index - 1)
|
|
58
|
-
updateCsvAndNotify(newCsvInstance)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// カラムを下に移動
|
|
63
|
-
const moveColumnDown = (index: number) => {
|
|
64
|
-
if (index < csvInstance.headers.length - 1) {
|
|
65
|
-
const newCsvInstance = csvInstance.moveColumn(index, index + 1)
|
|
66
|
-
updateCsvAndNotify(newCsvInstance)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 新しいカラムを追加
|
|
71
|
-
const addNewColumn = (event?: FormEvent) => {
|
|
72
|
-
if (event) {
|
|
73
|
-
event.preventDefault()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (newColumnName.trim() !== "") {
|
|
77
|
-
const newCsvInstance = csvInstance.addColumn(newColumnName)
|
|
78
|
-
updateCsvAndNotify(newCsvInstance)
|
|
79
|
-
setNewColumnName("")
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// CSVインスタンスを更新し、親コンポーネントに通知
|
|
84
|
-
const updateCsvAndNotify = (newCsvInstance: OpenCsv) => {
|
|
85
|
-
setCsvInstance(newCsvInstance)
|
|
86
|
-
if (props.onChange) {
|
|
87
|
-
props.onChange(newCsvInstance.toString())
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<Card className="h-full max-h-fit max-w-none gap-0 rounded-md p-0">
|
|
93
|
-
<div className="flex items-center justify-end border-b p-2">
|
|
94
|
-
<form className="flex items-center space-x-2" onSubmit={addNewColumn}>
|
|
95
|
-
<Input
|
|
96
|
-
className="h-9 w-40"
|
|
97
|
-
onChange={(e) => setNewColumnName(e.target.value)}
|
|
98
|
-
placeholder="新しいカラム名"
|
|
99
|
-
value={newColumnName}
|
|
100
|
-
/>
|
|
101
|
-
<Button
|
|
102
|
-
disabled={newColumnName.trim() === ""}
|
|
103
|
-
size="sm"
|
|
104
|
-
type="submit"
|
|
105
|
-
>
|
|
106
|
-
<Plus className="mr-1 h-4 w-4" />
|
|
107
|
-
{"追加"}
|
|
108
|
-
</Button>
|
|
109
|
-
</form>
|
|
110
|
-
</div>
|
|
111
|
-
<div className="flex flex-col gap-2 p-2">
|
|
112
|
-
{csvInstance.headers.map((header, headerIndex) => (
|
|
113
|
-
<Card
|
|
114
|
-
className={"relative gap-0 p-2"}
|
|
115
|
-
key={`column-${header}-${headerIndex.toFixed()}`}
|
|
116
|
-
>
|
|
117
|
-
<div className="flex items-center space-x-2">
|
|
118
|
-
<Input
|
|
119
|
-
autoFocus={editingHeader?.index === headerIndex}
|
|
120
|
-
className="h-8"
|
|
121
|
-
onChange={(e) =>
|
|
122
|
-
editingHeader?.index === headerIndex
|
|
123
|
-
? setEditingHeader({
|
|
124
|
-
...editingHeader,
|
|
125
|
-
value: e.target.value,
|
|
126
|
-
})
|
|
127
|
-
: startEditingHeader(headerIndex, e.target.value)
|
|
128
|
-
}
|
|
129
|
-
value={
|
|
130
|
-
editingHeader?.index === headerIndex
|
|
131
|
-
? editingHeader.value
|
|
132
|
-
: header
|
|
133
|
-
}
|
|
134
|
-
/>
|
|
135
|
-
{editingHeader?.index === headerIndex && (
|
|
136
|
-
<Button
|
|
137
|
-
className="h-8 w-8"
|
|
138
|
-
onClick={confirmHeaderEdit}
|
|
139
|
-
size="icon"
|
|
140
|
-
variant="ghost"
|
|
141
|
-
>
|
|
142
|
-
<Check className="h-4 w-4" />
|
|
143
|
-
</Button>
|
|
144
|
-
)}
|
|
145
|
-
<Button
|
|
146
|
-
className="h-7 w-7"
|
|
147
|
-
disabled={headerIndex === 0}
|
|
148
|
-
onClick={() => moveColumnUp(headerIndex)}
|
|
149
|
-
size="icon"
|
|
150
|
-
variant="outline"
|
|
151
|
-
>
|
|
152
|
-
<ArrowUp className="h-3 w-3" />
|
|
153
|
-
</Button>
|
|
154
|
-
<Button
|
|
155
|
-
className="h-7 w-7"
|
|
156
|
-
disabled={headerIndex === csvInstance.headers.length - 1}
|
|
157
|
-
onClick={() => moveColumnDown(headerIndex)}
|
|
158
|
-
size="icon"
|
|
159
|
-
variant="outline"
|
|
160
|
-
>
|
|
161
|
-
<ArrowDown className="h-3 w-3" />
|
|
162
|
-
</Button>
|
|
163
|
-
<Button
|
|
164
|
-
className={cn(
|
|
165
|
-
"h-7 w-7",
|
|
166
|
-
csvInstance.headers.length <= 1 && "opacity-50",
|
|
167
|
-
)}
|
|
168
|
-
disabled={csvInstance.headers.length <= 1}
|
|
169
|
-
onClick={() => removeColumn(headerIndex)}
|
|
170
|
-
size="icon"
|
|
171
|
-
variant="outline"
|
|
172
|
-
>
|
|
173
|
-
<XCircle className="h-3 w-3" />
|
|
174
|
-
</Button>
|
|
175
|
-
</div>
|
|
176
|
-
</Card>
|
|
177
|
-
))}
|
|
178
|
-
</div>
|
|
179
|
-
</Card>
|
|
180
|
-
)
|
|
181
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { DragEndEvent } from "@dnd-kit/core"
|
|
2
|
-
import {
|
|
3
|
-
closestCenter,
|
|
4
|
-
DndContext,
|
|
5
|
-
KeyboardSensor,
|
|
6
|
-
PointerSensor,
|
|
7
|
-
useSensor,
|
|
8
|
-
useSensors,
|
|
9
|
-
} from "@dnd-kit/core"
|
|
10
|
-
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
|
|
11
|
-
import {
|
|
12
|
-
arrayMove,
|
|
13
|
-
SortableContext,
|
|
14
|
-
verticalListSortingStrategy,
|
|
15
|
-
} from "@dnd-kit/sortable"
|
|
16
|
-
import { useState } from "react"
|
|
17
|
-
import { Card } from "@/components/ui/card"
|
|
18
|
-
import {
|
|
19
|
-
Table,
|
|
20
|
-
TableBody,
|
|
21
|
-
TableHead,
|
|
22
|
-
TableHeader,
|
|
23
|
-
TableRow,
|
|
24
|
-
} from "@/components/ui/table"
|
|
25
|
-
import { OpenCsv } from "@/lib/open-csv/open-csv"
|
|
26
|
-
import { SortableCsvRow } from "./sortable-csv-row"
|
|
27
|
-
|
|
28
|
-
type Props = {
|
|
29
|
-
content: string
|
|
30
|
-
onChange(content: string): void
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function FileCsvTable(props: Props) {
|
|
34
|
-
const [csvData, setCsvData] = useState(new OpenCsv(props.content))
|
|
35
|
-
|
|
36
|
-
const sensors = useSensors(
|
|
37
|
-
useSensor(PointerSensor, {
|
|
38
|
-
activationConstraint: {
|
|
39
|
-
distance: 5,
|
|
40
|
-
},
|
|
41
|
-
}),
|
|
42
|
-
useSensor(KeyboardSensor),
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
function handleDragEnd(event: DragEndEvent) {
|
|
46
|
-
const active = event.active
|
|
47
|
-
const over = event.over
|
|
48
|
-
|
|
49
|
-
if (over && active.id !== over.id) {
|
|
50
|
-
const activeId = String(active.id)
|
|
51
|
-
const overId = String(over.id)
|
|
52
|
-
|
|
53
|
-
const activeSplit = activeId.split("-")
|
|
54
|
-
const overSplit = overId.split("-")
|
|
55
|
-
|
|
56
|
-
if (activeSplit.length < 2 || overSplit.length < 2) {
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const activeIndex = activeSplit[1]
|
|
61
|
-
const overIndex = overSplit[1]
|
|
62
|
-
|
|
63
|
-
if (activeIndex === undefined || overIndex === undefined) {
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const activeRowIndex = Number.parseInt(activeIndex, 10) + 1 // +1 for header row
|
|
68
|
-
const overRowIndex = Number.parseInt(overIndex, 10) + 1
|
|
69
|
-
|
|
70
|
-
const newRecords = arrayMove(
|
|
71
|
-
csvData.records,
|
|
72
|
-
activeRowIndex - 1,
|
|
73
|
-
overRowIndex - 1,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
// Create a new CSV string to initialize a new OpenCsv instance
|
|
77
|
-
const headers = csvData.headers
|
|
78
|
-
if (headers.length > 0) {
|
|
79
|
-
const newData = [headers, ...newRecords.map((row) => [...row])]
|
|
80
|
-
const newCsvString = newData.map((row) => row.join(",")).join("\n")
|
|
81
|
-
setCsvData(new OpenCsv(newCsvString))
|
|
82
|
-
|
|
83
|
-
// 親コンポーネントに変更を通知
|
|
84
|
-
props.onChange(newCsvString)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
} // Create unique and stable ids for each row
|
|
88
|
-
const rowIds = csvData.records.map((_, idx) => `row-${idx}`)
|
|
89
|
-
|
|
90
|
-
function handleCellUpdate(
|
|
91
|
-
rowIndex: number,
|
|
92
|
-
columnIndex: number,
|
|
93
|
-
value: string,
|
|
94
|
-
) {
|
|
95
|
-
const newCsvData = csvData.updateCell(rowIndex + 1, columnIndex, value) // +1 for header row
|
|
96
|
-
const newCsvString = newCsvData.toString()
|
|
97
|
-
setCsvData(newCsvData)
|
|
98
|
-
props.onChange(newCsvString)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<Card className="h-full max-h-fit max-w-none overflow-hidden rounded-md p-0">
|
|
103
|
-
<Table className="w-full border-collapse">
|
|
104
|
-
<TableHeader>
|
|
105
|
-
<TableRow>
|
|
106
|
-
<TableHead />
|
|
107
|
-
{csvData.headers.map((header: string, index: number) => {
|
|
108
|
-
const headerKey = `header-${index}-${header}`
|
|
109
|
-
return <TableHead key={headerKey}>{header}</TableHead>
|
|
110
|
-
})}
|
|
111
|
-
</TableRow>
|
|
112
|
-
</TableHeader>
|
|
113
|
-
<DndContext
|
|
114
|
-
sensors={sensors}
|
|
115
|
-
collisionDetection={closestCenter}
|
|
116
|
-
onDragEnd={handleDragEnd}
|
|
117
|
-
modifiers={[restrictToVerticalAxis]}
|
|
118
|
-
>
|
|
119
|
-
<TableBody>
|
|
120
|
-
<SortableContext
|
|
121
|
-
items={rowIds}
|
|
122
|
-
strategy={verticalListSortingStrategy}
|
|
123
|
-
>
|
|
124
|
-
{csvData.records.map((row: string[], rowIndex: number) => {
|
|
125
|
-
const rowId = `row-${rowIndex}`
|
|
126
|
-
return (
|
|
127
|
-
<SortableCsvRow
|
|
128
|
-
key={rowId}
|
|
129
|
-
id={rowId}
|
|
130
|
-
row={row}
|
|
131
|
-
rowIndex={rowIndex}
|
|
132
|
-
onCellUpdate={handleCellUpdate}
|
|
133
|
-
/>
|
|
134
|
-
)
|
|
135
|
-
})}
|
|
136
|
-
</SortableContext>
|
|
137
|
-
</TableBody>
|
|
138
|
-
</DndContext>
|
|
139
|
-
</Table>
|
|
140
|
-
</Card>
|
|
141
|
-
)
|
|
142
|
-
}
|