@oneuptime/common 11.0.0 → 11.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Tests/Utils/ModelImportExport.test.ts +366 -0
- package/UI/Components/ImportExport/ExportModelCard.tsx +90 -0
- package/UI/Components/ImportExport/ImportModelsModal.tsx +239 -0
- package/UI/Components/ModelTable/ModelTable.tsx +294 -143
- package/UI/Utils/ModelImportExport.ts +207 -0
- package/Utils/ModelImportExport.ts +369 -0
- package/build/dist/UI/Components/ImportExport/ExportModelCard.js +50 -0
- package/build/dist/UI/Components/ImportExport/ExportModelCard.js.map +1 -0
- package/build/dist/UI/Components/ImportExport/ImportModelsModal.js +115 -0
- package/build/dist/UI/Components/ImportExport/ImportModelsModal.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/ModelTable.js +166 -74
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Utils/ModelImportExport.js +142 -0
- package/build/dist/UI/Utils/ModelImportExport.js.map +1 -0
- package/build/dist/Utils/ModelImportExport.js +257 -0
- package/build/dist/Utils/ModelImportExport.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import ModelAPI, { RequestOptions } from "../../Utils/ModelAPI/ModelAPI";
|
|
2
|
+
import API from "../../Utils/API/API";
|
|
3
|
+
import ModelImportExportUtil, {
|
|
4
|
+
ImportResult,
|
|
5
|
+
} from "../../Utils/ModelImportExport";
|
|
6
|
+
import PermissionUtil from "../../Utils/Permission";
|
|
7
|
+
import User from "../../Utils/User";
|
|
2
8
|
import { FormType, ModelField } from "../Forms/ModelForm";
|
|
3
9
|
import ModelFormModal from "../ModelFormModal/ModelFormModal";
|
|
4
|
-
import BaseModelTable, {
|
|
10
|
+
import BaseModelTable, {
|
|
11
|
+
BaseTableProps,
|
|
12
|
+
BulkActionProps,
|
|
13
|
+
ModalType,
|
|
14
|
+
} from "./BaseModelTable";
|
|
15
|
+
import {
|
|
16
|
+
BulkActionButtonSchema,
|
|
17
|
+
BulkActionFailed,
|
|
18
|
+
BulkActionOnClickProps,
|
|
19
|
+
} from "../BulkUpdate/BulkUpdateForm";
|
|
20
|
+
import ImportModelsModal from "../ImportExport/ImportModelsModal";
|
|
21
|
+
import { ButtonStyleType } from "../Button/Button";
|
|
22
|
+
import { ComponentProps as CardComponentProps } from "../Card/Card";
|
|
5
23
|
import { AnalyticsBaseModelType } from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
6
24
|
import BaseModel, {
|
|
7
25
|
DatabaseBaseModelType,
|
|
8
26
|
} from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
9
27
|
import Dictionary from "../../../Types/Dictionary";
|
|
28
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
10
29
|
import { JSONObject } from "../../../Types/JSON";
|
|
11
30
|
import ObjectID from "../../../Types/ObjectID";
|
|
12
|
-
import
|
|
31
|
+
import Permission from "../../../Types/Permission";
|
|
32
|
+
import React, { ReactElement, useState } from "react";
|
|
13
33
|
import Query from "../../../Types/BaseDatabase/Query";
|
|
14
34
|
import GroupBy from "../../../Types/BaseDatabase/GroupBy";
|
|
15
35
|
import Sort from "../../../Types/BaseDatabase/Sort";
|
|
@@ -18,6 +38,7 @@ import Select from "../../../Types/BaseDatabase/Select";
|
|
|
18
38
|
export interface ComponentProps<TBaseModel extends BaseModel>
|
|
19
39
|
extends BaseTableProps<TBaseModel> {
|
|
20
40
|
modelAPI?: typeof ModelAPI | undefined;
|
|
41
|
+
enableJsonImportExport?: boolean | undefined;
|
|
21
42
|
}
|
|
22
43
|
|
|
23
44
|
const ModelTable: <TBaseModel extends BaseModel>(
|
|
@@ -28,160 +49,290 @@ const ModelTable: <TBaseModel extends BaseModel>(
|
|
|
28
49
|
const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI;
|
|
29
50
|
const model: TBaseModel = new props.modelType();
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{...props}
|
|
34
|
-
callbacks={{
|
|
35
|
-
getJSONFromModel: (item: TBaseModel): JSONObject => {
|
|
36
|
-
return BaseModel.toJSONObject(item, props.modelType);
|
|
37
|
-
},
|
|
52
|
+
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
|
53
|
+
const [importRefreshCounter, setImportRefreshCounter] = useState<number>(0);
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
const { id, data } = args;
|
|
55
|
+
type GetExportBulkActionFunction = () => BulkActionButtonSchema<TBaseModel>;
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
const getExportBulkAction: GetExportBulkActionFunction =
|
|
58
|
+
(): BulkActionButtonSchema<TBaseModel> => {
|
|
59
|
+
return {
|
|
60
|
+
title: "Export JSON",
|
|
61
|
+
buttonStyleType: ButtonStyleType.NORMAL,
|
|
62
|
+
icon: IconProp.Download,
|
|
63
|
+
onClick: async ({
|
|
64
|
+
items,
|
|
65
|
+
onProgressInfo,
|
|
66
|
+
onBulkActionStart,
|
|
67
|
+
onBulkActionEnd,
|
|
68
|
+
}: BulkActionOnClickProps<TBaseModel>) => {
|
|
69
|
+
onBulkActionStart();
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
const inProgressItems: Array<TBaseModel> = [...items];
|
|
72
|
+
const successItems: Array<TBaseModel> = [];
|
|
73
|
+
const failedItems: Array<BulkActionFailed<TBaseModel>> = [];
|
|
74
|
+
const itemsToExport: Array<TBaseModel> = [];
|
|
52
75
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
query: Query<TBaseModel>;
|
|
56
|
-
groupBy?: GroupBy<TBaseModel> | undefined;
|
|
57
|
-
limit: number;
|
|
58
|
-
skip: number;
|
|
59
|
-
sort: Sort<TBaseModel>;
|
|
60
|
-
select: Select<TBaseModel>;
|
|
61
|
-
requestOptions?: RequestOptions | undefined;
|
|
62
|
-
}) => {
|
|
63
|
-
return await modelAPI.getList<TBaseModel>({
|
|
64
|
-
modelType: data.modelType as { new (): TBaseModel },
|
|
65
|
-
query: data.query,
|
|
66
|
-
limit: data.limit,
|
|
67
|
-
groupBy: data.groupBy,
|
|
68
|
-
skip: data.skip,
|
|
69
|
-
sort: data.sort,
|
|
70
|
-
select: data.select,
|
|
71
|
-
requestOptions: data.requestOptions,
|
|
72
|
-
});
|
|
73
|
-
},
|
|
76
|
+
for (const item of items) {
|
|
77
|
+
inProgressItems.splice(inProgressItems.indexOf(item), 1);
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
try {
|
|
80
|
+
if (!item.id) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${model.singularName || "Item"} id not found.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fetchedItem: TBaseModel =
|
|
87
|
+
await ModelImportExportUtil.fetchItemForExport<TBaseModel>({
|
|
88
|
+
modelType: props.modelType,
|
|
89
|
+
modelId: item.id,
|
|
90
|
+
modelAPI: modelAPI,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
itemsToExport.push(fetchedItem);
|
|
94
|
+
successItems.push(item);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
failedItems.push({
|
|
97
|
+
item: item,
|
|
98
|
+
failedMessage: API.getFriendlyMessage(err),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onProgressInfo({
|
|
103
|
+
totalItems: items,
|
|
104
|
+
inProgressItems: [...inProgressItems],
|
|
105
|
+
successItems: [...successItems],
|
|
106
|
+
failed: [...failedItems],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
79
109
|
|
|
80
|
-
if (
|
|
81
|
-
(
|
|
110
|
+
if (itemsToExport.length > 0) {
|
|
111
|
+
ModelImportExportUtil.downloadExportFile({
|
|
112
|
+
modelType: props.modelType,
|
|
113
|
+
items: itemsToExport,
|
|
114
|
+
});
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
|
|
117
|
+
onBulkActionEnd();
|
|
85
118
|
},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
86
121
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
122
|
+
let bulkActions: BulkActionProps<TBaseModel> | undefined = props.bulkActions;
|
|
123
|
+
let cardProps: CardComponentProps | undefined = props.cardProps;
|
|
124
|
+
let refreshToggle: string | undefined = props.refreshToggle;
|
|
125
|
+
|
|
126
|
+
if (props.enableJsonImportExport) {
|
|
127
|
+
bulkActions = {
|
|
128
|
+
...(props.bulkActions || {}),
|
|
129
|
+
buttons: [...(props.bulkActions?.buttons || []), getExportBulkAction()],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const permissions: Array<Permission> | null =
|
|
133
|
+
PermissionUtil.getAllPermissions();
|
|
134
|
+
|
|
135
|
+
const hasPermissionToCreate: boolean = permissions
|
|
136
|
+
? model.hasCreatePermissions(permissions) || User.isMasterAdmin()
|
|
137
|
+
: false;
|
|
138
|
+
|
|
139
|
+
if (hasPermissionToCreate) {
|
|
140
|
+
cardProps = {
|
|
141
|
+
...(props.cardProps || {}),
|
|
142
|
+
buttons: [
|
|
143
|
+
...(props.cardProps?.buttons || []),
|
|
144
|
+
{
|
|
145
|
+
title: "Import JSON",
|
|
146
|
+
buttonStyle: ButtonStyleType.OUTLINE,
|
|
147
|
+
icon: IconProp.Upload,
|
|
148
|
+
onClick: () => {
|
|
149
|
+
setShowImportModal(true);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
refreshToggle = `${props.refreshToggle || ""}-import-${importRefreshCounter}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
{showImportModal && (
|
|
162
|
+
<ImportModelsModal<TBaseModel>
|
|
163
|
+
modelType={props.modelType as { new (): TBaseModel }}
|
|
164
|
+
modelAPI={modelAPI}
|
|
165
|
+
onClose={() => {
|
|
166
|
+
setShowImportModal(false);
|
|
167
|
+
}}
|
|
168
|
+
onImportComplete={(result: ImportResult) => {
|
|
169
|
+
if (result.successCount > 0) {
|
|
170
|
+
setImportRefreshCounter((counter: number) => {
|
|
171
|
+
return counter + 1;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
<BaseModelTable
|
|
178
|
+
{...props}
|
|
179
|
+
bulkActions={bulkActions}
|
|
180
|
+
cardProps={cardProps}
|
|
181
|
+
refreshToggle={refreshToggle}
|
|
182
|
+
callbacks={{
|
|
183
|
+
getJSONFromModel: (item: TBaseModel): JSONObject => {
|
|
184
|
+
return BaseModel.toJSONObject(item, props.modelType);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
updateById: async (args: { id: ObjectID; data: JSONObject }) => {
|
|
188
|
+
const { id, data } = args;
|
|
189
|
+
|
|
190
|
+
await modelAPI.updateById({
|
|
191
|
+
modelType: props.modelType,
|
|
192
|
+
id: new ObjectID(id),
|
|
193
|
+
data: data,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
toJSONArray: (items: TBaseModel[]): JSONObject[] => {
|
|
198
|
+
return BaseModel.toJSONObjectArray(items, props.modelType);
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
getList: async (data: {
|
|
202
|
+
modelType: DatabaseBaseModelType | AnalyticsBaseModelType;
|
|
203
|
+
query: Query<TBaseModel>;
|
|
204
|
+
groupBy?: GroupBy<TBaseModel> | undefined;
|
|
205
|
+
limit: number;
|
|
206
|
+
skip: number;
|
|
207
|
+
sort: Sort<TBaseModel>;
|
|
208
|
+
select: Select<TBaseModel>;
|
|
209
|
+
requestOptions?: RequestOptions | undefined;
|
|
210
|
+
}) => {
|
|
211
|
+
return await modelAPI.getList<TBaseModel>({
|
|
212
|
+
modelType: data.modelType as { new (): TBaseModel },
|
|
213
|
+
query: data.query,
|
|
214
|
+
limit: data.limit,
|
|
215
|
+
groupBy: data.groupBy,
|
|
216
|
+
skip: data.skip,
|
|
217
|
+
sort: data.sort,
|
|
218
|
+
select: data.select,
|
|
219
|
+
requestOptions: data.requestOptions,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
addSlugToSelect: (select: Select<TBaseModel>): Select<TBaseModel> => {
|
|
224
|
+
const slugifyColumn: string | null = (
|
|
225
|
+
model as BaseModel
|
|
226
|
+
).getSlugifyColumn();
|
|
227
|
+
|
|
228
|
+
if (slugifyColumn) {
|
|
229
|
+
(select as Dictionary<boolean>)[slugifyColumn] = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return select;
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
showCreateEditModal: (data: {
|
|
236
|
+
modalType: ModalType;
|
|
237
|
+
modelIdToEdit?: ObjectID | undefined;
|
|
238
|
+
onBeforeCreate?:
|
|
239
|
+
| ((
|
|
240
|
+
item: TBaseModel,
|
|
241
|
+
miscDataProps: JSONObject,
|
|
242
|
+
) => Promise<TBaseModel>)
|
|
243
|
+
| undefined;
|
|
244
|
+
onSuccess?: ((item: TBaseModel) => void) | undefined;
|
|
245
|
+
onClose?: (() => void) | undefined;
|
|
246
|
+
}): ReactElement => {
|
|
247
|
+
const {
|
|
248
|
+
modalType,
|
|
249
|
+
modelIdToEdit,
|
|
250
|
+
onBeforeCreate,
|
|
251
|
+
onSuccess,
|
|
252
|
+
onClose,
|
|
253
|
+
} = data;
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<ModelFormModal<TBaseModel>
|
|
257
|
+
modelAPI={props.modelAPI}
|
|
258
|
+
title={
|
|
163
259
|
modalType === ModalType.Create
|
|
164
|
-
?
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
260
|
+
? `${props.createVerb || "Create"} New ${
|
|
261
|
+
props.singularName || model.singularName
|
|
262
|
+
}`
|
|
263
|
+
: `Edit ${props.singularName || model.singularName}`
|
|
264
|
+
}
|
|
265
|
+
formRef={props.createEditFromRef}
|
|
266
|
+
modalWidth={props.createEditModalWidth}
|
|
267
|
+
name={
|
|
268
|
+
modalType === ModalType.Create
|
|
269
|
+
? `${props.name} > ${props.createVerb || "Create"} New ${
|
|
270
|
+
props.singularName || model.singularName
|
|
271
|
+
}`
|
|
272
|
+
: `${props.name} > Edit ${
|
|
273
|
+
props.singularName || model.singularName
|
|
274
|
+
}`
|
|
275
|
+
}
|
|
276
|
+
initialValues={
|
|
277
|
+
modalType === ModalType.Create
|
|
278
|
+
? props.createInitialValues
|
|
279
|
+
: undefined
|
|
280
|
+
}
|
|
281
|
+
onClose={onClose}
|
|
282
|
+
submitButtonText={
|
|
283
|
+
modalType === ModalType.Create
|
|
284
|
+
? `${props.createVerb || "Create"} ${
|
|
285
|
+
props.singularName || model.singularName
|
|
286
|
+
}`
|
|
287
|
+
: `Save Changes`
|
|
288
|
+
}
|
|
289
|
+
onSuccess={onSuccess}
|
|
290
|
+
onBeforeCreate={onBeforeCreate}
|
|
291
|
+
modelType={props.modelType}
|
|
292
|
+
formProps={{
|
|
293
|
+
summary: props.formSummary,
|
|
294
|
+
name: `create-${props.modelType.name}-from`,
|
|
295
|
+
modelType: props.modelType,
|
|
296
|
+
id: `create-${props.modelType.name}-from`,
|
|
297
|
+
fields:
|
|
298
|
+
props.formFields?.filter(
|
|
299
|
+
(field: ModelField<TBaseModel>) => {
|
|
300
|
+
// If the field has doNotShowWhenEditing set to true, then don't show it when editing
|
|
171
301
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
302
|
+
if (modelIdToEdit) {
|
|
303
|
+
return !field.doNotShowWhenEditing;
|
|
304
|
+
}
|
|
175
305
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
306
|
+
// If the field has doNotShowWhenCreating set to true, then don't show it when creating
|
|
307
|
+
|
|
308
|
+
return !field.doNotShowWhenCreating;
|
|
309
|
+
},
|
|
310
|
+
) || [],
|
|
311
|
+
steps: props.formSteps || [],
|
|
312
|
+
formType:
|
|
313
|
+
modalType === ModalType.Create
|
|
314
|
+
? FormType.Create
|
|
315
|
+
: FormType.Update,
|
|
316
|
+
}}
|
|
317
|
+
modelIdToEdit={modelIdToEdit}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
getModelFromJSON: (item: JSONObject): TBaseModel => {
|
|
323
|
+
return BaseModel.fromJSON(item, props.modelType) as TBaseModel;
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
deleteItem: async (item: TBaseModel) => {
|
|
327
|
+
await modelAPI.deleteItem({
|
|
328
|
+
modelType: props.modelType,
|
|
329
|
+
id: item.id as ObjectID,
|
|
330
|
+
requestOptions: props.deleteRequestOptions,
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
}}
|
|
334
|
+
/>
|
|
335
|
+
</>
|
|
185
336
|
);
|
|
186
337
|
};
|
|
187
338
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
2
|
+
import { JSONObject } from "../../Types/JSON";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
5
|
+
import ModelImportExport from "../../Utils/ModelImportExport";
|
|
6
|
+
import API from "./API/API";
|
|
7
|
+
import ModelAPI from "./ModelAPI/ModelAPI";
|
|
8
|
+
|
|
9
|
+
export interface ImportFailure {
|
|
10
|
+
itemName: string;
|
|
11
|
+
errorMessage: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ImportResult {
|
|
15
|
+
successCount: number;
|
|
16
|
+
failures: Array<ImportFailure>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default class ModelImportExportUtil {
|
|
20
|
+
public static async fetchItemForExport<TBaseModel extends BaseModel>(data: {
|
|
21
|
+
modelType: { new (): TBaseModel };
|
|
22
|
+
modelId: ObjectID;
|
|
23
|
+
modelAPI?: typeof ModelAPI | undefined;
|
|
24
|
+
}): Promise<TBaseModel> {
|
|
25
|
+
const modelAPI: typeof ModelAPI = data.modelAPI || ModelAPI;
|
|
26
|
+
|
|
27
|
+
const item: TBaseModel | null = await modelAPI.getItem<TBaseModel>({
|
|
28
|
+
modelType: data.modelType,
|
|
29
|
+
id: data.modelId,
|
|
30
|
+
select: ModelImportExport.getImportExportSelect(data.modelType),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!item || !item.id) {
|
|
34
|
+
const model: BaseModel = new data.modelType();
|
|
35
|
+
throw new BadDataException(
|
|
36
|
+
`${model.singularName || "Item"} not found. It may have been deleted.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return item;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static downloadExportFile<TBaseModel extends BaseModel>(data: {
|
|
44
|
+
modelType: { new (): TBaseModel };
|
|
45
|
+
items: Array<TBaseModel>;
|
|
46
|
+
}): void {
|
|
47
|
+
const model: BaseModel = new data.modelType();
|
|
48
|
+
|
|
49
|
+
const envelope: JSONObject = ModelImportExport.buildExportEnvelope({
|
|
50
|
+
modelType: data.modelType,
|
|
51
|
+
items: data.items,
|
|
52
|
+
exportedAt: new Date(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const resourceName: string = (model.tableName || "resources")
|
|
56
|
+
.replace(/[^a-zA-Z0-9-]/g, "-")
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
|
|
59
|
+
const timestamp: string = new Date()
|
|
60
|
+
.toISOString()
|
|
61
|
+
.replace(/[:.]/g, "-")
|
|
62
|
+
.slice(0, 19);
|
|
63
|
+
|
|
64
|
+
this.downloadJSONFile({
|
|
65
|
+
content: JSON.stringify(envelope, null, 2),
|
|
66
|
+
filename: `${resourceName}-export-${timestamp}.json`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public static downloadJSONFile(data: {
|
|
71
|
+
content: string;
|
|
72
|
+
filename: string;
|
|
73
|
+
}): void {
|
|
74
|
+
const blob: Blob = new Blob([data.content], {
|
|
75
|
+
type: "application/json;charset=utf-8;",
|
|
76
|
+
});
|
|
77
|
+
const url: string = window.URL.createObjectURL(blob);
|
|
78
|
+
const anchor: HTMLAnchorElement = document.createElement("a");
|
|
79
|
+
anchor.href = url;
|
|
80
|
+
anchor.download = data.filename;
|
|
81
|
+
document.body.appendChild(anchor);
|
|
82
|
+
anchor.click();
|
|
83
|
+
document.body.removeChild(anchor);
|
|
84
|
+
window.URL.revokeObjectURL(url);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public static parseImportFileText(data: {
|
|
88
|
+
modelType: { new (): BaseModel };
|
|
89
|
+
fileText: string;
|
|
90
|
+
}): Array<JSONObject> {
|
|
91
|
+
let payload: unknown = null;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
payload = JSON.parse(data.fileText);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new BadDataException(
|
|
97
|
+
"This file is not valid JSON. Please select a JSON export file.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return ModelImportExport.parseImportPayload({
|
|
102
|
+
modelType: data.modelType,
|
|
103
|
+
payload: payload as JSONObject,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Creates each item through the regular create API so all server-side
|
|
109
|
+
* validation, permission checks and hooks apply. When an item fails only
|
|
110
|
+
* because a resource with the same name already exists, it is retried once
|
|
111
|
+
* with an "(Imported)" suffix so re-importing into the same project works.
|
|
112
|
+
*/
|
|
113
|
+
public static async importItems<TBaseModel extends BaseModel>(data: {
|
|
114
|
+
modelType: { new (): TBaseModel };
|
|
115
|
+
itemJsons: Array<JSONObject>;
|
|
116
|
+
modelAPI?: typeof ModelAPI | undefined;
|
|
117
|
+
onProgress?:
|
|
118
|
+
| ((completedCount: number, totalCount: number) => void)
|
|
119
|
+
| undefined;
|
|
120
|
+
}): Promise<ImportResult> {
|
|
121
|
+
const modelAPI: typeof ModelAPI = data.modelAPI || ModelAPI;
|
|
122
|
+
|
|
123
|
+
const result: ImportResult = {
|
|
124
|
+
successCount: 0,
|
|
125
|
+
failures: [],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* The column that holds the human-readable name of the resource - used
|
|
130
|
+
* to label failures and to rename on duplicate-name conflicts. Most
|
|
131
|
+
* models use "name"; template models use "templateName".
|
|
132
|
+
*/
|
|
133
|
+
const displayNameColumn: string | undefined = [
|
|
134
|
+
"name",
|
|
135
|
+
"templateName",
|
|
136
|
+
"title",
|
|
137
|
+
].find((columnName: string) => {
|
|
138
|
+
return new data.modelType().getTableColumns().hasColumn(columnName);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
let completedCount: number = 0;
|
|
142
|
+
|
|
143
|
+
for (const itemJson of data.itemJsons) {
|
|
144
|
+
const itemName: string =
|
|
145
|
+
(displayNameColumn &&
|
|
146
|
+
typeof itemJson[displayNameColumn] === "string" &&
|
|
147
|
+
(itemJson[displayNameColumn] as string)) ||
|
|
148
|
+
`Item ${completedCount + 1}`;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const item: TBaseModel = ModelImportExport.fromImportJSON({
|
|
152
|
+
json: itemJson,
|
|
153
|
+
modelType: data.modelType,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await modelAPI.create<TBaseModel>({
|
|
158
|
+
model: item,
|
|
159
|
+
modelType: data.modelType,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
result.successCount += 1;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const errorMessage: string = API.getFriendlyMessage(err);
|
|
165
|
+
|
|
166
|
+
const canRetryWithNewName: boolean = Boolean(
|
|
167
|
+
errorMessage.toLowerCase().includes("already exists") &&
|
|
168
|
+
displayNameColumn &&
|
|
169
|
+
typeof item.getValue(displayNameColumn) === "string",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (canRetryWithNewName) {
|
|
173
|
+
item.setValue(
|
|
174
|
+
displayNameColumn!,
|
|
175
|
+
`${item.getValue(displayNameColumn!)?.toString()} (Imported)`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await modelAPI.create<TBaseModel>({
|
|
179
|
+
model: item,
|
|
180
|
+
modelType: data.modelType,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
result.successCount += 1;
|
|
184
|
+
} else {
|
|
185
|
+
result.failures.push({
|
|
186
|
+
itemName: itemName,
|
|
187
|
+
errorMessage: errorMessage,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
result.failures.push({
|
|
193
|
+
itemName: itemName,
|
|
194
|
+
errorMessage: API.getFriendlyMessage(err),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
completedCount += 1;
|
|
199
|
+
|
|
200
|
+
if (data.onProgress) {
|
|
201
|
+
data.onProgress(completedCount, data.itemJsons.length);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
}
|