@skspwork/config-doc 2.0.4 → 2.0.5
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/package.json +2 -2
- package/packages/web/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/packages/web/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/packages/web/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/packages/web/.next/standalone/.next/server/app/api/config/save/route.js +1 -1
- package/packages/web/.next/standalone/.next/server/app/api/config/save/route.js.nft.json +1 -1
- package/packages/web/.next/standalone/.next/server/app/api/export/route.js +3 -3
- package/packages/web/.next/standalone/.next/server/app/api/export/route.js.nft.json +1 -1
- package/packages/web/.next/standalone/.next/server/app/index.html +1 -1
- package/packages/web/.next/standalone/.next/server/app/index.rsc +3 -3
- package/packages/web/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/packages/web/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/packages/web/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__40e87302._.js +3 -0
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__93da9fce._.js +1 -1
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__c9655ac8._.js +3 -0
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__e19366f6._.js +1 -1
- package/packages/web/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d09de205.js +345 -27
- package/packages/web/.next/standalone/.next/server/chunks/ssr/app_page_tsx_55b2e5ee._.js +1 -1
- package/packages/web/.next/standalone/.next/server/pages/404.html +1 -1
- package/packages/web/.next/standalone/.next/static/chunks/02de70e4c30afe2f.js +1 -0
- package/packages/web/.next/standalone/.next/static/chunks/862e384b52cfebf3.css +3 -0
- package/packages/web/.next/standalone/app/api/config/metadata/route.ts +5 -3
- package/packages/web/.next/standalone/playwright-report/index.html +1 -1
- package/packages/web/.next/static/chunks/02de70e4c30afe2f.js +1 -0
- package/packages/web/.next/static/chunks/862e384b52cfebf3.css +3 -0
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__1a68b1f3._.js +0 -3
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__2c94dfea._.js +0 -3
- package/packages/web/.next/standalone/.next/static/chunks/9726c2cde77e0916.js +0 -1
- package/packages/web/.next/standalone/.next/static/chunks/cd878566fda12635.css +0 -3
- package/packages/web/.next/standalone/app/api/config/load/route.ts +0 -57
- package/packages/web/.next/standalone/app/api/config/save/route.ts +0 -73
- package/packages/web/.next/standalone/app/api/export/route.ts +0 -75
- package/packages/web/.next/standalone/app/api/export/settings/route.ts +0 -144
- package/packages/web/.next/standalone/app/api/files/browse/route.ts +0 -46
- package/packages/web/.next/standalone/app/api/project/route.ts +0 -41
- package/packages/web/.next/standalone/app/globals.css +0 -26
- package/packages/web/.next/standalone/app/icon.svg +0 -41
- package/packages/web/.next/standalone/app/layout.tsx +0 -34
- package/packages/web/.next/standalone/app/page.tsx +0 -135
- package/packages/web/.next/standalone/components/ConfigFileTabs.tsx +0 -188
- package/packages/web/.next/standalone/components/ConfigTree.tsx +0 -176
- package/packages/web/.next/standalone/components/EditableList.tsx +0 -337
- package/packages/web/.next/standalone/components/ExportDialog.tsx +0 -234
- package/packages/web/.next/standalone/components/FieldsEditor.tsx +0 -92
- package/packages/web/.next/standalone/components/FileBrowser.tsx +0 -290
- package/packages/web/.next/standalone/components/Header.tsx +0 -37
- package/packages/web/.next/standalone/components/PropertyEditor.tsx +0 -102
- package/packages/web/.next/standalone/components/TagEditor.tsx +0 -86
- package/packages/web/.next/standalone/components/Toast.tsx +0 -91
- package/packages/web/.next/standalone/eslint.config.mjs +0 -18
- package/packages/web/.next/standalone/hooks/useConfigManager.ts +0 -653
- package/packages/web/.next/standalone/lib/configManagerUtils.ts +0 -84
- package/packages/web/.next/standalone/lib/configParser.ts +0 -155
- package/packages/web/.next/standalone/lib/fileSystem.ts +0 -186
- package/packages/web/.next/standalone/lib/getRootPath.ts +0 -45
- package/packages/web/.next/standalone/lib/htmlGenerator.ts +0 -865
- package/packages/web/.next/standalone/lib/jsonUtils.ts +0 -26
- package/packages/web/.next/standalone/lib/markdownGenerator.ts +0 -110
- package/packages/web/.next/standalone/lib/markdownTableGenerator.ts +0 -103
- package/packages/web/.next/standalone/lib/storage.ts +0 -104
- package/packages/web/.next/standalone/lib/utils.ts +0 -89
- package/packages/web/.next/standalone/next.config.ts +0 -10
- package/packages/web/.next/standalone/package-lock.json +0 -8216
- package/packages/web/.next/standalone/playwright.config.ts +0 -27
- package/packages/web/.next/standalone/postcss.config.mjs +0 -7
- package/packages/web/.next/standalone/test-results/.last-run.json +0 -4
- package/packages/web/.next/standalone/tsconfig.json +0 -34
- package/packages/web/.next/standalone/tsconfig.tsbuildinfo +0 -1
- package/packages/web/.next/standalone/types/index.ts +0 -74
- package/packages/web/.next/standalone/vitest.config.ts +0 -14
- package/packages/web/.next/static/chunks/9726c2cde77e0916.js +0 -1
- package/packages/web/.next/static/chunks/cd878566fda12635.css +0 -3
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { FolderOpenIcon, XIcon } from 'lucide-react';
|
|
5
|
-
import { ConfigDocs } from '@/types';
|
|
6
|
-
|
|
7
|
-
export interface LoadedConfig {
|
|
8
|
-
filePath: string;
|
|
9
|
-
configData: unknown;
|
|
10
|
-
docs: ConfigDocs;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface ConfigFileTabsProps {
|
|
14
|
-
loadedConfigs: LoadedConfig[];
|
|
15
|
-
activeConfigIndex: number;
|
|
16
|
-
onTabClick: (index: number) => void;
|
|
17
|
-
onRemoveConfig: (index: number) => void;
|
|
18
|
-
onAddFileClick: () => void;
|
|
19
|
-
onReorder: (newConfigs: LoadedConfig[], newActiveIndex: number) => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 同名ファイルを区別するための表示名を生成
|
|
23
|
-
function getDisplayName(filePath: string, allFilePaths: string[]): string {
|
|
24
|
-
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
|
25
|
-
|
|
26
|
-
// 同じファイル名を持つファイルパスを検索
|
|
27
|
-
const sameNamePaths = allFilePaths.filter(p => {
|
|
28
|
-
const name = p.split(/[/\\]/).pop();
|
|
29
|
-
return name === fileName;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// 同名のファイルが1つしかない場合はファイル名のみ
|
|
33
|
-
if (sameNamePaths.length === 1) {
|
|
34
|
-
return fileName;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 同名ファイルが複数ある場合、親フォルダを追加
|
|
38
|
-
const pathParts = filePath.split(/[/\\]/);
|
|
39
|
-
|
|
40
|
-
// 必要な親フォルダのレベル数を決定
|
|
41
|
-
for (let depth = 1; depth < pathParts.length; depth++) {
|
|
42
|
-
const displayWithParents = pathParts.slice(-depth - 1).join('/');
|
|
43
|
-
|
|
44
|
-
// この表示名が他の同名ファイルと区別できるかチェック
|
|
45
|
-
const conflicts = sameNamePaths.filter(p => {
|
|
46
|
-
const otherParts = p.split(/[/\\]/);
|
|
47
|
-
const otherDisplay = otherParts.slice(-depth - 1).join('/');
|
|
48
|
-
return displayWithParents === otherDisplay && p !== filePath;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (conflicts.length === 0) {
|
|
52
|
-
return displayWithParents;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 全パスが必要な場合
|
|
57
|
-
return filePath;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function ConfigFileTabs({
|
|
61
|
-
loadedConfigs,
|
|
62
|
-
activeConfigIndex,
|
|
63
|
-
onTabClick,
|
|
64
|
-
onRemoveConfig,
|
|
65
|
-
onAddFileClick,
|
|
66
|
-
onReorder
|
|
67
|
-
}: ConfigFileTabsProps) {
|
|
68
|
-
// ドラッグ&ドロップ用の状態
|
|
69
|
-
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
|
70
|
-
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
71
|
-
|
|
72
|
-
const handleDragStart = (index: number) => {
|
|
73
|
-
setDragIndex(index);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const handleDragOver = (e: React.DragEvent, index: number) => {
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
if (dragIndex !== null && dragIndex !== index) {
|
|
79
|
-
setDragOverIndex(index);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const handleDragLeave = () => {
|
|
84
|
-
setDragOverIndex(null);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const handleDrop = (index: number) => {
|
|
88
|
-
if (dragIndex === null || dragIndex === index) {
|
|
89
|
-
setDragIndex(null);
|
|
90
|
-
setDragOverIndex(null);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 配列の並び替え
|
|
95
|
-
const newConfigs = [...loadedConfigs];
|
|
96
|
-
const [draggedItem] = newConfigs.splice(dragIndex, 1);
|
|
97
|
-
newConfigs.splice(index, 0, draggedItem);
|
|
98
|
-
|
|
99
|
-
// アクティブなインデックスを調整
|
|
100
|
-
let newActiveIndex = activeConfigIndex;
|
|
101
|
-
if (activeConfigIndex === dragIndex) {
|
|
102
|
-
newActiveIndex = index;
|
|
103
|
-
} else if (dragIndex < activeConfigIndex && index >= activeConfigIndex) {
|
|
104
|
-
newActiveIndex = activeConfigIndex - 1;
|
|
105
|
-
} else if (dragIndex > activeConfigIndex && index <= activeConfigIndex) {
|
|
106
|
-
newActiveIndex = activeConfigIndex + 1;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
onReorder(newConfigs, newActiveIndex);
|
|
110
|
-
|
|
111
|
-
setDragIndex(null);
|
|
112
|
-
setDragOverIndex(null);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const handleDragEnd = () => {
|
|
116
|
-
setDragIndex(null);
|
|
117
|
-
setDragOverIndex(null);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const allFilePaths = loadedConfigs.map(c => c.filePath);
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div className="bg-white/90 backdrop-blur-sm rounded-2xl shadow-xl border border-gray-100 p-6 mb-8 hover:shadow-2xl transition-shadow duration-300">
|
|
124
|
-
<div className="flex items-center justify-between mb-4">
|
|
125
|
-
<div className="flex items-center gap-2">
|
|
126
|
-
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center">
|
|
127
|
-
<FolderOpenIcon className="w-5 h-5 text-white" />
|
|
128
|
-
</div>
|
|
129
|
-
<h2 className="text-xl font-bold text-gray-800">設定ファイル</h2>
|
|
130
|
-
</div>
|
|
131
|
-
<button
|
|
132
|
-
onClick={onAddFileClick}
|
|
133
|
-
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-lg hover:from-blue-600 hover:to-blue-700 shadow-md hover:shadow-lg transition-all duration-200 transform"
|
|
134
|
-
>
|
|
135
|
-
<FolderOpenIcon className="w-5 h-5" />
|
|
136
|
-
<span className="font-medium">ファイルを追加</span>
|
|
137
|
-
</button>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
{loadedConfigs.length > 0 ? (
|
|
141
|
-
<div className="flex flex-wrap gap-3">
|
|
142
|
-
{loadedConfigs.map((config, index) => (
|
|
143
|
-
<div
|
|
144
|
-
key={config.filePath}
|
|
145
|
-
draggable
|
|
146
|
-
onDragStart={() => handleDragStart(index)}
|
|
147
|
-
onDragOver={(e) => handleDragOver(e, index)}
|
|
148
|
-
onDragLeave={handleDragLeave}
|
|
149
|
-
onDrop={() => handleDrop(index)}
|
|
150
|
-
onDragEnd={handleDragEnd}
|
|
151
|
-
onClick={() => onTabClick(index)}
|
|
152
|
-
className={`group flex items-center gap-2 px-4 py-2.5 rounded-xl border-2 cursor-grab transition-all duration-200 ${
|
|
153
|
-
activeConfigIndex === index
|
|
154
|
-
? 'bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-400 shadow-md'
|
|
155
|
-
: 'bg-white border-gray-200 hover:border-blue-300 hover:shadow-md'
|
|
156
|
-
} ${
|
|
157
|
-
dragIndex === index ? 'opacity-50' : ''
|
|
158
|
-
} ${
|
|
159
|
-
dragOverIndex === index ? 'border-blue-500 border-dashed' : ''
|
|
160
|
-
}`}
|
|
161
|
-
>
|
|
162
|
-
<span className={`text-sm font-medium ${
|
|
163
|
-
activeConfigIndex === index ? 'text-blue-700' : 'text-gray-700'
|
|
164
|
-
}`}>
|
|
165
|
-
{getDisplayName(config.filePath, allFilePaths)}
|
|
166
|
-
</span>
|
|
167
|
-
<button
|
|
168
|
-
onClick={(e) => {
|
|
169
|
-
e.stopPropagation();
|
|
170
|
-
onRemoveConfig(index);
|
|
171
|
-
}}
|
|
172
|
-
className="text-gray-400 hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100"
|
|
173
|
-
>
|
|
174
|
-
<XIcon className="w-4 h-4" />
|
|
175
|
-
</button>
|
|
176
|
-
</div>
|
|
177
|
-
))}
|
|
178
|
-
</div>
|
|
179
|
-
) : (
|
|
180
|
-
<div className="text-center py-12 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl border-2 border-dashed border-gray-300">
|
|
181
|
-
<FolderOpenIcon className="w-16 h-16 text-gray-400 mx-auto mb-3" />
|
|
182
|
-
<p className="text-sm text-gray-600 font-medium">設定ファイルを選択してください</p>
|
|
183
|
-
<p className="text-xs text-gray-500 mt-1">「ファイルを追加」ボタンから開始</p>
|
|
184
|
-
</div>
|
|
185
|
-
)}
|
|
186
|
-
</div>
|
|
187
|
-
);
|
|
188
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { ConfigTreeNode, ConfigDocs } from '@/types';
|
|
5
|
-
import { ChevronRightIcon, ChevronDownIcon, FileTextIcon } from 'lucide-react';
|
|
6
|
-
import { ConfigParser } from '@/lib/configParser';
|
|
7
|
-
|
|
8
|
-
interface ConfigTreeProps {
|
|
9
|
-
config: any;
|
|
10
|
-
docs: ConfigDocs;
|
|
11
|
-
onSelectProperty: (path: string) => void;
|
|
12
|
-
onEditProperty: (path: string) => void;
|
|
13
|
-
selectedPath?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function ConfigTree({
|
|
17
|
-
config,
|
|
18
|
-
docs,
|
|
19
|
-
onSelectProperty,
|
|
20
|
-
onEditProperty,
|
|
21
|
-
selectedPath
|
|
22
|
-
}: ConfigTreeProps) {
|
|
23
|
-
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
|
24
|
-
|
|
25
|
-
const toggleNode = (path: string) => {
|
|
26
|
-
const newExpanded = new Set(expandedNodes);
|
|
27
|
-
if (newExpanded.has(path)) {
|
|
28
|
-
newExpanded.delete(path);
|
|
29
|
-
} else {
|
|
30
|
-
newExpanded.add(path);
|
|
31
|
-
}
|
|
32
|
-
setExpandedNodes(newExpanded);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// すべてのノードパスを収集
|
|
36
|
-
const getAllNodePaths = (nodes: ConfigTreeNode[]): string[] => {
|
|
37
|
-
const paths: string[] = [];
|
|
38
|
-
const collectPaths = (node: ConfigTreeNode) => {
|
|
39
|
-
if (node.children && node.children.length > 0) {
|
|
40
|
-
paths.push(node.fullPath);
|
|
41
|
-
node.children.forEach(collectPaths);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
nodes.forEach(collectPaths);
|
|
45
|
-
return paths;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// すべて展開
|
|
49
|
-
const expandAll = () => {
|
|
50
|
-
const tree = ConfigParser.buildTree(config);
|
|
51
|
-
const allPaths = getAllNodePaths(tree);
|
|
52
|
-
setExpandedNodes(new Set(allPaths));
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// すべて閉じる
|
|
56
|
-
const collapseAll = () => {
|
|
57
|
-
setExpandedNodes(new Set());
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// ドキュメントが有効な内容を持っているかチェック
|
|
61
|
-
const hasValidDocumentation = (path: string): boolean => {
|
|
62
|
-
const doc = docs.properties[path];
|
|
63
|
-
if (!doc) return false;
|
|
64
|
-
|
|
65
|
-
// タグがあるか
|
|
66
|
-
if (doc.tags && doc.tags.length > 0) return true;
|
|
67
|
-
|
|
68
|
-
// フィールドに値があるか
|
|
69
|
-
if (doc.fields) {
|
|
70
|
-
const hasFieldValue = Object.values(doc.fields).some(
|
|
71
|
-
value => value && value.trim() !== ''
|
|
72
|
-
);
|
|
73
|
-
if (hasFieldValue) return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return false;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const renderNode = (node: ConfigTreeNode, depth: number = 0) => {
|
|
80
|
-
const hasDoc = hasValidDocumentation(node.fullPath);
|
|
81
|
-
const isExpanded = expandedNodes.has(node.fullPath);
|
|
82
|
-
const isSelected = selectedPath === node.fullPath;
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<div key={node.fullPath}>
|
|
86
|
-
<div
|
|
87
|
-
style={{ paddingLeft: `${depth * 20}px` }}
|
|
88
|
-
className={`group flex items-center gap-2 py-2 px-3 cursor-pointer rounded-lg transition-all duration-150 ${
|
|
89
|
-
isSelected
|
|
90
|
-
? 'bg-gradient-to-r from-blue-100 to-indigo-100 border-l-4 border-blue-500 shadow-sm'
|
|
91
|
-
: 'hover:bg-gradient-to-r hover:from-gray-50 hover:to-gray-100'
|
|
92
|
-
}`}
|
|
93
|
-
>
|
|
94
|
-
{node.children && node.children.length > 0 ? (
|
|
95
|
-
<button
|
|
96
|
-
onClick={() => toggleNode(node.fullPath)}
|
|
97
|
-
className="p-1 hover:bg-blue-100 rounded-md transition-colors"
|
|
98
|
-
>
|
|
99
|
-
{isExpanded ? (
|
|
100
|
-
<ChevronDownIcon className="w-4 h-4 text-blue-600" />
|
|
101
|
-
) : (
|
|
102
|
-
<ChevronRightIcon className="w-4 h-4 text-gray-600" />
|
|
103
|
-
)}
|
|
104
|
-
</button>
|
|
105
|
-
) : (
|
|
106
|
-
<div className="w-6" />
|
|
107
|
-
)}
|
|
108
|
-
|
|
109
|
-
<div
|
|
110
|
-
onClick={() => onSelectProperty(node.fullPath)}
|
|
111
|
-
className="flex-1 flex items-center gap-2"
|
|
112
|
-
>
|
|
113
|
-
<span className={`${node.children ? 'font-bold text-gray-800' : 'font-medium text-gray-700'}`}>
|
|
114
|
-
{node.key}
|
|
115
|
-
</span>
|
|
116
|
-
{!node.children && (
|
|
117
|
-
<span className="text-sm text-gray-500 truncate max-w-[200px]">
|
|
118
|
-
: {JSON.stringify(node.value)}
|
|
119
|
-
</span>
|
|
120
|
-
)}
|
|
121
|
-
{hasDoc && (
|
|
122
|
-
<div className="flex items-center gap-1 px-2 py-0.5 bg-green-100 border border-green-300 rounded-full">
|
|
123
|
-
<FileTextIcon className="w-3 h-3 text-green-600" />
|
|
124
|
-
<span className="text-xs text-green-700 font-medium">Doc</span>
|
|
125
|
-
</div>
|
|
126
|
-
)}
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
{!node.children && (
|
|
130
|
-
<button
|
|
131
|
-
onClick={(e) => {
|
|
132
|
-
e.stopPropagation();
|
|
133
|
-
onEditProperty(node.fullPath);
|
|
134
|
-
}}
|
|
135
|
-
className="p-1 hover:bg-gray-200 rounded"
|
|
136
|
-
title="編集"
|
|
137
|
-
/>
|
|
138
|
-
)}
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
{node.children && isExpanded && (
|
|
142
|
-
<div>
|
|
143
|
-
{node.children.map((child) => renderNode(child, depth + 1))}
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const tree = ConfigParser.buildTree(config);
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<div className="h-full flex flex-col border-2 border-gray-100 rounded-xl bg-white shadow-sm">
|
|
154
|
-
{/* ツールバー */}
|
|
155
|
-
<div className="flex items-center gap-2 p-3 border-b-2 border-gray-100 bg-gradient-to-r from-gray-50 to-gray-100">
|
|
156
|
-
<button
|
|
157
|
-
onClick={expandAll}
|
|
158
|
-
className="px-4 py-1.5 text-sm font-medium bg-white border-2 border-blue-200 text-blue-600 rounded-lg hover:bg-blue-50 hover:border-blue-300 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
159
|
-
>
|
|
160
|
-
すべて展開
|
|
161
|
-
</button>
|
|
162
|
-
<button
|
|
163
|
-
onClick={collapseAll}
|
|
164
|
-
className="px-4 py-1.5 text-sm font-medium bg-white border-2 border-gray-200 text-gray-600 rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
165
|
-
>
|
|
166
|
-
すべて閉じる
|
|
167
|
-
</button>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
{/* ツリー表示 */}
|
|
171
|
-
<div className="flex-1 overflow-y-auto p-3">
|
|
172
|
-
{tree.map((node) => renderNode(node))}
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useMemo } from 'react';
|
|
4
|
-
import { PencilIcon, PlusIcon, XIcon, SaveIcon, GripVerticalIcon } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
export interface EditingItem {
|
|
7
|
-
originalName: string;
|
|
8
|
-
newName: string;
|
|
9
|
-
isNew: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface EditableListWrapperProps {
|
|
13
|
-
/** セクションのラベル */
|
|
14
|
-
label: string;
|
|
15
|
-
/** 現在のアイテム一覧 */
|
|
16
|
-
items: string[];
|
|
17
|
-
/** アイテム一覧が変更されたときのコールバック */
|
|
18
|
-
onItemsChange: (items: string[]) => void;
|
|
19
|
-
/** 編集ボタンのtitle属性 */
|
|
20
|
-
editButtonTitle: string;
|
|
21
|
-
/** 編集モードの説明テキスト */
|
|
22
|
-
editModeDescription: string;
|
|
23
|
-
/** 入力欄のプレースホルダー */
|
|
24
|
-
inputPlaceholder: string;
|
|
25
|
-
/** 新規追加入力欄のプレースホルダー */
|
|
26
|
-
newItemPlaceholder: string;
|
|
27
|
-
/** 削除ボタンのtitle属性 */
|
|
28
|
-
deleteButtonTitle: string;
|
|
29
|
-
/** 追加ボタンのtitle属性 */
|
|
30
|
-
addButtonTitle: string;
|
|
31
|
-
/** 重複エラーメッセージ */
|
|
32
|
-
duplicateErrorMessage: string;
|
|
33
|
-
/** 名前変更時に呼ばれるコールバック(旧名 -> 新名のマップ) */
|
|
34
|
-
onRename?: (renamedMap: Record<string, string>) => void;
|
|
35
|
-
/** 通常モードの表示内容 */
|
|
36
|
-
children: React.ReactNode;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 編集可能なリストを管理する共通コンポーネント
|
|
41
|
-
* タグやフィールドの編集UIで使用
|
|
42
|
-
*/
|
|
43
|
-
export function EditableListWrapper({
|
|
44
|
-
label,
|
|
45
|
-
items,
|
|
46
|
-
onItemsChange,
|
|
47
|
-
editButtonTitle,
|
|
48
|
-
editModeDescription,
|
|
49
|
-
inputPlaceholder,
|
|
50
|
-
newItemPlaceholder,
|
|
51
|
-
deleteButtonTitle,
|
|
52
|
-
addButtonTitle,
|
|
53
|
-
duplicateErrorMessage,
|
|
54
|
-
onRename,
|
|
55
|
-
children
|
|
56
|
-
}: EditableListWrapperProps) {
|
|
57
|
-
const [isEditMode, setIsEditMode] = useState(false);
|
|
58
|
-
const [editingItems, setEditingItems] = useState<EditingItem[]>([]);
|
|
59
|
-
const [newItemName, setNewItemName] = useState('');
|
|
60
|
-
|
|
61
|
-
// ドラッグ&ドロップ用の状態
|
|
62
|
-
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
|
63
|
-
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
64
|
-
|
|
65
|
-
// ドラッグ中のプレビュー用に並び替えた配列を計算
|
|
66
|
-
const displayItems = useMemo(() => {
|
|
67
|
-
if (dragIndex === null || dragOverIndex === null || dragIndex === dragOverIndex) {
|
|
68
|
-
return editingItems.map((item, index) => ({ item, originalIndex: index }));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ドラッグ中は見た目上の並び替えを表示
|
|
72
|
-
const result = editingItems.map((item, index) => ({ item, originalIndex: index }));
|
|
73
|
-
const [draggedItem] = result.splice(dragIndex, 1);
|
|
74
|
-
result.splice(dragOverIndex, 0, draggedItem);
|
|
75
|
-
return result;
|
|
76
|
-
}, [editingItems, dragIndex, dragOverIndex]);
|
|
77
|
-
|
|
78
|
-
// 編集モードに入る
|
|
79
|
-
const enterEditMode = () => {
|
|
80
|
-
const itemList = items.map(name => ({
|
|
81
|
-
originalName: name,
|
|
82
|
-
newName: name,
|
|
83
|
-
isNew: false
|
|
84
|
-
}));
|
|
85
|
-
setEditingItems(itemList);
|
|
86
|
-
setIsEditMode(true);
|
|
87
|
-
setNewItemName('');
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// 編集モードをキャンセル
|
|
91
|
-
const cancelEditMode = () => {
|
|
92
|
-
setIsEditMode(false);
|
|
93
|
-
setEditingItems([]);
|
|
94
|
-
setNewItemName('');
|
|
95
|
-
setDragIndex(null);
|
|
96
|
-
setDragOverIndex(null);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// アイテムを追加(編集モード内)
|
|
100
|
-
const handleAddItem = () => {
|
|
101
|
-
const trimmedName = newItemName.trim();
|
|
102
|
-
if (trimmedName && !editingItems.some(item => item.newName === trimmedName)) {
|
|
103
|
-
setEditingItems([...editingItems, {
|
|
104
|
-
originalName: '',
|
|
105
|
-
newName: trimmedName,
|
|
106
|
-
isNew: true
|
|
107
|
-
}]);
|
|
108
|
-
setNewItemName('');
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// アイテムを削除(編集モード内)
|
|
113
|
-
const handleRemoveItem = (originalIndex: number) => {
|
|
114
|
-
setEditingItems(editingItems.filter((_, i) => i !== originalIndex));
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// アイテム名を変更(編集モード内)
|
|
118
|
-
const handleNameChange = (originalIndex: number, newName: string) => {
|
|
119
|
-
setEditingItems(editingItems.map((item, i) =>
|
|
120
|
-
i === originalIndex ? { ...item, newName } : item
|
|
121
|
-
));
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// ドラッグ開始
|
|
125
|
-
const handleDragStart = (originalIndex: number) => {
|
|
126
|
-
setDragIndex(originalIndex);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// ドラッグオーバー(表示上のインデックスで処理)
|
|
130
|
-
const handleDragOver = (e: React.DragEvent, displayIndex: number) => {
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
if (dragIndex === null) return;
|
|
133
|
-
|
|
134
|
-
// 表示上のインデックスをそのまま使用
|
|
135
|
-
if (dragOverIndex !== displayIndex) {
|
|
136
|
-
setDragOverIndex(displayIndex);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// ドロップ時にデータを確定
|
|
141
|
-
const handleDrop = () => {
|
|
142
|
-
if (dragIndex === null || dragOverIndex === null || dragIndex === dragOverIndex) {
|
|
143
|
-
setDragIndex(null);
|
|
144
|
-
setDragOverIndex(null);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 配列の並び替えを確定
|
|
149
|
-
const newItems = [...editingItems];
|
|
150
|
-
const [draggedItem] = newItems.splice(dragIndex, 1);
|
|
151
|
-
newItems.splice(dragOverIndex, 0, draggedItem);
|
|
152
|
-
setEditingItems(newItems);
|
|
153
|
-
|
|
154
|
-
setDragIndex(null);
|
|
155
|
-
setDragOverIndex(null);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// ドラッグ終了(キャンセル時など)
|
|
159
|
-
const handleDragEnd = () => {
|
|
160
|
-
setDragIndex(null);
|
|
161
|
-
setDragOverIndex(null);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// 保存処理
|
|
165
|
-
const handleSave = () => {
|
|
166
|
-
const newItems: string[] = [];
|
|
167
|
-
const renamedMap: Record<string, string> = {};
|
|
168
|
-
|
|
169
|
-
for (const item of editingItems) {
|
|
170
|
-
const trimmedName = item.newName.trim();
|
|
171
|
-
if (!trimmedName) continue;
|
|
172
|
-
|
|
173
|
-
newItems.push(trimmedName);
|
|
174
|
-
|
|
175
|
-
// 名前変更の追跡
|
|
176
|
-
if (!item.isNew && item.originalName !== trimmedName) {
|
|
177
|
-
renamedMap[item.originalName] = trimmedName;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 入力中のアイテムがあれば追加
|
|
182
|
-
const pendingName = newItemName.trim();
|
|
183
|
-
if (pendingName && !newItems.includes(pendingName)) {
|
|
184
|
-
newItems.push(pendingName);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 名前変更コールバック
|
|
188
|
-
if (onRename && Object.keys(renamedMap).length > 0) {
|
|
189
|
-
onRename(renamedMap);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
onItemsChange(newItems);
|
|
193
|
-
setIsEditMode(false);
|
|
194
|
-
setEditingItems([]);
|
|
195
|
-
setNewItemName('');
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
199
|
-
if (e.key === 'Enter') {
|
|
200
|
-
e.preventDefault();
|
|
201
|
-
handleAddItem();
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// バリデーション: 重複チェック
|
|
206
|
-
const hasDuplicateNames = () => {
|
|
207
|
-
const names = editingItems.map(item => item.newName.trim()).filter(Boolean);
|
|
208
|
-
return new Set(names).size !== names.length;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// バリデーション: 空の名前チェック
|
|
212
|
-
const hasEmptyNames = () => {
|
|
213
|
-
return editingItems.some(item => !item.newName.trim());
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const canSave = !hasDuplicateNames() && !hasEmptyNames();
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<div>
|
|
220
|
-
<div className="flex items-center justify-between mb-2">
|
|
221
|
-
<label className="block text-sm font-semibold text-gray-700">
|
|
222
|
-
{label}
|
|
223
|
-
</label>
|
|
224
|
-
{!isEditMode && (
|
|
225
|
-
<button
|
|
226
|
-
onClick={enterEditMode}
|
|
227
|
-
className="text-gray-500 hover:text-blue-600 transition-colors p-1"
|
|
228
|
-
title={editButtonTitle}
|
|
229
|
-
>
|
|
230
|
-
<PencilIcon className="w-4 h-4" />
|
|
231
|
-
</button>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
|
-
{isEditMode ? (
|
|
236
|
-
<div className="space-y-3 p-4 bg-blue-50 rounded-lg border-2 border-blue-200">
|
|
237
|
-
<div className="text-xs font-medium text-blue-700 mb-2">
|
|
238
|
-
{editModeDescription}(ドラッグで並び替え可能)
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
{/* 既存アイテムの編集 */}
|
|
242
|
-
<div className="space-y-2">
|
|
243
|
-
{displayItems.map(({ item, originalIndex }, displayIndex) => {
|
|
244
|
-
const isDragging = dragIndex === originalIndex;
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<div
|
|
248
|
-
key={originalIndex}
|
|
249
|
-
draggable
|
|
250
|
-
onDragStart={() => handleDragStart(originalIndex)}
|
|
251
|
-
onDragOver={(e) => handleDragOver(e, displayIndex)}
|
|
252
|
-
onDrop={handleDrop}
|
|
253
|
-
onDragEnd={handleDragEnd}
|
|
254
|
-
className={`flex items-center gap-2 transition-all ${
|
|
255
|
-
isDragging ? 'opacity-50' : ''
|
|
256
|
-
}`}
|
|
257
|
-
>
|
|
258
|
-
<div
|
|
259
|
-
className="cursor-grab text-gray-400 hover:text-gray-600 p-1"
|
|
260
|
-
title="ドラッグして並び替え"
|
|
261
|
-
>
|
|
262
|
-
<GripVerticalIcon className="w-4 h-4" />
|
|
263
|
-
</div>
|
|
264
|
-
<input
|
|
265
|
-
type="text"
|
|
266
|
-
value={item.newName}
|
|
267
|
-
onChange={(e) => handleNameChange(originalIndex, e.target.value)}
|
|
268
|
-
placeholder={inputPlaceholder}
|
|
269
|
-
className={`flex-1 px-3 py-2 border-2 rounded-lg text-sm focus:ring-2 transition-all duration-200 ${
|
|
270
|
-
!item.newName.trim()
|
|
271
|
-
? 'border-red-300 focus:border-red-400 focus:ring-red-200'
|
|
272
|
-
: 'border-gray-200 focus:border-blue-400 focus:ring-blue-200'
|
|
273
|
-
}`}
|
|
274
|
-
/>
|
|
275
|
-
<button
|
|
276
|
-
onClick={() => handleRemoveItem(originalIndex)}
|
|
277
|
-
className="text-gray-400 hover:text-red-600 transition-colors p-2"
|
|
278
|
-
title={deleteButtonTitle}
|
|
279
|
-
>
|
|
280
|
-
<XIcon className="w-4 h-4" />
|
|
281
|
-
</button>
|
|
282
|
-
</div>
|
|
283
|
-
);
|
|
284
|
-
})}
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
{/* 新規アイテム追加 */}
|
|
288
|
-
<div className="flex items-center gap-2 pt-2 border-t border-blue-200">
|
|
289
|
-
<input
|
|
290
|
-
type="text"
|
|
291
|
-
value={newItemName}
|
|
292
|
-
onChange={(e) => setNewItemName(e.target.value)}
|
|
293
|
-
onKeyDown={handleKeyDown}
|
|
294
|
-
placeholder={newItemPlaceholder}
|
|
295
|
-
className="flex-1 px-3 py-2 border-2 border-gray-200 rounded-lg text-sm focus:border-blue-400 focus:ring-2 focus:ring-blue-200 transition-all duration-200"
|
|
296
|
-
/>
|
|
297
|
-
<button
|
|
298
|
-
onClick={handleAddItem}
|
|
299
|
-
disabled={!newItemName.trim() || editingItems.some(item => item.newName.trim() === newItemName.trim())}
|
|
300
|
-
className="px-3 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
|
|
301
|
-
title={addButtonTitle}
|
|
302
|
-
>
|
|
303
|
-
<PlusIcon className="w-4 h-4" />
|
|
304
|
-
</button>
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
{/* エラーメッセージ */}
|
|
308
|
-
{hasDuplicateNames() && (
|
|
309
|
-
<div className="text-xs text-red-600 mt-2">
|
|
310
|
-
{duplicateErrorMessage}
|
|
311
|
-
</div>
|
|
312
|
-
)}
|
|
313
|
-
|
|
314
|
-
{/* 保存・キャンセルボタン */}
|
|
315
|
-
<div className="flex items-center gap-2 pt-3 border-t border-blue-200">
|
|
316
|
-
<button
|
|
317
|
-
onClick={handleSave}
|
|
318
|
-
disabled={!canSave}
|
|
319
|
-
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed text-sm font-medium"
|
|
320
|
-
>
|
|
321
|
-
<SaveIcon className="w-4 h-4" />
|
|
322
|
-
保存
|
|
323
|
-
</button>
|
|
324
|
-
<button
|
|
325
|
-
onClick={cancelEditMode}
|
|
326
|
-
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors text-sm font-medium"
|
|
327
|
-
>
|
|
328
|
-
キャンセル
|
|
329
|
-
</button>
|
|
330
|
-
</div>
|
|
331
|
-
</div>
|
|
332
|
-
) : (
|
|
333
|
-
children
|
|
334
|
-
)}
|
|
335
|
-
</div>
|
|
336
|
-
);
|
|
337
|
-
}
|