@skspwork/config-doc 2.0.3 → 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 +3 -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 +368 -29
- 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/package.json +2 -0
- 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/package.json +2 -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/4bbca8cd642026de.css +0 -3
- package/packages/web/.next/standalone/.next/static/chunks/54e2bd8f072e7d4e.js +0 -1
- 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 -633
- 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 -839
- package/packages/web/.next/standalone/lib/jsonUtils.ts +0 -26
- package/packages/web/.next/standalone/lib/markdownGenerator.ts +0 -79
- package/packages/web/.next/standalone/lib/markdownTableGenerator.ts +0 -107
- package/packages/web/.next/standalone/lib/storage.ts +0 -104
- package/packages/web/.next/standalone/lib/utils.ts +0 -72
- package/packages/web/.next/standalone/next.config.ts +0 -10
- package/packages/web/.next/standalone/package-lock.json +0 -7977
- 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/4bbca8cd642026de.css +0 -3
- package/packages/web/.next/static/chunks/54e2bd8f072e7d4e.js +0 -1
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import { ConfigDocs, PropertyDoc, ExportSettings, DEFAULT_FIELDS } from '@/types';
|
|
5
|
-
import { LoadedConfig } from '@/components/ConfigFileTabs';
|
|
6
|
-
import { ToastType } from '@/components/Toast';
|
|
7
|
-
|
|
8
|
-
interface Toast {
|
|
9
|
-
id: string;
|
|
10
|
-
message: string;
|
|
11
|
-
type: ToastType;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface UseConfigManagerReturn {
|
|
15
|
-
// 状態
|
|
16
|
-
loadedConfigs: LoadedConfig[];
|
|
17
|
-
activeConfigIndex: number;
|
|
18
|
-
activeConfig: LoadedConfig | undefined;
|
|
19
|
-
selectedPath: string;
|
|
20
|
-
editingDoc: PropertyDoc | null;
|
|
21
|
-
originalDoc: PropertyDoc | null;
|
|
22
|
-
hasUnsavedChanges: boolean;
|
|
23
|
-
exportSettings: ExportSettings | undefined;
|
|
24
|
-
availableTags: string[];
|
|
25
|
-
projectFields: Record<string, string>;
|
|
26
|
-
toasts: Toast[];
|
|
27
|
-
rootPath: string;
|
|
28
|
-
isInitialized: boolean;
|
|
29
|
-
|
|
30
|
-
// アクション
|
|
31
|
-
setActiveConfigIndex: (index: number) => void;
|
|
32
|
-
setSelectedPath: (path: string) => void;
|
|
33
|
-
setEditingDoc: (doc: PropertyDoc | null) => void;
|
|
34
|
-
setHasUnsavedChanges: (value: boolean) => void;
|
|
35
|
-
showToast: (message: string, type?: Toast['type']) => void;
|
|
36
|
-
removeToast: (id: string) => void;
|
|
37
|
-
loadConfigFile: (filePath: string) => Promise<void>;
|
|
38
|
-
handleSelectConfigFiles: (filePaths: string[]) => Promise<void>;
|
|
39
|
-
handleRemoveConfig: (index: number) => Promise<void>;
|
|
40
|
-
handleReorderConfigs: (newConfigs: LoadedConfig[], newActiveIndex: number) => void;
|
|
41
|
-
handleSelectProperty: (path: string) => void;
|
|
42
|
-
handleSaveProperty: () => Promise<void>;
|
|
43
|
-
handleExport: (settings: ExportSettings) => Promise<void>;
|
|
44
|
-
handleAvailableTagsChange: (tags: string[]) => Promise<void>;
|
|
45
|
-
handleProjectFieldsChange: (fields: Record<string, string>) => Promise<void>;
|
|
46
|
-
checkForChanges: (current: PropertyDoc | null, original: PropertyDoc | null) => boolean;
|
|
47
|
-
resetSelection: () => void;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function useConfigManager(): UseConfigManagerReturn {
|
|
51
|
-
const [loadedConfigs, setLoadedConfigs] = useState<LoadedConfig[]>([]);
|
|
52
|
-
const [activeConfigIndex, setActiveConfigIndex] = useState<number>(0);
|
|
53
|
-
const [selectedPath, setSelectedPath] = useState<string>('');
|
|
54
|
-
const [isInitialized, setIsInitialized] = useState(false);
|
|
55
|
-
const [rootPath, setRootPath] = useState<string>('.');
|
|
56
|
-
|
|
57
|
-
// 編集中のプロパティドキュメント
|
|
58
|
-
const [editingDoc, setEditingDoc] = useState<PropertyDoc | null>(null);
|
|
59
|
-
const [originalDoc, setOriginalDoc] = useState<PropertyDoc | null>(null);
|
|
60
|
-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
61
|
-
|
|
62
|
-
// エクスポート設定
|
|
63
|
-
const [exportSettings, setExportSettings] = useState<ExportSettings | undefined>();
|
|
64
|
-
|
|
65
|
-
// 利用可能なタグ
|
|
66
|
-
const [availableTags, setAvailableTags] = useState<string[]>(['required', 'nullable', 'string', 'number', 'boolean']);
|
|
67
|
-
|
|
68
|
-
// プロジェクトのフィールド定義
|
|
69
|
-
const [projectFields, setProjectFields] = useState<Record<string, string>>(DEFAULT_FIELDS);
|
|
70
|
-
|
|
71
|
-
// トースト通知
|
|
72
|
-
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
73
|
-
|
|
74
|
-
const activeConfig = loadedConfigs[activeConfigIndex];
|
|
75
|
-
|
|
76
|
-
const showToast = useCallback((message: string, type: Toast['type'] = 'success') => {
|
|
77
|
-
const id = Date.now().toString();
|
|
78
|
-
setToasts(prev => [...prev, { id, message, type }]);
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
const removeToast = useCallback((id: string) => {
|
|
82
|
-
setToasts(prev => prev.filter(t => t.id !== id));
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
// 差分チェック関数
|
|
86
|
-
const checkForChanges = useCallback((current: PropertyDoc | null, original: PropertyDoc | null): boolean => {
|
|
87
|
-
if (!current || !original) return false;
|
|
88
|
-
|
|
89
|
-
// タグの比較
|
|
90
|
-
const currentTags = current.tags || [];
|
|
91
|
-
const originalTags = original.tags || [];
|
|
92
|
-
if (currentTags.length !== originalTags.length) return true;
|
|
93
|
-
if (currentTags.some((tag, index) => tag !== originalTags[index])) return true;
|
|
94
|
-
|
|
95
|
-
// フィールドの比較
|
|
96
|
-
const currentFields = current.fields || {};
|
|
97
|
-
const originalFields = original.fields || {};
|
|
98
|
-
const allKeys = new Set([...Object.keys(currentFields), ...Object.keys(originalFields)]);
|
|
99
|
-
for (const key of allKeys) {
|
|
100
|
-
const currentValue = currentFields[key] || '';
|
|
101
|
-
const originalValue = originalFields[key] || '';
|
|
102
|
-
if (currentValue !== originalValue) return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return false;
|
|
106
|
-
}, []);
|
|
107
|
-
|
|
108
|
-
// 絶対パスを相対パスに変換
|
|
109
|
-
const toRelativePath = useCallback((absolutePath: string, basePath: string): string => {
|
|
110
|
-
const normalizeSlash = (p: string) => p.replace(/\\/g, '/');
|
|
111
|
-
const normAbsolute = normalizeSlash(absolutePath);
|
|
112
|
-
const normBase = normalizeSlash(basePath);
|
|
113
|
-
|
|
114
|
-
if (!normAbsolute.match(/^[a-zA-Z]:\//) && !normAbsolute.startsWith('/')) {
|
|
115
|
-
return absolutePath;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (normAbsolute.startsWith(normBase)) {
|
|
119
|
-
const relative = normAbsolute.substring(normBase.length);
|
|
120
|
-
return relative.startsWith('/') ? relative.substring(1) : relative;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return absolutePath;
|
|
124
|
-
}, []);
|
|
125
|
-
|
|
126
|
-
// projectFieldsでフィルタリングしてPropertyDocを整形
|
|
127
|
-
const normalizePropertyDoc = useCallback((doc: PropertyDoc): PropertyDoc => {
|
|
128
|
-
const filteredFields: Record<string, string> = {};
|
|
129
|
-
for (const key of Object.keys(projectFields)) {
|
|
130
|
-
filteredFields[key] = doc.fields[key] || '';
|
|
131
|
-
}
|
|
132
|
-
return { ...doc, fields: filteredFields };
|
|
133
|
-
}, [projectFields]);
|
|
134
|
-
|
|
135
|
-
// 設定ファイルを読み込む
|
|
136
|
-
const loadConfigFile = useCallback(async (filePath: string) => {
|
|
137
|
-
try {
|
|
138
|
-
const response = await fetch('/api/config/load', {
|
|
139
|
-
method: 'POST',
|
|
140
|
-
headers: { 'Content-Type': 'application/json' },
|
|
141
|
-
body: JSON.stringify({ filePath })
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const result = await response.json();
|
|
145
|
-
if (result.success) {
|
|
146
|
-
setLoadedConfigs(prev => {
|
|
147
|
-
if (prev.some(c => c.filePath === filePath)) {
|
|
148
|
-
return prev;
|
|
149
|
-
}
|
|
150
|
-
return [...prev, {
|
|
151
|
-
filePath,
|
|
152
|
-
configData: result.data.configData,
|
|
153
|
-
docs: result.data.docs
|
|
154
|
-
}];
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.error('Failed to load config file:', error);
|
|
159
|
-
}
|
|
160
|
-
}, []);
|
|
161
|
-
|
|
162
|
-
// メタデータを更新
|
|
163
|
-
const updateMetadata = useCallback(async (configFilePaths: string[]) => {
|
|
164
|
-
try {
|
|
165
|
-
const response = await fetch('/api/config/metadata', {
|
|
166
|
-
method: 'POST',
|
|
167
|
-
headers: { 'Content-Type': 'application/json' },
|
|
168
|
-
body: JSON.stringify({ configFilePaths })
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const result = await response.json();
|
|
172
|
-
|
|
173
|
-
if (result.deletedDocsFiles && result.deletedDocsFiles.length > 0) {
|
|
174
|
-
showToast(
|
|
175
|
-
`${result.deletedDocsFiles.length}件のドキュメントファイルを削除しました`,
|
|
176
|
-
'warning'
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
console.error('Failed to update metadata:', error);
|
|
181
|
-
}
|
|
182
|
-
}, [showToast]);
|
|
183
|
-
|
|
184
|
-
// 自動エクスポート
|
|
185
|
-
const handleAutoExport = useCallback(async () => {
|
|
186
|
-
if (!exportSettings) return;
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
const response = await fetch('/api/export', {
|
|
190
|
-
method: 'POST',
|
|
191
|
-
headers: { 'Content-Type': 'application/json' },
|
|
192
|
-
body: JSON.stringify({
|
|
193
|
-
format: exportSettings.format,
|
|
194
|
-
fileName: exportSettings.fileName,
|
|
195
|
-
outputDir: exportSettings.outputDir
|
|
196
|
-
})
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
if (response.ok) {
|
|
200
|
-
const updatedSettings = {
|
|
201
|
-
...exportSettings,
|
|
202
|
-
lastExportedAt: new Date().toISOString()
|
|
203
|
-
};
|
|
204
|
-
setExportSettings(updatedSettings);
|
|
205
|
-
|
|
206
|
-
await fetch('/api/export/settings', {
|
|
207
|
-
method: 'POST',
|
|
208
|
-
headers: { 'Content-Type': 'application/json' },
|
|
209
|
-
body: JSON.stringify({ settings: updatedSettings })
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
console.error('Auto export failed:', error);
|
|
214
|
-
}
|
|
215
|
-
}, [exportSettings]);
|
|
216
|
-
|
|
217
|
-
// 初期化
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
const loadSavedConfigs = async () => {
|
|
220
|
-
try {
|
|
221
|
-
const response = await fetch('/api/project');
|
|
222
|
-
const result = await response.json();
|
|
223
|
-
|
|
224
|
-
if (result.success) {
|
|
225
|
-
setRootPath(result.data.rootPath);
|
|
226
|
-
|
|
227
|
-
if (result.data.hasConfigDoc) {
|
|
228
|
-
const metaResponse = await fetch('/api/config/metadata');
|
|
229
|
-
const metaResult = await metaResponse.json();
|
|
230
|
-
|
|
231
|
-
if (metaResult.success && metaResult.data?.configFiles) {
|
|
232
|
-
for (const configFile of metaResult.data.configFiles) {
|
|
233
|
-
await loadConfigFile(configFile.filePath);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (metaResult.success && metaResult.data?.availableTags) {
|
|
238
|
-
setAvailableTags(metaResult.data.availableTags);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (metaResult.success && metaResult.data?.fields) {
|
|
242
|
-
setProjectFields(metaResult.data.fields);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const exportResponse = await fetch('/api/export/settings');
|
|
248
|
-
const exportResult = await exportResponse.json();
|
|
249
|
-
if (exportResult.success) {
|
|
250
|
-
setExportSettings(exportResult.data);
|
|
251
|
-
}
|
|
252
|
-
} catch (error) {
|
|
253
|
-
console.error('Failed to load saved configs:', error);
|
|
254
|
-
} finally {
|
|
255
|
-
setIsInitialized(true);
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
loadSavedConfigs();
|
|
260
|
-
}, [loadConfigFile]);
|
|
261
|
-
|
|
262
|
-
// 設定ファイルを選択
|
|
263
|
-
const handleSelectConfigFiles = useCallback(async (filePaths: string[]) => {
|
|
264
|
-
const newConfigs: LoadedConfig[] = [];
|
|
265
|
-
|
|
266
|
-
for (const filePath of filePaths) {
|
|
267
|
-
const relativePath = toRelativePath(filePath, rootPath);
|
|
268
|
-
|
|
269
|
-
if (loadedConfigs.some(c => c.filePath === relativePath)) {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const response = await fetch('/api/config/load', {
|
|
275
|
-
method: 'POST',
|
|
276
|
-
headers: { 'Content-Type': 'application/json' },
|
|
277
|
-
body: JSON.stringify({ filePath })
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const result = await response.json();
|
|
281
|
-
if (result.success) {
|
|
282
|
-
newConfigs.push({
|
|
283
|
-
filePath: relativePath,
|
|
284
|
-
configData: result.data.configData,
|
|
285
|
-
docs: result.data.docs
|
|
286
|
-
});
|
|
287
|
-
} else {
|
|
288
|
-
showToast(`設定ファイルの読み込みに失敗しました (${filePath}): ${result.error}`, 'error');
|
|
289
|
-
}
|
|
290
|
-
} catch (error) {
|
|
291
|
-
console.error('Failed to load config:', error);
|
|
292
|
-
showToast(`設定ファイルの読み込み中にエラーが発生しました: ${filePath}`, 'error');
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (newConfigs.length > 0) {
|
|
297
|
-
setLoadedConfigs(prev => {
|
|
298
|
-
const updated = [...prev, ...newConfigs];
|
|
299
|
-
updateMetadata(updated.map(c => c.filePath));
|
|
300
|
-
return updated;
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
}, [loadedConfigs, rootPath, showToast, toRelativePath, updateMetadata]);
|
|
304
|
-
|
|
305
|
-
// 設定ファイルを削除
|
|
306
|
-
const handleRemoveConfig = useCallback(async (index: number) => {
|
|
307
|
-
const configToRemove = loadedConfigs[index];
|
|
308
|
-
|
|
309
|
-
if (configToRemove.docs && Object.keys(configToRemove.docs.properties).length > 0) {
|
|
310
|
-
if (!confirm('既に説明文が設定されていますが、削除してよろしいですか?')) {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
setLoadedConfigs(prev => {
|
|
316
|
-
const updated = prev.filter((_, i) => i !== index);
|
|
317
|
-
updateMetadata(updated.map(c => c.filePath));
|
|
318
|
-
return updated;
|
|
319
|
-
});
|
|
320
|
-
if (activeConfigIndex >= index && activeConfigIndex > 0) {
|
|
321
|
-
setActiveConfigIndex(activeConfigIndex - 1);
|
|
322
|
-
}
|
|
323
|
-
setSelectedPath('');
|
|
324
|
-
setEditingDoc(null);
|
|
325
|
-
setHasUnsavedChanges(false);
|
|
326
|
-
}, [activeConfigIndex, loadedConfigs, updateMetadata]);
|
|
327
|
-
|
|
328
|
-
// タブの並び替え
|
|
329
|
-
const handleReorderConfigs = useCallback((newConfigs: LoadedConfig[], newActiveIndex: number) => {
|
|
330
|
-
setLoadedConfigs(newConfigs);
|
|
331
|
-
setActiveConfigIndex(newActiveIndex);
|
|
332
|
-
updateMetadata(newConfigs.map(c => c.filePath));
|
|
333
|
-
}, [updateMetadata]);
|
|
334
|
-
|
|
335
|
-
// プロパティを選択
|
|
336
|
-
const handleSelectProperty = useCallback((path: string) => {
|
|
337
|
-
if (hasUnsavedChanges) {
|
|
338
|
-
if (!confirm('保存されていない変更があります。破棄しますか?')) {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
setSelectedPath(path);
|
|
344
|
-
const existingDoc = activeConfig?.docs.properties[path];
|
|
345
|
-
|
|
346
|
-
if (existingDoc) {
|
|
347
|
-
const docCopy = normalizePropertyDoc({
|
|
348
|
-
...existingDoc,
|
|
349
|
-
tags: existingDoc.tags || []
|
|
350
|
-
});
|
|
351
|
-
setEditingDoc(docCopy);
|
|
352
|
-
setOriginalDoc(docCopy);
|
|
353
|
-
} else {
|
|
354
|
-
const newDoc: PropertyDoc = {
|
|
355
|
-
path,
|
|
356
|
-
tags: [],
|
|
357
|
-
fields: { ...projectFields },
|
|
358
|
-
modifiedAt: new Date().toISOString()
|
|
359
|
-
};
|
|
360
|
-
setEditingDoc(newDoc);
|
|
361
|
-
setOriginalDoc(newDoc);
|
|
362
|
-
}
|
|
363
|
-
setHasUnsavedChanges(false);
|
|
364
|
-
}, [activeConfig, hasUnsavedChanges, normalizePropertyDoc, projectFields]);
|
|
365
|
-
|
|
366
|
-
// プロパティを保存
|
|
367
|
-
const handleSaveProperty = useCallback(async () => {
|
|
368
|
-
if (!editingDoc || !activeConfig) return;
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
const propertyDoc = {
|
|
372
|
-
...editingDoc,
|
|
373
|
-
modifiedAt: new Date().toISOString()
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
const response = await fetch('/api/config/save', {
|
|
377
|
-
method: 'POST',
|
|
378
|
-
headers: { 'Content-Type': 'application/json' },
|
|
379
|
-
body: JSON.stringify({
|
|
380
|
-
configFilePath: activeConfig.filePath,
|
|
381
|
-
propertyPath: selectedPath,
|
|
382
|
-
propertyDoc
|
|
383
|
-
})
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const result = await response.json();
|
|
387
|
-
if (result.success) {
|
|
388
|
-
setLoadedConfigs(prev => prev.map((config, idx) =>
|
|
389
|
-
idx === activeConfigIndex
|
|
390
|
-
? {
|
|
391
|
-
...config,
|
|
392
|
-
docs: {
|
|
393
|
-
...config.docs,
|
|
394
|
-
properties: {
|
|
395
|
-
...config.docs.properties,
|
|
396
|
-
[selectedPath]: propertyDoc
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
: config
|
|
401
|
-
));
|
|
402
|
-
setEditingDoc(propertyDoc);
|
|
403
|
-
setOriginalDoc(propertyDoc);
|
|
404
|
-
setHasUnsavedChanges(false);
|
|
405
|
-
showToast('保存しました');
|
|
406
|
-
|
|
407
|
-
if (exportSettings?.autoExport) {
|
|
408
|
-
await handleAutoExport();
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
showToast('保存に失敗しました: ' + result.error, 'error');
|
|
412
|
-
}
|
|
413
|
-
} catch (error) {
|
|
414
|
-
console.error('Failed to save:', error);
|
|
415
|
-
showToast('保存中にエラーが発生しました', 'error');
|
|
416
|
-
}
|
|
417
|
-
}, [activeConfig, activeConfigIndex, editingDoc, exportSettings?.autoExport, handleAutoExport, selectedPath, showToast]);
|
|
418
|
-
|
|
419
|
-
// エクスポート
|
|
420
|
-
const handleExport = useCallback(async (settings: ExportSettings) => {
|
|
421
|
-
try {
|
|
422
|
-
const response = await fetch('/api/export', {
|
|
423
|
-
method: 'POST',
|
|
424
|
-
headers: { 'Content-Type': 'application/json' },
|
|
425
|
-
body: JSON.stringify({
|
|
426
|
-
format: settings.format,
|
|
427
|
-
fileName: settings.fileName,
|
|
428
|
-
outputDir: settings.outputDir
|
|
429
|
-
})
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
const result = await response.json();
|
|
433
|
-
if (result.success) {
|
|
434
|
-
const updatedSettings = {
|
|
435
|
-
...settings,
|
|
436
|
-
lastExportedAt: new Date().toISOString()
|
|
437
|
-
};
|
|
438
|
-
setExportSettings(updatedSettings);
|
|
439
|
-
|
|
440
|
-
await fetch('/api/export/settings', {
|
|
441
|
-
method: 'POST',
|
|
442
|
-
headers: { 'Content-Type': 'application/json' },
|
|
443
|
-
body: JSON.stringify({ settings: updatedSettings })
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
showToast(`エクスポートしました: ${result.outputPath}`);
|
|
447
|
-
} else {
|
|
448
|
-
showToast('エクスポートに失敗しました: ' + result.error, 'error');
|
|
449
|
-
}
|
|
450
|
-
} catch (error) {
|
|
451
|
-
console.error('Export failed:', error);
|
|
452
|
-
showToast('エクスポート中にエラーが発生しました', 'error');
|
|
453
|
-
}
|
|
454
|
-
}, [showToast]);
|
|
455
|
-
|
|
456
|
-
// 利用可能なタグを更新
|
|
457
|
-
const handleAvailableTagsChange = useCallback(async (tags: string[]) => {
|
|
458
|
-
const removedTags = availableTags.filter(tag => !tags.includes(tag));
|
|
459
|
-
|
|
460
|
-
setAvailableTags(tags);
|
|
461
|
-
await fetch('/api/config/metadata', {
|
|
462
|
-
method: 'POST',
|
|
463
|
-
headers: { 'Content-Type': 'application/json' },
|
|
464
|
-
body: JSON.stringify({ availableTags: tags })
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
if (removedTags.length > 0) {
|
|
468
|
-
for (const config of loadedConfigs) {
|
|
469
|
-
let hasChanges = false;
|
|
470
|
-
const updatedProperties = { ...config.docs.properties };
|
|
471
|
-
|
|
472
|
-
for (const [propPath, doc] of Object.entries(updatedProperties)) {
|
|
473
|
-
if (doc.tags && doc.tags.length > 0) {
|
|
474
|
-
const updatedTags = doc.tags.filter(tag => !removedTags.includes(tag));
|
|
475
|
-
if (updatedTags.length !== doc.tags.length) {
|
|
476
|
-
updatedProperties[propPath] = { ...doc, tags: updatedTags };
|
|
477
|
-
hasChanges = true;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (hasChanges) {
|
|
483
|
-
await fetch('/api/config/save', {
|
|
484
|
-
method: 'POST',
|
|
485
|
-
headers: { 'Content-Type': 'application/json' },
|
|
486
|
-
body: JSON.stringify({
|
|
487
|
-
configFilePath: config.filePath,
|
|
488
|
-
properties: updatedProperties
|
|
489
|
-
})
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
setLoadedConfigs(prev => prev.map(c =>
|
|
493
|
-
c.filePath === config.filePath
|
|
494
|
-
? { ...c, docs: { ...c.docs, properties: updatedProperties } }
|
|
495
|
-
: c
|
|
496
|
-
));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (editingDoc && editingDoc.tags) {
|
|
501
|
-
const updatedEditingTags = editingDoc.tags.filter(tag => !removedTags.includes(tag));
|
|
502
|
-
if (updatedEditingTags.length !== editingDoc.tags.length) {
|
|
503
|
-
const updated = { ...editingDoc, tags: updatedEditingTags };
|
|
504
|
-
setEditingDoc(updated);
|
|
505
|
-
if (originalDoc) {
|
|
506
|
-
const updatedOriginalTags = (originalDoc.tags || []).filter(tag => !removedTags.includes(tag));
|
|
507
|
-
setOriginalDoc({ ...originalDoc, tags: updatedOriginalTags });
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}, [availableTags, editingDoc, loadedConfigs, originalDoc]);
|
|
513
|
-
|
|
514
|
-
// プロジェクトフィールドを更新
|
|
515
|
-
const handleProjectFieldsChange = useCallback(async (fields: Record<string, string>) => {
|
|
516
|
-
const oldFieldKeys = Object.keys(projectFields);
|
|
517
|
-
const newFieldKeys = Object.keys(fields);
|
|
518
|
-
const removedFields = oldFieldKeys.filter(key => !newFieldKeys.includes(key));
|
|
519
|
-
|
|
520
|
-
setProjectFields(fields);
|
|
521
|
-
await fetch('/api/config/metadata', {
|
|
522
|
-
method: 'POST',
|
|
523
|
-
headers: { 'Content-Type': 'application/json' },
|
|
524
|
-
body: JSON.stringify({ fields })
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
if (removedFields.length > 0) {
|
|
528
|
-
for (const config of loadedConfigs) {
|
|
529
|
-
let hasChanges = false;
|
|
530
|
-
const updatedProperties = { ...config.docs.properties };
|
|
531
|
-
|
|
532
|
-
for (const [propPath, doc] of Object.entries(updatedProperties)) {
|
|
533
|
-
if (doc.fields) {
|
|
534
|
-
const updatedFields = { ...doc.fields };
|
|
535
|
-
let fieldRemoved = false;
|
|
536
|
-
|
|
537
|
-
for (const removedField of removedFields) {
|
|
538
|
-
if (removedField in updatedFields) {
|
|
539
|
-
delete updatedFields[removedField];
|
|
540
|
-
fieldRemoved = true;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (fieldRemoved) {
|
|
545
|
-
updatedProperties[propPath] = { ...doc, fields: updatedFields };
|
|
546
|
-
hasChanges = true;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (hasChanges) {
|
|
552
|
-
await fetch('/api/config/save', {
|
|
553
|
-
method: 'POST',
|
|
554
|
-
headers: { 'Content-Type': 'application/json' },
|
|
555
|
-
body: JSON.stringify({
|
|
556
|
-
configFilePath: config.filePath,
|
|
557
|
-
properties: updatedProperties
|
|
558
|
-
})
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
setLoadedConfigs(prev => prev.map(c =>
|
|
562
|
-
c.filePath === config.filePath
|
|
563
|
-
? { ...c, docs: { ...c.docs, properties: updatedProperties } }
|
|
564
|
-
: c
|
|
565
|
-
));
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (originalDoc) {
|
|
571
|
-
const updatedOriginalFields: Record<string, string> = {};
|
|
572
|
-
for (const key of newFieldKeys) {
|
|
573
|
-
updatedOriginalFields[key] = originalDoc.fields?.[key] || '';
|
|
574
|
-
}
|
|
575
|
-
setOriginalDoc({ ...originalDoc, fields: updatedOriginalFields });
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (editingDoc) {
|
|
579
|
-
const updatedEditingFields: Record<string, string> = {};
|
|
580
|
-
for (const key of newFieldKeys) {
|
|
581
|
-
updatedEditingFields[key] = editingDoc.fields?.[key] || '';
|
|
582
|
-
}
|
|
583
|
-
const updated = { ...editingDoc, fields: updatedEditingFields };
|
|
584
|
-
setEditingDoc(updated);
|
|
585
|
-
|
|
586
|
-
const updatedOriginal = originalDoc
|
|
587
|
-
? { ...originalDoc, fields: Object.fromEntries(newFieldKeys.map(key => [key, originalDoc.fields?.[key] || ''])) }
|
|
588
|
-
: null;
|
|
589
|
-
setHasUnsavedChanges(checkForChanges(updated, updatedOriginal));
|
|
590
|
-
}
|
|
591
|
-
}, [checkForChanges, editingDoc, loadedConfigs, originalDoc, projectFields]);
|
|
592
|
-
|
|
593
|
-
// 選択状態をリセット
|
|
594
|
-
const resetSelection = useCallback(() => {
|
|
595
|
-
setSelectedPath('');
|
|
596
|
-
setEditingDoc(null);
|
|
597
|
-
setHasUnsavedChanges(false);
|
|
598
|
-
}, []);
|
|
599
|
-
|
|
600
|
-
return {
|
|
601
|
-
loadedConfigs,
|
|
602
|
-
activeConfigIndex,
|
|
603
|
-
activeConfig,
|
|
604
|
-
selectedPath,
|
|
605
|
-
editingDoc,
|
|
606
|
-
originalDoc,
|
|
607
|
-
hasUnsavedChanges,
|
|
608
|
-
exportSettings,
|
|
609
|
-
availableTags,
|
|
610
|
-
projectFields,
|
|
611
|
-
toasts,
|
|
612
|
-
rootPath,
|
|
613
|
-
isInitialized,
|
|
614
|
-
|
|
615
|
-
setActiveConfigIndex,
|
|
616
|
-
setSelectedPath,
|
|
617
|
-
setEditingDoc,
|
|
618
|
-
setHasUnsavedChanges,
|
|
619
|
-
showToast,
|
|
620
|
-
removeToast,
|
|
621
|
-
loadConfigFile,
|
|
622
|
-
handleSelectConfigFiles,
|
|
623
|
-
handleRemoveConfig,
|
|
624
|
-
handleReorderConfigs,
|
|
625
|
-
handleSelectProperty,
|
|
626
|
-
handleSaveProperty,
|
|
627
|
-
handleExport,
|
|
628
|
-
handleAvailableTagsChange,
|
|
629
|
-
handleProjectFieldsChange,
|
|
630
|
-
checkForChanges,
|
|
631
|
-
resetSelection
|
|
632
|
-
};
|
|
633
|
-
}
|