@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,653 +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
|
-
import {
|
|
8
|
-
sortTagsByOrder,
|
|
9
|
-
reorderFields,
|
|
10
|
-
detectTagChanges,
|
|
11
|
-
detectFieldChanges
|
|
12
|
-
} from '@/lib/configManagerUtils';
|
|
13
|
-
|
|
14
|
-
interface Toast {
|
|
15
|
-
id: string;
|
|
16
|
-
message: string;
|
|
17
|
-
type: ToastType;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface UseConfigManagerReturn {
|
|
21
|
-
// 状態
|
|
22
|
-
loadedConfigs: LoadedConfig[];
|
|
23
|
-
activeConfigIndex: number;
|
|
24
|
-
activeConfig: LoadedConfig | undefined;
|
|
25
|
-
selectedPath: string;
|
|
26
|
-
editingDoc: PropertyDoc | null;
|
|
27
|
-
originalDoc: PropertyDoc | null;
|
|
28
|
-
hasUnsavedChanges: boolean;
|
|
29
|
-
exportSettings: ExportSettings | undefined;
|
|
30
|
-
availableTags: string[];
|
|
31
|
-
projectFields: Record<string, string>;
|
|
32
|
-
toasts: Toast[];
|
|
33
|
-
rootPath: string;
|
|
34
|
-
isInitialized: boolean;
|
|
35
|
-
|
|
36
|
-
// アクション
|
|
37
|
-
setActiveConfigIndex: (index: number) => void;
|
|
38
|
-
setSelectedPath: (path: string) => void;
|
|
39
|
-
setEditingDoc: (doc: PropertyDoc | null) => void;
|
|
40
|
-
setHasUnsavedChanges: (value: boolean) => void;
|
|
41
|
-
showToast: (message: string, type?: Toast['type']) => void;
|
|
42
|
-
removeToast: (id: string) => void;
|
|
43
|
-
loadConfigFile: (filePath: string) => Promise<void>;
|
|
44
|
-
handleSelectConfigFiles: (filePaths: string[]) => Promise<void>;
|
|
45
|
-
handleRemoveConfig: (index: number) => Promise<void>;
|
|
46
|
-
handleReorderConfigs: (newConfigs: LoadedConfig[], newActiveIndex: number) => void;
|
|
47
|
-
handleSelectProperty: (path: string) => void;
|
|
48
|
-
handleSaveProperty: () => Promise<void>;
|
|
49
|
-
handleExport: (settings: ExportSettings) => Promise<void>;
|
|
50
|
-
handleAvailableTagsChange: (tags: string[]) => Promise<void>;
|
|
51
|
-
handleProjectFieldsChange: (fields: Record<string, string>) => Promise<void>;
|
|
52
|
-
checkForChanges: (current: PropertyDoc | null, original: PropertyDoc | null) => boolean;
|
|
53
|
-
resetSelection: () => void;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function useConfigManager(): UseConfigManagerReturn {
|
|
57
|
-
const [loadedConfigs, setLoadedConfigs] = useState<LoadedConfig[]>([]);
|
|
58
|
-
const [activeConfigIndex, setActiveConfigIndex] = useState<number>(0);
|
|
59
|
-
const [selectedPath, setSelectedPath] = useState<string>('');
|
|
60
|
-
const [isInitialized, setIsInitialized] = useState(false);
|
|
61
|
-
const [rootPath, setRootPath] = useState<string>('.');
|
|
62
|
-
|
|
63
|
-
// 編集中のプロパティドキュメント
|
|
64
|
-
const [editingDoc, setEditingDoc] = useState<PropertyDoc | null>(null);
|
|
65
|
-
const [originalDoc, setOriginalDoc] = useState<PropertyDoc | null>(null);
|
|
66
|
-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
67
|
-
|
|
68
|
-
// エクスポート設定
|
|
69
|
-
const [exportSettings, setExportSettings] = useState<ExportSettings | undefined>();
|
|
70
|
-
|
|
71
|
-
// 利用可能なタグ
|
|
72
|
-
const [availableTags, setAvailableTags] = useState<string[]>(['required', 'nullable', 'string', 'number', 'boolean']);
|
|
73
|
-
|
|
74
|
-
// プロジェクトのフィールド定義
|
|
75
|
-
const [projectFields, setProjectFields] = useState<Record<string, string>>(DEFAULT_FIELDS);
|
|
76
|
-
|
|
77
|
-
// トースト通知
|
|
78
|
-
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
79
|
-
|
|
80
|
-
const activeConfig = loadedConfigs[activeConfigIndex];
|
|
81
|
-
|
|
82
|
-
const showToast = useCallback((message: string, type: Toast['type'] = 'success') => {
|
|
83
|
-
const id = Date.now().toString();
|
|
84
|
-
setToasts(prev => [...prev, { id, message, type }]);
|
|
85
|
-
}, []);
|
|
86
|
-
|
|
87
|
-
const removeToast = useCallback((id: string) => {
|
|
88
|
-
setToasts(prev => prev.filter(t => t.id !== id));
|
|
89
|
-
}, []);
|
|
90
|
-
|
|
91
|
-
// 差分チェック関数
|
|
92
|
-
const checkForChanges = useCallback((current: PropertyDoc | null, original: PropertyDoc | null): boolean => {
|
|
93
|
-
if (!current || !original) return false;
|
|
94
|
-
|
|
95
|
-
// タグの比較
|
|
96
|
-
const currentTags = current.tags || [];
|
|
97
|
-
const originalTags = original.tags || [];
|
|
98
|
-
if (detectTagChanges(originalTags, currentTags)) return true;
|
|
99
|
-
|
|
100
|
-
// フィールドの比較
|
|
101
|
-
const currentFields = current.fields || {};
|
|
102
|
-
const originalFields = original.fields || {};
|
|
103
|
-
if (detectFieldChanges(originalFields, currentFields)) return true;
|
|
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
|
-
const oldTagOrder = availableTags;
|
|
460
|
-
const newTagOrder = tags;
|
|
461
|
-
|
|
462
|
-
setAvailableTags(tags);
|
|
463
|
-
await fetch('/api/config/metadata', {
|
|
464
|
-
method: 'POST',
|
|
465
|
-
headers: { 'Content-Type': 'application/json' },
|
|
466
|
-
body: JSON.stringify({ availableTags: tags })
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// タグの削除または順序変更がある場合、すべてのドキュメントを更新
|
|
470
|
-
const hasRemovedTags = removedTags.length > 0;
|
|
471
|
-
const hasOrderChanged = oldTagOrder.some((tag, idx) => tag !== newTagOrder[idx]) ||
|
|
472
|
-
oldTagOrder.length !== newTagOrder.length;
|
|
473
|
-
|
|
474
|
-
if (hasRemovedTags || hasOrderChanged) {
|
|
475
|
-
for (const config of loadedConfigs) {
|
|
476
|
-
let hasChanges = false;
|
|
477
|
-
const updatedProperties = { ...config.docs.properties };
|
|
478
|
-
|
|
479
|
-
for (const [propPath, doc] of Object.entries(updatedProperties)) {
|
|
480
|
-
if (doc.tags && doc.tags.length > 0) {
|
|
481
|
-
// 削除されたタグを除外
|
|
482
|
-
const filteredTags = doc.tags.filter(tag => !removedTags.includes(tag));
|
|
483
|
-
|
|
484
|
-
// availableTagsの順序でソート
|
|
485
|
-
const updatedTags = sortTagsByOrder(filteredTags, newTagOrder);
|
|
486
|
-
|
|
487
|
-
// タグの内容または順序が変わったかチェック
|
|
488
|
-
const tagsChanged = detectTagChanges(doc.tags, updatedTags);
|
|
489
|
-
|
|
490
|
-
if (tagsChanged) {
|
|
491
|
-
updatedProperties[propPath] = { ...doc, tags: updatedTags };
|
|
492
|
-
hasChanges = true;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (hasChanges) {
|
|
498
|
-
await fetch('/api/config/save', {
|
|
499
|
-
method: 'POST',
|
|
500
|
-
headers: { 'Content-Type': 'application/json' },
|
|
501
|
-
body: JSON.stringify({
|
|
502
|
-
configFilePath: config.filePath,
|
|
503
|
-
properties: updatedProperties
|
|
504
|
-
})
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
setLoadedConfigs(prev => prev.map(c =>
|
|
508
|
-
c.filePath === config.filePath
|
|
509
|
-
? { ...c, docs: { ...c.docs, properties: updatedProperties } }
|
|
510
|
-
: c
|
|
511
|
-
));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// 編集中のドキュメントも更新
|
|
516
|
-
if (editingDoc && editingDoc.tags && editingDoc.tags.length > 0) {
|
|
517
|
-
const filteredEditingTags = editingDoc.tags.filter(tag => !removedTags.includes(tag));
|
|
518
|
-
const updatedEditingTags = sortTagsByOrder(filteredEditingTags, newTagOrder);
|
|
519
|
-
|
|
520
|
-
const tagsChanged = detectTagChanges(editingDoc.tags, updatedEditingTags);
|
|
521
|
-
|
|
522
|
-
if (tagsChanged) {
|
|
523
|
-
const updated = { ...editingDoc, tags: updatedEditingTags };
|
|
524
|
-
setEditingDoc(updated);
|
|
525
|
-
|
|
526
|
-
if (originalDoc && originalDoc.tags) {
|
|
527
|
-
const filteredOriginalTags = originalDoc.tags.filter(tag => !removedTags.includes(tag));
|
|
528
|
-
const updatedOriginalTags = sortTagsByOrder(filteredOriginalTags, newTagOrder);
|
|
529
|
-
setOriginalDoc({ ...originalDoc, tags: updatedOriginalTags });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}, [availableTags, editingDoc, loadedConfigs, originalDoc]);
|
|
535
|
-
|
|
536
|
-
// プロジェクトフィールドを更新
|
|
537
|
-
const handleProjectFieldsChange = useCallback(async (fields: Record<string, string>) => {
|
|
538
|
-
const oldFieldKeys = Object.keys(projectFields);
|
|
539
|
-
const newFieldKeys = Object.keys(fields);
|
|
540
|
-
const removedFields = oldFieldKeys.filter(key => !newFieldKeys.includes(key));
|
|
541
|
-
|
|
542
|
-
setProjectFields(fields);
|
|
543
|
-
await fetch('/api/config/metadata', {
|
|
544
|
-
method: 'POST',
|
|
545
|
-
headers: { 'Content-Type': 'application/json' },
|
|
546
|
-
body: JSON.stringify({ fields })
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// すべての設定ファイルのドキュメントを更新
|
|
550
|
-
// フィールドの削除または順序変更に対応
|
|
551
|
-
for (const config of loadedConfigs) {
|
|
552
|
-
let hasChanges = false;
|
|
553
|
-
const updatedProperties = { ...config.docs.properties };
|
|
554
|
-
|
|
555
|
-
for (const [propPath, doc] of Object.entries(updatedProperties)) {
|
|
556
|
-
if (doc.fields) {
|
|
557
|
-
// 新しいフィールド定義の順序で再構築
|
|
558
|
-
const reorderedDocFields = reorderFields(doc.fields, newFieldKeys);
|
|
559
|
-
|
|
560
|
-
// 順序が変わったか、削除されたフィールドがあるかチェック
|
|
561
|
-
const oldKeys = Object.keys(doc.fields);
|
|
562
|
-
const orderChanged = oldKeys.some((key, idx) => key !== newFieldKeys[idx]);
|
|
563
|
-
const hasRemovedFields = removedFields.some(field => field in doc.fields);
|
|
564
|
-
|
|
565
|
-
if (orderChanged || hasRemovedFields || oldKeys.length !== newFieldKeys.length) {
|
|
566
|
-
updatedProperties[propPath] = { ...doc, fields: reorderedDocFields };
|
|
567
|
-
hasChanges = true;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (hasChanges) {
|
|
573
|
-
await fetch('/api/config/save', {
|
|
574
|
-
method: 'POST',
|
|
575
|
-
headers: { 'Content-Type': 'application/json' },
|
|
576
|
-
body: JSON.stringify({
|
|
577
|
-
configFilePath: config.filePath,
|
|
578
|
-
properties: updatedProperties
|
|
579
|
-
})
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
setLoadedConfigs(prev => prev.map(c =>
|
|
583
|
-
c.filePath === config.filePath
|
|
584
|
-
? { ...c, docs: { ...c.docs, properties: updatedProperties } }
|
|
585
|
-
: c
|
|
586
|
-
));
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (originalDoc) {
|
|
591
|
-
const updatedOriginalFields: Record<string, string> = {};
|
|
592
|
-
for (const key of newFieldKeys) {
|
|
593
|
-
updatedOriginalFields[key] = originalDoc.fields?.[key] || '';
|
|
594
|
-
}
|
|
595
|
-
setOriginalDoc({ ...originalDoc, fields: updatedOriginalFields });
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
if (editingDoc) {
|
|
599
|
-
const updatedEditingFields: Record<string, string> = {};
|
|
600
|
-
for (const key of newFieldKeys) {
|
|
601
|
-
updatedEditingFields[key] = editingDoc.fields?.[key] || '';
|
|
602
|
-
}
|
|
603
|
-
const updated = { ...editingDoc, fields: updatedEditingFields };
|
|
604
|
-
setEditingDoc(updated);
|
|
605
|
-
|
|
606
|
-
const updatedOriginal = originalDoc
|
|
607
|
-
? { ...originalDoc, fields: Object.fromEntries(newFieldKeys.map(key => [key, originalDoc.fields?.[key] || ''])) }
|
|
608
|
-
: null;
|
|
609
|
-
setHasUnsavedChanges(checkForChanges(updated, updatedOriginal));
|
|
610
|
-
}
|
|
611
|
-
}, [checkForChanges, editingDoc, loadedConfigs, originalDoc, projectFields]);
|
|
612
|
-
|
|
613
|
-
// 選択状態をリセット
|
|
614
|
-
const resetSelection = useCallback(() => {
|
|
615
|
-
setSelectedPath('');
|
|
616
|
-
setEditingDoc(null);
|
|
617
|
-
setHasUnsavedChanges(false);
|
|
618
|
-
}, []);
|
|
619
|
-
|
|
620
|
-
return {
|
|
621
|
-
loadedConfigs,
|
|
622
|
-
activeConfigIndex,
|
|
623
|
-
activeConfig,
|
|
624
|
-
selectedPath,
|
|
625
|
-
editingDoc,
|
|
626
|
-
originalDoc,
|
|
627
|
-
hasUnsavedChanges,
|
|
628
|
-
exportSettings,
|
|
629
|
-
availableTags,
|
|
630
|
-
projectFields,
|
|
631
|
-
toasts,
|
|
632
|
-
rootPath,
|
|
633
|
-
isInitialized,
|
|
634
|
-
|
|
635
|
-
setActiveConfigIndex,
|
|
636
|
-
setSelectedPath,
|
|
637
|
-
setEditingDoc,
|
|
638
|
-
setHasUnsavedChanges,
|
|
639
|
-
showToast,
|
|
640
|
-
removeToast,
|
|
641
|
-
loadConfigFile,
|
|
642
|
-
handleSelectConfigFiles,
|
|
643
|
-
handleRemoveConfig,
|
|
644
|
-
handleReorderConfigs,
|
|
645
|
-
handleSelectProperty,
|
|
646
|
-
handleSaveProperty,
|
|
647
|
-
handleExport,
|
|
648
|
-
handleAvailableTagsChange,
|
|
649
|
-
handleProjectFieldsChange,
|
|
650
|
-
checkForChanges,
|
|
651
|
-
resetSelection
|
|
652
|
-
};
|
|
653
|
-
}
|