@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,50 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query"
|
|
2
|
+
import { useNavigate } from "@tanstack/react-router"
|
|
3
|
+
import { ArrowLeft } from "lucide-react"
|
|
4
|
+
import { useState, useEffect } from "react"
|
|
5
|
+
import { ReloadButton } from "@/components/reload-button"
|
|
6
|
+
import { SidebarButton } from "@/components/sidebar-button"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import { Input } from "@/components/ui/input"
|
|
9
|
+
import { VscodeButton } from "@/components/vscode-button"
|
|
10
|
+
import { apiClient } from "@/lib/api-client"
|
|
11
|
+
import { normalizePath } from "@/lib/path-utils"
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
filePath: string
|
|
15
|
+
fileData: {
|
|
16
|
+
path: string
|
|
17
|
+
title: string | null
|
|
18
|
+
}
|
|
19
|
+
cwd: string
|
|
20
|
+
onReload: () => void
|
|
21
|
+
isLoading: boolean
|
|
22
|
+
children?: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function FileHeader(props: Props) {
|
|
26
|
+
const navigate = useNavigate()
|
|
27
|
+
|
|
28
|
+
const getInitialTitle = () => {
|
|
29
|
+
if (props.fileData.title) return props.fileData.title
|
|
30
|
+
return props.filePath.split("/").pop()?.replace(/\.md$/, "") || ""
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [title, setTitle] = useState(getInitialTitle())
|
|
34
|
+
|
|
35
|
+
// propsが変更されたときにタイトルを更新
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setTitle(getInitialTitle())
|
|
38
|
+
}, [props.fileData.title, props.filePath])
|
|
39
|
+
|
|
40
|
+
const updateTitleMutation = useMutation({
|
|
41
|
+
async mutationFn(newTitle: string) {
|
|
42
|
+
const normalizedPath = normalizePath(props.filePath)
|
|
43
|
+
|
|
44
|
+
await apiClient.api.files[":path{.+}"].$put({
|
|
45
|
+
param: {
|
|
46
|
+
path: normalizedPath,
|
|
47
|
+
},
|
|
48
|
+
json: {
|
|
49
|
+
title: newTitle,
|
|
50
|
+
properties: null,
|
|
51
|
+
content: null,
|
|
52
|
+
description: null,
|
|
53
|
+
isArchived: null,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
},
|
|
57
|
+
onSuccess() {
|
|
58
|
+
props.onReload()
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
63
|
+
const newTitle = e.target.value
|
|
64
|
+
setTitle(newTitle)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const handleTitleBlur = async () => {
|
|
68
|
+
if (props.fileData.path.endsWith(".md")) {
|
|
69
|
+
updateTitleMutation.mutate(title)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleBackClick = () => {
|
|
74
|
+
const pathSegments = props.filePath.split("/")
|
|
75
|
+
pathSegments.pop()
|
|
76
|
+
|
|
77
|
+
// アーカイブディレクトリ(_)を削除
|
|
78
|
+
const filteredSegments = pathSegments.filter((segment) => segment !== "_")
|
|
79
|
+
const directoryPath = filteredSegments.join("/")
|
|
80
|
+
|
|
81
|
+
navigate({ to: `/${directoryPath}` })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
<SidebarButton />
|
|
88
|
+
<VscodeButton
|
|
89
|
+
cwd={props.cwd}
|
|
90
|
+
filePath={props.fileData.path}
|
|
91
|
+
size="icon"
|
|
92
|
+
variant="outline"
|
|
93
|
+
/>
|
|
94
|
+
<Button onClick={handleBackClick} size="icon" variant="outline">
|
|
95
|
+
<ArrowLeft className="h-4 w-4" />
|
|
96
|
+
</Button>
|
|
97
|
+
|
|
98
|
+
<Input
|
|
99
|
+
value={title}
|
|
100
|
+
onChange={handleTitleChange}
|
|
101
|
+
onBlur={handleTitleBlur}
|
|
102
|
+
placeholder="タイトルを入力"
|
|
103
|
+
className="flex-1"
|
|
104
|
+
/>
|
|
105
|
+
<ReloadButton
|
|
106
|
+
onReload={props.onReload}
|
|
107
|
+
size="icon"
|
|
108
|
+
variant="outline"
|
|
109
|
+
disabled={props.isLoading}
|
|
110
|
+
/>
|
|
111
|
+
{props.children}
|
|
112
|
+
</div>
|
|
113
|
+
</>
|
|
114
|
+
)
|
|
115
|
+
}
|