plugin-migration-manager 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/client.d.ts +2 -2
- package/client.js +1 -1
- package/dist/client/index.js +10 -1
- package/dist/client-v2/946.4af90ac0c3eee699.js +10 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/externalVersion.js +15 -4
- package/dist/locale/en-US.json +68 -8
- package/dist/locale/id-ID.json +10 -8
- package/dist/locale/zh-CN.json +68 -0
- package/dist/server/controllers/migration.js +26 -1
- package/dist/server/index.js +17 -12
- package/package.json +43 -23
- package/server.d.ts +2 -2
- package/server.js +1 -1
- package/src/client/index.tsx +7 -18
- package/src/client-v2/index.tsx +1 -0
- package/src/client-v2/locale.ts +13 -0
- package/src/client-v2/pages/MigrationPage.tsx +808 -0
- package/src/client-v2/plugin.tsx +22 -0
- package/src/locale/en-US.json +68 -8
- package/src/locale/id-ID.json +10 -8
- package/src/locale/zh-CN.json +68 -0
- package/src/server/controllers/migration.ts +17 -0
- package/src/server/index.ts +33 -37
- package/INSTALL.md +0 -100
- package/Migration_Manager_Documentation_1.0.pdf +0 -0
- package/README.md +0 -30
- package/dist/client/bf902aacdcc9b8b8.js +0 -1
- package/dist/client/index.d.ts +0 -5
- package/dist/client/pages/MigrationPage.d.ts +0 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -39
- package/dist/server/controllers/migration.d.ts +0 -27
- package/dist/server/index.d.ts +0 -12
- package/src/client/pages/MigrationPage.tsx +0 -565
- package/src/index.ts +0 -2
|
@@ -1,565 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Card, Tabs, Button, Table, message, Upload, Space, Typography, Alert, Modal,
|
|
4
|
-
Tag, Tooltip, Spin, Input,
|
|
5
|
-
} from 'antd';
|
|
6
|
-
import type { TabsProps } from 'antd';
|
|
7
|
-
import {
|
|
8
|
-
DownloadOutlined, UploadOutlined, DatabaseOutlined, BranchesOutlined,
|
|
9
|
-
LayoutOutlined, ExclamationCircleOutlined
|
|
10
|
-
} from '@ant-design/icons';
|
|
11
|
-
import { useRequest, useAPIClient } from '@nocobase/client';
|
|
12
|
-
|
|
13
|
-
const { Title, Text } = Typography;
|
|
14
|
-
|
|
15
|
-
const { Search } = Input;
|
|
16
|
-
|
|
17
|
-
function unwrap<T = any>(res: any): T {
|
|
18
|
-
return (res?.data?.data ?? res?.data ?? res) as T;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function prettifyCollectionTitle(title: string) {
|
|
22
|
-
if (!title) return '';
|
|
23
|
-
return title.replace(/\{\{t\("(.+?)"\)\}\}/g, '$1').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type ItemsShape = { collections: any[]; workflows: any[]; uiSchemas: any[]; };
|
|
27
|
-
|
|
28
|
-
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
29
|
-
|
|
30
|
-
export const MigrationPage: React.FC = () => {
|
|
31
|
-
const api = useAPIClient();
|
|
32
|
-
const [activeTab, setActiveTab] = useState('export');
|
|
33
|
-
const [availableItems, setAvailableItems] = useState<ItemsShape>({ collections: [], workflows: [], uiSchemas: [] });
|
|
34
|
-
|
|
35
|
-
const [selectedCollections, setSelectedCollections] = useState<string[]>([]);
|
|
36
|
-
const [selectedWorkflows, setSelectedWorkflows] = useState<(string | number)[]>([]);
|
|
37
|
-
const [selectedSchemas, setSelectedSchemas] = useState<string[]>([]);
|
|
38
|
-
|
|
39
|
-
const [restarting, setRestarting] = useState(false);
|
|
40
|
-
const [applying, setApplying] = useState(false);
|
|
41
|
-
const [lastImportPayload, setLastImportPayload] = useState<any | null>(null);
|
|
42
|
-
const [lastWasPreview, setLastWasPreview] = useState(false);
|
|
43
|
-
|
|
44
|
-
const [collectionQuery, setCollectionQuery] = useState('');
|
|
45
|
-
const [workflowQuery, setWorkflowQuery] = useState('');
|
|
46
|
-
const [schemaQuery, setSchemaQuery] = useState('');
|
|
47
|
-
|
|
48
|
-
const [collectionPage, setCollectionPage] = useState({ current: 1, pageSize: 10 });
|
|
49
|
-
const [workflowPage, setWorkflowPage] = useState({ current: 1, pageSize: 10 });
|
|
50
|
-
const [schemaPage, setSchemaPage] = useState({ current: 1, pageSize: 10 });
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
|
|
54
|
-
if (restarting || applying) {
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
e.returnValue = '';
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
window.addEventListener('beforeunload', beforeUnloadHandler);
|
|
60
|
-
return () => window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
61
|
-
}, [restarting, applying]);
|
|
62
|
-
|
|
63
|
-
const { run: fetchItems, loading: loadingItems } = useRequest(
|
|
64
|
-
{ url: 'migration:list', method: 'get' },
|
|
65
|
-
{
|
|
66
|
-
manual: false,
|
|
67
|
-
onSuccess: (res) => {
|
|
68
|
-
const payload = unwrap<ItemsShape>(res);
|
|
69
|
-
setAvailableItems({
|
|
70
|
-
collections: payload?.collections ?? [],
|
|
71
|
-
workflows: payload?.workflows ?? [],
|
|
72
|
-
uiSchemas: payload?.uiSchemas ?? [],
|
|
73
|
-
});
|
|
74
|
-
},
|
|
75
|
-
onError: () => setAvailableItems({ collections: [], workflows: [], uiSchemas: [] }),
|
|
76
|
-
}
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const { run: exportData, loading: exporting } = useRequest(
|
|
80
|
-
{ url: 'migration:export', method: 'post' },
|
|
81
|
-
{
|
|
82
|
-
manual: true,
|
|
83
|
-
onSuccess: (res) => {
|
|
84
|
-
const payload = unwrap(res);
|
|
85
|
-
const hasData =
|
|
86
|
-
(payload?.collections?.length > 0) ||
|
|
87
|
-
(payload?.workflows?.length > 0) ||
|
|
88
|
-
(payload?.uiSchemas?.length > 0) ||
|
|
89
|
-
(payload?.desktopRoutes?.length > 0);
|
|
90
|
-
if (!hasData) {
|
|
91
|
-
message.warning('Export succeeded but no data.');
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
|
95
|
-
const url = URL.createObjectURL(blob);
|
|
96
|
-
const a = document.createElement('a');
|
|
97
|
-
a.href = url;
|
|
98
|
-
a.download = `nocobase-migration-${Date.now()}.json`;
|
|
99
|
-
a.click();
|
|
100
|
-
URL.revokeObjectURL(url);
|
|
101
|
-
message.success(
|
|
102
|
-
`Export successful! ${payload.collections?.length || 0} collections, ${payload.workflows?.length || 0} workflows, ${payload.uiSchemas?.length || 0} UI schemas, ${payload.desktopRoutes?.length || 0} routes`
|
|
103
|
-
);
|
|
104
|
-
},
|
|
105
|
-
onError: (error) => message.error(`Export failed: ${error.message}`),
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const triggerRestart = async (silentWaitMs = 10000, extraWaitMs = 2000) => {
|
|
111
|
-
setRestarting(true);
|
|
112
|
-
try {
|
|
113
|
-
await api.request({ url: 'app:restart', method: 'post', timeout: 60000 });
|
|
114
|
-
await delay(silentWaitMs);
|
|
115
|
-
await delay(extraWaitMs);
|
|
116
|
-
} finally {
|
|
117
|
-
setRestarting(false);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const applyWithRetry = async (collectionsPayload: any, attempts = 15, intervalMs = 1500) => {
|
|
122
|
-
let lastErr: any = null;
|
|
123
|
-
for (let i = 0; i < attempts; i++) {
|
|
124
|
-
try {
|
|
125
|
-
const res = await api.request({
|
|
126
|
-
url: 'migration:apply',
|
|
127
|
-
method: 'post',
|
|
128
|
-
data: { data: { collections: collectionsPayload } },
|
|
129
|
-
headers: { 'Content-Type': 'application/json' },
|
|
130
|
-
timeout: 600000,
|
|
131
|
-
});
|
|
132
|
-
return unwrap<any>(res);
|
|
133
|
-
} catch (e: any) {
|
|
134
|
-
lastErr = e;
|
|
135
|
-
if (e?.response?.status === 503 || String(e?.message || '').includes('APP_COMMANDING')) {
|
|
136
|
-
await delay(intervalMs);
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
throw e;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
throw lastErr || new Error('Apply failed after several attempts.');
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const { run: runImport, loading: importing } = useRequest(
|
|
146
|
-
(payloadWithOptions: any) =>
|
|
147
|
-
api.request({
|
|
148
|
-
url: 'migration:import',
|
|
149
|
-
method: 'post',
|
|
150
|
-
data: { data: payloadWithOptions },
|
|
151
|
-
headers: { 'Content-Type': 'application/json' },
|
|
152
|
-
timeout: 600000,
|
|
153
|
-
}) as any,
|
|
154
|
-
{
|
|
155
|
-
manual: true,
|
|
156
|
-
onSuccess: async (res) => {
|
|
157
|
-
const data = unwrap<any>(res);
|
|
158
|
-
const { needsConfirmation, results } = data || {};
|
|
159
|
-
|
|
160
|
-
if (needsConfirmation) {
|
|
161
|
-
const conflicts: any[] = results?.collections?.conflicts || [];
|
|
162
|
-
Modal.confirm({
|
|
163
|
-
title: 'Confirm Potentially Data-Altering Changes',
|
|
164
|
-
icon: <ExclamationCircleOutlined />,
|
|
165
|
-
width: 760,
|
|
166
|
-
okText: 'Proceed & Override',
|
|
167
|
-
cancelText: 'Cancel',
|
|
168
|
-
content: (
|
|
169
|
-
<div>
|
|
170
|
-
<Alert type="warning" showIcon message="Conflicts were found on existing fields." style={{ marginBottom: 12 }} />
|
|
171
|
-
<div style={{ maxHeight: 360, overflow: 'auto' }}>
|
|
172
|
-
<Table
|
|
173
|
-
size="small"
|
|
174
|
-
pagination={false}
|
|
175
|
-
rowKey={(r) => `${r.collection}.${r.field}`}
|
|
176
|
-
dataSource={conflicts}
|
|
177
|
-
columns={[
|
|
178
|
-
{ title: 'Collection', dataIndex: 'collection' },
|
|
179
|
-
{ title: 'Field', dataIndex: 'field' },
|
|
180
|
-
{ title: 'Current', render: (_: any, r: any) => `type=${r.current.type}, interface=${r.current.interface}, unique=${String(r.current.unique)}, allowNull=${String(r.current.allowNull)}, pk=${String(r.current.primaryKey)}` },
|
|
181
|
-
{ title: 'Incoming', render: (_: any, r: any) => `type=${r.incoming.type}, interface=${r.incoming.interface}, unique=${String(r.incoming.unique)}, allowNull=${String(r.incoming.allowNull)}, pk=${String(r.incoming.primaryKey)}` },
|
|
182
|
-
]}
|
|
183
|
-
/>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
),
|
|
187
|
-
onOk: () => {
|
|
188
|
-
if (!lastImportPayload) return;
|
|
189
|
-
setLastWasPreview(false);
|
|
190
|
-
runImport({ ...lastImportPayload, options: { forceOverride: true } });
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (lastWasPreview && lastImportPayload) {
|
|
197
|
-
setLastWasPreview(false);
|
|
198
|
-
runImport(lastImportPayload);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const r = results || {};
|
|
203
|
-
Modal.success({
|
|
204
|
-
title: 'Import Successful',
|
|
205
|
-
content: (
|
|
206
|
-
<div>
|
|
207
|
-
<p>Collections: {r.collections?.success || 0} succeeded, {r.collections?.failed || 0} failed</p>
|
|
208
|
-
<p>Workflows: {r.workflows?.success || 0} succeeded, {r.workflows?.failed || 0} failed</p>
|
|
209
|
-
<p>UI Schemas: {r.uiSchemas?.success || 0} succeeded, {r.uiSchemas?.failed || 0} failed</p>
|
|
210
|
-
{(r.collections?.errors?.length > 0 || r.workflows?.errors?.length > 0 || r.uiSchemas?.errors?.length > 0) && (
|
|
211
|
-
<Alert
|
|
212
|
-
message="Some errors occurred"
|
|
213
|
-
description={
|
|
214
|
-
<ul>
|
|
215
|
-
{[...(r.collections?.errors || []), ...(r.workflows?.errors || []), ...(r.uiSchemas?.errors || [])].map((err: any, idx: number) => (
|
|
216
|
-
<li key={idx}>{err.error || String(err)}</li>
|
|
217
|
-
))}
|
|
218
|
-
</ul>
|
|
219
|
-
}
|
|
220
|
-
type="warning"
|
|
221
|
-
style={{ marginTop: 10 }}
|
|
222
|
-
/>
|
|
223
|
-
)}
|
|
224
|
-
</div>
|
|
225
|
-
),
|
|
226
|
-
okText: 'Continue',
|
|
227
|
-
onOk: async () => {
|
|
228
|
-
try {
|
|
229
|
-
setApplying(true);
|
|
230
|
-
await triggerRestart(12000, 2000);
|
|
231
|
-
const collectionsPayload = (lastImportPayload?.collections || []).map((c: any) =>
|
|
232
|
-
typeof c === 'string' ? { name: c } : c
|
|
233
|
-
);
|
|
234
|
-
const applyData = await applyWithRetry(collectionsPayload, 15, 1500);
|
|
235
|
-
|
|
236
|
-
Modal.success({
|
|
237
|
-
title: 'Apply Complete',
|
|
238
|
-
content: (
|
|
239
|
-
<div>
|
|
240
|
-
<p>The changes have been successfully applied. Click <strong>Continue</strong> to refresh the page for the changes to take effect.</p>
|
|
241
|
-
{(applyData?.results?.errors?.length > 0) && (
|
|
242
|
-
<Alert
|
|
243
|
-
type="warning"
|
|
244
|
-
message="Errors occurred during apply"
|
|
245
|
-
description={
|
|
246
|
-
<ul style={{ marginBottom: 0 }}>
|
|
247
|
-
{(applyData?.results?.errors || []).map((e: any, i: number) => (
|
|
248
|
-
<li key={i}>
|
|
249
|
-
{(e.collection || e.route || e.schema || 'item')}: {e.error}
|
|
250
|
-
</li>
|
|
251
|
-
))}
|
|
252
|
-
</ul>
|
|
253
|
-
}
|
|
254
|
-
/>
|
|
255
|
-
)}
|
|
256
|
-
</div>
|
|
257
|
-
),
|
|
258
|
-
okText: 'Continue',
|
|
259
|
-
onOk: () => {
|
|
260
|
-
window.location.reload();
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
} catch (e: any) {
|
|
264
|
-
message.error(e?.message || 'Apply failed');
|
|
265
|
-
} finally {
|
|
266
|
-
setApplying(false);
|
|
267
|
-
setLastImportPayload(null);
|
|
268
|
-
fetchItems();
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
fetchItems();
|
|
273
|
-
},
|
|
274
|
-
onError: (error) => message.error(`Import failed: ${error.message}`),
|
|
275
|
-
}
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
const handleExport = () => {
|
|
279
|
-
if (!selectedCollections.length && !selectedWorkflows.length && !selectedSchemas.length) {
|
|
280
|
-
message.warning('Select at least one item to export');
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
Modal.confirm({
|
|
284
|
-
title: 'Export Confirmation',
|
|
285
|
-
icon: <ExclamationCircleOutlined />,
|
|
286
|
-
content: (
|
|
287
|
-
<div>
|
|
288
|
-
<p>You will export:</p>
|
|
289
|
-
<ul>
|
|
290
|
-
<li>{selectedCollections.length} Collections</li>
|
|
291
|
-
<li>{selectedWorkflows.length} Workflows</li>
|
|
292
|
-
<li>{selectedSchemas.length} UI Schemas/Routes</li>
|
|
293
|
-
</ul>
|
|
294
|
-
<Alert message="Note" description="Export includes collection structure, workflow config, UI schema subtree, and desktop routes." type="info" style={{ marginTop: 10 }} />
|
|
295
|
-
</div>
|
|
296
|
-
),
|
|
297
|
-
onOk: () => {
|
|
298
|
-
exportData({
|
|
299
|
-
data: {
|
|
300
|
-
collections: selectedCollections,
|
|
301
|
-
workflows: selectedWorkflows,
|
|
302
|
-
uiSchemas: [],
|
|
303
|
-
desktopRoutes: selectedSchemas,
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const handleImport = (file: File) => {
|
|
312
|
-
const reader = new FileReader();
|
|
313
|
-
reader.onload = (e) => {
|
|
314
|
-
try {
|
|
315
|
-
const payload = JSON.parse(e.target?.result as string);
|
|
316
|
-
setLastImportPayload(payload);
|
|
317
|
-
setLastWasPreview(true);
|
|
318
|
-
Modal.confirm({
|
|
319
|
-
title: 'Import Confirmation',
|
|
320
|
-
icon: <ExclamationCircleOutlined />,
|
|
321
|
-
content: (
|
|
322
|
-
<div>
|
|
323
|
-
<p>The file will import:</p>
|
|
324
|
-
<ul>
|
|
325
|
-
<li>{payload.collections?.length || 0} Collections</li>
|
|
326
|
-
<li>{payload.workflows?.length || 0} Workflows</li>
|
|
327
|
-
<li>{payload.uiSchemas?.length || 0} UI Schemas</li>
|
|
328
|
-
</ul>
|
|
329
|
-
<Alert message="Warning" description="The first step will run a PREVIEW. If safe, the process will proceed automatically." type="warning" style={{ marginTop: 10 }} />
|
|
330
|
-
</div>
|
|
331
|
-
),
|
|
332
|
-
onOk: () => runImport({ ...payload, options: { preview: true } }),
|
|
333
|
-
});
|
|
334
|
-
} catch {
|
|
335
|
-
message.error('Invalid or corrupt file');
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
reader.readAsText(file);
|
|
339
|
-
return false;
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
const filteredCollections = useMemo(() => {
|
|
343
|
-
const q = collectionQuery.trim().toLowerCase();
|
|
344
|
-
const arr = (availableItems.collections || []).filter((c: any) =>
|
|
345
|
-
(c.title || c.name || '').toLowerCase().includes(q)
|
|
346
|
-
);
|
|
347
|
-
return { total: arr.length, data: arr };
|
|
348
|
-
}, [availableItems.collections, collectionQuery]);
|
|
349
|
-
|
|
350
|
-
const pagedCollections = useMemo(() => {
|
|
351
|
-
const start = (collectionPage.current - 1) * collectionPage.pageSize;
|
|
352
|
-
return filteredCollections.data.slice(start, start + collectionPage.pageSize);
|
|
353
|
-
}, [filteredCollections, collectionPage]);
|
|
354
|
-
|
|
355
|
-
const filteredWorkflows = useMemo(() => {
|
|
356
|
-
const q = workflowQuery.trim().toLowerCase();
|
|
357
|
-
const arr = (availableItems.workflows || []).filter((w: any) =>
|
|
358
|
-
(w.title || '').toLowerCase().includes(q)
|
|
359
|
-
);
|
|
360
|
-
return { total: arr.length, data: arr };
|
|
361
|
-
}, [availableItems.workflows, workflowQuery]);
|
|
362
|
-
|
|
363
|
-
const pagedWorkflows = useMemo(() => {
|
|
364
|
-
const start = (workflowPage.current - 1) * workflowPage.pageSize;
|
|
365
|
-
return filteredWorkflows.data.slice(start, start + workflowPage.pageSize);
|
|
366
|
-
}, [filteredWorkflows, workflowPage]);
|
|
367
|
-
|
|
368
|
-
const filteredSchemas = useMemo(() => {
|
|
369
|
-
const q = schemaQuery.trim().toLowerCase();
|
|
370
|
-
const arr = (availableItems.uiSchemas || []).filter((s: any) =>
|
|
371
|
-
((s.displayTitle || s.title || s.name || '') as string).toLowerCase().includes(q)
|
|
372
|
-
);
|
|
373
|
-
return { total: arr.length, data: arr };
|
|
374
|
-
}, [availableItems.uiSchemas, schemaQuery]);
|
|
375
|
-
|
|
376
|
-
const pagedSchemas = useMemo(() => {
|
|
377
|
-
const start = (schemaPage.current - 1) * schemaPage.pageSize;
|
|
378
|
-
return filteredSchemas.data.slice(start, start + schemaPage.pageSize);
|
|
379
|
-
}, [filteredSchemas, schemaPage]);
|
|
380
|
-
|
|
381
|
-
return (
|
|
382
|
-
<div style={{ padding: 24, position: 'relative' }}>
|
|
383
|
-
<Card>
|
|
384
|
-
<Title level={2}>Migration Manager</Title>
|
|
385
|
-
<Text type="secondary">Export and import collections, workflows, and UI schemas across NocoBase instances</Text>
|
|
386
|
-
|
|
387
|
-
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[
|
|
388
|
-
{
|
|
389
|
-
label: 'Export',
|
|
390
|
-
key: 'export',
|
|
391
|
-
children: (
|
|
392
|
-
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
393
|
-
<Card title={<Space><DatabaseOutlined />Collections</Space>} size="small">
|
|
394
|
-
<div style={{ marginBottom: 12 }}>
|
|
395
|
-
<Search
|
|
396
|
-
allowClear
|
|
397
|
-
placeholder="Filter by Title"
|
|
398
|
-
onSearch={(v) => { setCollectionQuery(v); setCollectionPage({ ...collectionPage, current: 1 }); }}
|
|
399
|
-
onChange={(e) => { setCollectionQuery(e.target.value); setCollectionPage({ ...collectionPage, current: 1 }); }}
|
|
400
|
-
value={collectionQuery}
|
|
401
|
-
style={{ maxWidth: 320 }}
|
|
402
|
-
/>
|
|
403
|
-
</div>
|
|
404
|
-
<Table
|
|
405
|
-
rowSelection={{ selectedRowKeys: selectedCollections, onChange: (keys) => setSelectedCollections(keys as string[]) }}
|
|
406
|
-
columns={[
|
|
407
|
-
{ title: 'Name', dataIndex: 'name', key: 'name' },
|
|
408
|
-
{ title: 'Title', dataIndex: 'title', key: 'title', render: prettifyCollectionTitle },
|
|
409
|
-
{ title: 'Fields', dataIndex: 'fields', key: 'fields' },
|
|
410
|
-
]}
|
|
411
|
-
dataSource={pagedCollections}
|
|
412
|
-
rowKey="name"
|
|
413
|
-
loading={loadingItems}
|
|
414
|
-
pagination={{
|
|
415
|
-
current: collectionPage.current,
|
|
416
|
-
pageSize: collectionPage.pageSize,
|
|
417
|
-
total: filteredCollections.total,
|
|
418
|
-
showSizeChanger: true,
|
|
419
|
-
onChange: (current, pageSize) => setCollectionPage({ current, pageSize }),
|
|
420
|
-
}}
|
|
421
|
-
size="small"
|
|
422
|
-
/>
|
|
423
|
-
</Card>
|
|
424
|
-
|
|
425
|
-
<Card title={<Space><BranchesOutlined />Workflows</Space>} size="small">
|
|
426
|
-
<div style={{ marginBottom: 12 }}>
|
|
427
|
-
<Search
|
|
428
|
-
allowClear
|
|
429
|
-
placeholder="Filter by Title"
|
|
430
|
-
onSearch={(v) => { setWorkflowQuery(v); setWorkflowPage({ ...workflowPage, current: 1 }); }}
|
|
431
|
-
onChange={(e) => { setWorkflowQuery(e.target.value); setWorkflowPage({ ...workflowPage, current: 1 }); }}
|
|
432
|
-
value={workflowQuery}
|
|
433
|
-
style={{ maxWidth: 320 }}
|
|
434
|
-
/>
|
|
435
|
-
</div>
|
|
436
|
-
<Table
|
|
437
|
-
rowSelection={{ selectedRowKeys: selectedWorkflows, onChange: (keys) => setSelectedWorkflows(keys as (string | number)[]) }}
|
|
438
|
-
columns={[
|
|
439
|
-
{ title: 'Title', dataIndex: 'title', key: 'title' },
|
|
440
|
-
{ title: 'Key', dataIndex: 'key', key: 'key' },
|
|
441
|
-
{ title: 'Status', dataIndex: 'enabled', key: 'enabled', render: (v: boolean) => (v ? 'Enabled' : 'Disabled') },
|
|
442
|
-
]}
|
|
443
|
-
dataSource={pagedWorkflows}
|
|
444
|
-
rowKey="id"
|
|
445
|
-
loading={loadingItems}
|
|
446
|
-
pagination={{
|
|
447
|
-
current: workflowPage.current,
|
|
448
|
-
pageSize: workflowPage.pageSize,
|
|
449
|
-
total: filteredWorkflows.total,
|
|
450
|
-
showSizeChanger: true,
|
|
451
|
-
onChange: (current, pageSize) => setWorkflowPage({ current, pageSize }),
|
|
452
|
-
}}
|
|
453
|
-
size="small"
|
|
454
|
-
/>
|
|
455
|
-
</Card>
|
|
456
|
-
|
|
457
|
-
<Card title={<Space><LayoutOutlined />UI Schemas</Space>} size="small">
|
|
458
|
-
<div style={{ marginBottom: 12 }}>
|
|
459
|
-
<Search
|
|
460
|
-
allowClear
|
|
461
|
-
placeholder="Filter by Title"
|
|
462
|
-
onSearch={(v) => { setSchemaQuery(v); setSchemaPage({ ...schemaPage, current: 1 }); }}
|
|
463
|
-
onChange={(e) => { setSchemaQuery(e.target.value); setSchemaPage({ ...schemaPage, current: 1 }); }}
|
|
464
|
-
value={schemaQuery}
|
|
465
|
-
style={{ maxWidth: 320 }}
|
|
466
|
-
/>
|
|
467
|
-
</div>
|
|
468
|
-
<Table
|
|
469
|
-
rowSelection={{ selectedRowKeys: selectedSchemas, onChange: (keys) => setSelectedSchemas(keys as string[]) }}
|
|
470
|
-
columns={[
|
|
471
|
-
{
|
|
472
|
-
title: 'Menu / Page',
|
|
473
|
-
dataIndex: 'displayTitle',
|
|
474
|
-
key: 'displayTitle',
|
|
475
|
-
render: (_: any, record: any) => (
|
|
476
|
-
<Space>
|
|
477
|
-
<Text>{record?.displayTitle || record?.title || record?.name || 'Untitled'}</Text>
|
|
478
|
-
<Tag>{record?.type}</Tag>
|
|
479
|
-
</Space>
|
|
480
|
-
),
|
|
481
|
-
},
|
|
482
|
-
{ title: 'UID', dataIndex: 'schemaUid', key: 'schemaUid' },
|
|
483
|
-
]}
|
|
484
|
-
dataSource={pagedSchemas}
|
|
485
|
-
rowKey="schemaUid"
|
|
486
|
-
loading={loadingItems}
|
|
487
|
-
pagination={{
|
|
488
|
-
current: schemaPage.current,
|
|
489
|
-
pageSize: schemaPage.pageSize,
|
|
490
|
-
total: filteredSchemas.total,
|
|
491
|
-
showSizeChanger: true,
|
|
492
|
-
onChange: (current, pageSize) => setSchemaPage({ current, pageSize }),
|
|
493
|
-
}}
|
|
494
|
-
size="small"
|
|
495
|
-
/>
|
|
496
|
-
</Card>
|
|
497
|
-
|
|
498
|
-
<Button
|
|
499
|
-
type="primary"
|
|
500
|
-
icon={<DownloadOutlined />}
|
|
501
|
-
onClick={handleExport}
|
|
502
|
-
loading={exporting}
|
|
503
|
-
size="large"
|
|
504
|
-
disabled={restarting || applying}
|
|
505
|
-
>
|
|
506
|
-
Export Selected Items
|
|
507
|
-
</Button>
|
|
508
|
-
</Space>
|
|
509
|
-
),
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
label: 'Import',
|
|
513
|
-
key: 'import',
|
|
514
|
-
children: (
|
|
515
|
-
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
516
|
-
<Alert
|
|
517
|
-
message="Import from Development to Production"
|
|
518
|
-
description={
|
|
519
|
-
<div>
|
|
520
|
-
<p>Upload the exported JSON file from the development server.</p>
|
|
521
|
-
<p><strong>What will be imported:</strong></p>
|
|
522
|
-
<ul>
|
|
523
|
-
<li>Collection structure (without data)</li>
|
|
524
|
-
<li>Workflow configuration</li>
|
|
525
|
-
<li>UI Schema/Page design</li>
|
|
526
|
-
</ul>
|
|
527
|
-
<p><strong>What will NOT be affected:</strong></p>
|
|
528
|
-
<ul>
|
|
529
|
-
<li>Data within collections</li>
|
|
530
|
-
<li>Collections not present in the import file</li>
|
|
531
|
-
</ul>
|
|
532
|
-
</div>
|
|
533
|
-
}
|
|
534
|
-
type="info"
|
|
535
|
-
/>
|
|
536
|
-
|
|
537
|
-
<Upload accept=".json" beforeUpload={handleImport} showUploadList={false} disabled={restarting || applying}>
|
|
538
|
-
<Button type="primary" icon={<UploadOutlined />} loading={importing} size="large" disabled={restarting || applying}>
|
|
539
|
-
Upload Migration File (.json)
|
|
540
|
-
</Button>
|
|
541
|
-
</Upload>
|
|
542
|
-
</Space>
|
|
543
|
-
),
|
|
544
|
-
},
|
|
545
|
-
]} />
|
|
546
|
-
</Card>
|
|
547
|
-
|
|
548
|
-
{(restarting || applying) && (
|
|
549
|
-
<div
|
|
550
|
-
style={{
|
|
551
|
-
position: 'fixed',
|
|
552
|
-
inset: 0,
|
|
553
|
-
backgroundColor: 'rgba(255,255,255,0.8)',
|
|
554
|
-
display: 'flex',
|
|
555
|
-
alignItems: 'center',
|
|
556
|
-
justifyContent: 'center',
|
|
557
|
-
zIndex: 9999,
|
|
558
|
-
}}
|
|
559
|
-
>
|
|
560
|
-
<Spin size="large" tip={restarting ? 'Restarting...' : 'Applying...'} />
|
|
561
|
-
</div>
|
|
562
|
-
)}
|
|
563
|
-
</div>
|
|
564
|
-
);
|
|
565
|
-
};
|
package/src/index.ts
DELETED