@saltcorn/server 0.9.0-beta.1 → 0.9.0-beta.10

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 CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.0-beta.1",
3
+ "version": "0.9.0-beta.10",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@saltcorn/base-plugin": "0.9.0-beta.1",
10
- "@saltcorn/builder": "0.9.0-beta.1",
11
- "@saltcorn/data": "0.9.0-beta.1",
12
- "@saltcorn/admin-models": "0.9.0-beta.1",
13
- "@saltcorn/filemanager": "0.9.0-beta.1",
14
- "@saltcorn/markup": "0.9.0-beta.1",
15
- "@saltcorn/sbadmin2": "0.9.0-beta.1",
9
+ "@saltcorn/base-plugin": "0.9.0-beta.10",
10
+ "@saltcorn/builder": "0.9.0-beta.10",
11
+ "@saltcorn/data": "0.9.0-beta.10",
12
+ "@saltcorn/admin-models": "0.9.0-beta.10",
13
+ "@saltcorn/filemanager": "0.9.0-beta.10",
14
+ "@saltcorn/markup": "0.9.0-beta.10",
15
+ "@saltcorn/sbadmin2": "0.9.0-beta.10",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -45,6 +45,7 @@
45
45
  "node-fetch": "2.6.9",
46
46
  "node-watch": "^0.7.2",
47
47
  "notp": "2.0.3",
48
+ "npm-registry-fetch": "16.0.0",
48
49
  "passport": "^0.6.0",
49
50
  "passport-custom": "^1.1.1",
50
51
  "passport-http-bearer": "^1.0.1",
@@ -55,7 +56,7 @@
55
56
  "qrcode": "1.5.1",
56
57
  "resize-with-sharp-or-jimp": "0.1.6",
57
58
  "socket.io": "4.6.0",
58
- "systeminformation": "^5.11.12",
59
+ "systeminformation": "^5.21.7",
59
60
  "thirty-two": "1.0.2",
60
61
  "tmp-promise": "^3.0.2",
61
62
  "uuid": "^8.2.0",
@@ -446,3 +446,7 @@ Copyright (c) 2017 Taha Paksu
446
446
  padding: 5px 9px;
447
447
  z-index: 1;
448
448
  }
449
+
450
+ div.builder-config-field {
451
+ margin-top: 0.5rem;
452
+ }
@@ -51,6 +51,7 @@ const nubBy = (prop, xs) => {
51
51
  });
52
52
  };
53
53
  function apply_showif() {
54
+ const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
54
55
  $("[data-show-if]").each(function (ix, element) {
55
56
  var e = $(element);
56
57
  try {
@@ -78,6 +79,15 @@ function apply_showif() {
78
79
  console.error(e);
79
80
  }
80
81
  });
82
+ $("[data-dyn-href]").each(function (ix, element) {
83
+ const e = $(element);
84
+ const rec = get_form_record(e);
85
+ const href = new Function(
86
+ `{${Object.keys(rec).join(",")}}`,
87
+ "return " + e.attr("data-dyn-href")
88
+ )(rec);
89
+ e.attr("href", href);
90
+ });
81
91
  $("[data-calc-options]").each(function (ix, element) {
82
92
  var e = $(element);
83
93
  var data = JSON.parse(decodeURIComponent(e.attr("data-calc-options")));
@@ -263,10 +273,12 @@ function apply_showif() {
263
273
 
264
274
  if (typeof cache[recS] !== "undefined") {
265
275
  e.html(cache[recS]);
276
+ e.prop("data-source-url-current", recS);
266
277
  activate_onchange_coldef();
267
278
  return;
268
279
  }
269
- ajax_post_json(e.attr("data-source-url"), rec, {
280
+
281
+ const cb = {
270
282
  success: (data) => {
271
283
  e.html(data);
272
284
  const cacheNow = e.prop("data-source-url-cache") || {};
@@ -286,7 +298,11 @@ function apply_showif() {
286
298
  });
287
299
  e.html("");
288
300
  },
289
- });
301
+ };
302
+ if (isNode) ajax_post_json(e.attr("data-source-url"), rec, cb);
303
+ else {
304
+ local_post_json(e.attr("data-source-url"), rec, cb);
305
+ }
290
306
  });
291
307
  const locale =
292
308
  navigator.userLanguage ||
@@ -509,6 +525,11 @@ function initialize_page() {
509
525
  });
510
526
 
511
527
  $("form").change(apply_showif);
528
+ // also change if we select same
529
+ $("form select").on("blur", (e) => {
530
+ if (!e || !e.target) return;
531
+ $(e.target).closest("form").trigger("change");
532
+ });
512
533
  apply_showif();
513
534
  apply_showif();
514
535
  $("[data-inline-edit-dest-url]").each(function () {
@@ -530,6 +551,7 @@ function initialize_page() {
530
551
  var ajax = !!$(this).attr("data-inline-edit-ajax");
531
552
  var type = $(this).attr("data-inline-edit-type");
532
553
  var schema = $(this).attr("data-inline-edit-schema");
554
+ var decimalPlaces = $(this).attr("data-inline-edit-decimal-places");
533
555
  if (schema) {
534
556
  schema = JSON.parse(decodeURIComponent(schema));
535
557
  }
@@ -552,6 +574,7 @@ function initialize_page() {
552
574
  type,
553
575
  is_key,
554
576
  schema,
577
+ ...(decimalPlaces ? { decimalPlaces } : {}),
555
578
  })
556
579
  );
557
580
  const doAjaxOptionsFetch = (tblName, target) => {
@@ -604,7 +627,17 @@ function initialize_page() {
604
627
  }
605
628
  <input type="${
606
629
  type === "Integer" || type === "Float" ? "number" : "text"
607
- }" name="${key}" value="${escapeHtml(current)}">
630
+ }" ${
631
+ type === "Float"
632
+ ? `step="${
633
+ decimalPlaces
634
+ ? Math.round(
635
+ Math.pow(10, -decimalPlaces) * Math.pow(10, decimalPlaces)
636
+ ) / Math.pow(10, decimalPlaces)
637
+ : "any"
638
+ }"`
639
+ : ""
640
+ } name="${key}" value="${escapeHtml(current)}">
608
641
  <button type="submit" class="btn btn-sm btn-primary">OK</button>
609
642
  <button onclick="cancel_inline_edit(event, '${opts}')" type="button" class="btn btn-sm btn-danger"><i class="fas fa-times"></i></button>
610
643
  </form>`
@@ -657,10 +690,13 @@ function initialize_page() {
657
690
  setTimeout(() => {
658
691
  codes.forEach((el) => {
659
692
  //console.log($(el).attr("mode"), el);
693
+ if ($(el).hasClass("codemirror-enabled")) return;
694
+
660
695
  const cm = CodeMirror.fromTextArea(el, {
661
696
  lineNumbers: true,
662
697
  mode: $(el).attr("mode"),
663
698
  });
699
+ $(el).addClass("codemirror-enabled");
664
700
  cm.on(
665
701
  "change",
666
702
  $.debounce(() => {
@@ -771,6 +807,11 @@ function inline_submit_success(e, form, opts) {
771
807
  : ""
772
808
  }
773
809
  ${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
810
+ ${
811
+ opts.decimalPlaces
812
+ ? `data-inline-edit-decimal-places="${opts.decimalPlaces}"`
813
+ : ""
814
+ }
774
815
  data-inline-edit-dest-url="${opts.url}">
775
816
  <span class="current">${val}</span>
776
817
  <i class="editicon ${!isNode ? "visible" : ""} fas fa-edit ms-1"></i>
@@ -948,14 +989,25 @@ function emptyAlerts() {
948
989
  $("#toasts-area").html("");
949
990
  }
950
991
 
951
- function press_store_button(clicked) {
992
+ function press_store_button(clicked, keepOld) {
952
993
  let btn = clicked;
953
994
  if ($(clicked).is("form")) btn = $(clicked).find("button[type=submit]");
954
-
995
+ if (keepOld) {
996
+ const oldText = $(btn).html();
997
+ $(btn).data("old-text", oldText);
998
+ }
955
999
  const width = $(btn).width();
956
1000
  $(btn).html('<i class="fas fa-spinner fa-spin"></i>').width(width);
957
1001
  }
958
1002
 
1003
+ function restore_old_button(btnId) {
1004
+ const btn = $(`#${btnId}`);
1005
+ const oldText = $(btn).data("old-text");
1006
+ btn.html(oldText);
1007
+ btn.css({ width: "" });
1008
+ btn.removeData("old-text");
1009
+ }
1010
+
959
1011
  function common_done(res, viewname, isWeb = true) {
960
1012
  const handle = (element, fn) => {
961
1013
  if (Array.isArray(element)) for (const current of element) fn(current);
@@ -994,6 +1046,7 @@ function common_done(res, viewname, isWeb = true) {
994
1046
  if (input.attr("type") === "checkbox")
995
1047
  input.prop("checked", res.set_fields[k]);
996
1048
  else input.val(res.set_fields[k]);
1049
+ input.trigger("set_form_field");
997
1050
  });
998
1051
  }
999
1052
  if (res.goto && !isWeb)
@@ -1011,6 +1064,7 @@ function common_done(res, viewname, isWeb = true) {
1011
1064
  if (
1012
1065
  prev.origin === next.origin &&
1013
1066
  prev.pathname === next.pathname &&
1067
+ prev.searchParams.toString() === next.searchParams.toString() &&
1014
1068
  next.hash !== prev.hash
1015
1069
  )
1016
1070
  location.reload();
@@ -340,6 +340,28 @@ function ajax_modal(url, opts = {}) {
340
340
  $("body").css("overflow", "");
341
341
  });
342
342
  },
343
+ ...(opts.onError
344
+ ? {
345
+ error: opts.onError,
346
+ }
347
+ : {}),
348
+ });
349
+ }
350
+
351
+ function selectVersionError(res, btnId) {
352
+ notifyAlert({
353
+ type: "danger",
354
+ text: res.responseJSON?.error || "unknown error",
355
+ });
356
+ restore_old_button(btnId);
357
+ }
358
+
359
+ function submitWithAjax(e) {
360
+ saveAndContinue(e, (res) => {
361
+ if (res && res.responseJSON && res.responseJSON.url_when_done)
362
+ window.location.href = res.responseJSON.url_when_done;
363
+ if (res && res.responseJSON && res.responseJSON.error)
364
+ notifyAlert({ type: "danger", text: res.responseJSON.error });
343
365
  });
344
366
  }
345
367
 
@@ -368,6 +390,9 @@ function saveAndContinue(e, k) {
368
390
  if (res.notify) {
369
391
  notifyAlert(res.notify);
370
392
  }
393
+ if (res.reload_page) {
394
+ location.reload(); //TODO notify to cookie if reload or goto
395
+ }
371
396
  },
372
397
  error: function (request) {
373
398
  var ct = request.getResponseHeader("content-type") || "";
@@ -388,8 +413,8 @@ function saveAndContinue(e, k) {
388
413
  }
389
414
  ajax_indicate_error(e, request);
390
415
  },
391
- complete: function () {
392
- if (k) k();
416
+ complete: function (res) {
417
+ if (k) k(res);
393
418
  },
394
419
  });
395
420
 
@@ -410,7 +435,8 @@ function updateMatchingRows(e, viewname) {
410
435
  }
411
436
  }
412
437
 
413
- function applyViewConfig(e, url, k) {
438
+ function applyViewConfig(e, url, k, event) {
439
+ if (event && event.target && event.target.id === "myEditor_icon") return;
414
440
  var form = $(e).closest("form");
415
441
  var form_data = form.serializeArray();
416
442
  const cfg = {};
package/routes/actions.js CHANGED
@@ -174,8 +174,9 @@ const triggerForm = async (req, trigger) => {
174
174
  attributes: {
175
175
  explainers: {
176
176
  Often: req.__("Every 5 minutes"),
177
- Never:
178
- req.__("Not scheduled but can be run as an action from a button click"),
177
+ Never: req.__(
178
+ "Not scheduled but can be run as an action from a button click"
179
+ ),
179
180
  },
180
181
  },
181
182
  },
@@ -201,6 +202,7 @@ const triggerForm = async (req, trigger) => {
201
202
  label: req.__("Action"),
202
203
  type: "String",
203
204
  required: true,
205
+ help: { topic: "Actions" },
204
206
  attributes: {
205
207
  calcOptions: ["when_trigger", action_options],
206
208
  },
@@ -402,7 +404,7 @@ router.get(
402
404
  form.values = trigger.configuration;
403
405
  const events = Trigger.when_options;
404
406
  const actions = Trigger.find({
405
- when_trigger: {or: ["API call", "Never"]},
407
+ when_trigger: { or: ["API call", "Never"] },
406
408
  });
407
409
  const tables = (await Table.find({})).map((t) => ({
408
410
  name: t.name,
package/routes/admin.js CHANGED
@@ -285,6 +285,9 @@ router.get(
285
285
  backupForm.values.auto_backup_expire_days = getState().getConfig(
286
286
  "auto_backup_expire_days"
287
287
  );
288
+ backupForm.values.backup_with_event_log = getState().getConfig(
289
+ "backup_with_event_log"
290
+ );
288
291
  //
289
292
  const aSnapshotForm = snapshotForm(req);
290
293
  aSnapshotForm.values.snapshots_enabled =
@@ -721,6 +724,15 @@ const autoBackupForm = (req) =>
721
724
  auto_backup_destination: "Local directory",
722
725
  },
723
726
  },
727
+ {
728
+ type: "Bool",
729
+ label: req.__("Include Event Logs"),
730
+ sublabel: req.__("Backup with event logs"),
731
+ name: "backup_with_event_log",
732
+ showIf: {
733
+ auto_backup_frequency: ["Daily", "Weekly"],
734
+ },
735
+ },
724
736
  ],
725
737
  });
726
738
 
@@ -1545,7 +1557,7 @@ router.get(
1545
1557
  input({
1546
1558
  type: "hidden",
1547
1559
  name: "entryPointType",
1548
- value: "view",
1560
+ value: builderSettings.entryPointType || "view",
1549
1561
  id: "entryPointTypeID",
1550
1562
  }),
1551
1563
  div(
@@ -1578,7 +1590,8 @@ router.get(
1578
1590
  div(
1579
1591
  {
1580
1592
  class: `nav-link ${
1581
- !builderSettings.entryPointType || builderSettings.entryPointType === "view"
1593
+ !builderSettings.entryPointType ||
1594
+ builderSettings.entryPointType === "view"
1582
1595
  ? "active"
1583
1596
  : ""
1584
1597
  }`,
@@ -1613,7 +1626,8 @@ router.get(
1613
1626
  ? "d-none"
1614
1627
  : ""
1615
1628
  }`,
1616
- ...(!builderSettings.entryPointType || builderSettings.entryPointType === "view"
1629
+ ...(!builderSettings.entryPointType ||
1630
+ builderSettings.entryPointType === "view"
1617
1631
  ? { name: "entryPoint" }
1618
1632
  : {}),
1619
1633
  id: "viewInputID",
@@ -1636,7 +1650,8 @@ router.get(
1636
1650
  select(
1637
1651
  {
1638
1652
  class: `form-select ${
1639
- !builderSettings.entryPointType || builderSettings.entryPointType === "view"
1653
+ !builderSettings.entryPointType ||
1654
+ builderSettings.entryPointType === "view"
1640
1655
  ? "d-none"
1641
1656
  : ""
1642
1657
  }`,
package/routes/fields.js CHANGED
@@ -409,7 +409,7 @@ const fieldFlow = (req) =>
409
409
  instance_options[model.name].push(...instances.map((i) => i.name));
410
410
 
411
411
  const outputs = await applyAsync(
412
- model.templateObj.prediction_outputs || [],
412
+ model.templateObj?.prediction_outputs || [], // unit tests can have templateObj undefined
413
413
  { table, configuration: model.configuration }
414
414
  );
415
415
  output_options[model.name] = outputs.map((o) => o.name);
@@ -840,6 +840,11 @@ router.post(
840
840
  const table = Table.findOne({ name: tableName });
841
841
  const role = req.user && req.user.id ? req.user.role_id : 100;
842
842
 
843
+ getState().log(
844
+ 5,
845
+ `Route /fields/show-calculated/${tableName}/${fieldName}/${fieldview} user=${req.user?.id}`
846
+ );
847
+
843
848
  const fields = table.getFields();
844
849
  let row = { ...req.body };
845
850
  if (row && Object.keys(row).length > 0) readState(row, fields);
@@ -1018,6 +1023,13 @@ router.post(
1018
1023
  const { tableName, fieldName, fieldview } = req.params;
1019
1024
  const table = Table.findOne({ name: tableName });
1020
1025
  const fields = table.getFields();
1026
+ const state = getState();
1027
+
1028
+ state.log(
1029
+ 5,
1030
+ `Route /fields/preview/${tableName}/${fieldName}/${fieldview} user=${req.user?.id}`
1031
+ );
1032
+
1021
1033
  let field, row, value;
1022
1034
  if (fieldName.includes(".")) {
1023
1035
  const [refNm, targetNm] = fieldName.split(".");
@@ -1048,9 +1060,9 @@ router.post(
1048
1060
  }
1049
1061
  const fieldviews =
1050
1062
  field.type === "Key"
1051
- ? getState().keyFieldviews
1063
+ ? state.keyFieldviews
1052
1064
  : field.type === "File"
1053
- ? getState().fileviews
1065
+ ? state.fileviews
1054
1066
  : field.type.fieldviews;
1055
1067
  if (!field.type || !fieldviews) {
1056
1068
  res.send("");
package/routes/menu.js CHANGED
@@ -44,7 +44,7 @@ const menuForm = async (req) => {
44
44
  const views = await View.find({}, { orderBy: "name", nocase: true });
45
45
  const pages = await Page.find({}, { orderBy: "name", nocase: true });
46
46
  const roles = await User.get_roles();
47
- const tables = await Table.find({});
47
+ const tables = await Table.find_with_external({});
48
48
  const dynTableOptions = tables.map((t) => t.name);
49
49
  const dynOrderFieldOptions = {},
50
50
  dynSectionFieldOptions = {};
package/routes/packs.js CHANGED
@@ -6,18 +6,19 @@
6
6
 
7
7
  const Router = require("express-promise-router");
8
8
  const { isAdmin, error_catcher } = require("./utils.js");
9
- const { mkTable, renderForm, link, post_btn } = require("@saltcorn/markup");
10
- const { getState } = require("@saltcorn/data/db/state");
9
+ const { renderForm } = require("@saltcorn/markup");
11
10
  const Table = require("@saltcorn/data/models/table");
12
11
  const Form = require("@saltcorn/data/models/form");
13
12
  const View = require("@saltcorn/data/models/view");
14
- const Field = require("@saltcorn/data/models/field");
15
13
  const Plugin = require("@saltcorn/data/models/plugin");
16
14
  const Page = require("@saltcorn/data/models/page");
15
+ const Tag = require("@saltcorn/data/models/tag");
16
+ const EventLog = require("@saltcorn/data/models/eventlog");
17
+ const Model = require("@saltcorn/data/models/model");
18
+ const ModelInstance = require("@saltcorn/data/models/model_instance");
17
19
  const load_plugins = require("../load_plugins");
18
20
 
19
21
  const { is_pack } = require("@saltcorn/data/contracts");
20
- const { contract, is } = require("contractis");
21
22
  const {
22
23
  table_pack,
23
24
  view_pack,
@@ -26,15 +27,20 @@ const {
26
27
  role_pack,
27
28
  library_pack,
28
29
  trigger_pack,
30
+ tag_pack,
31
+ model_pack,
32
+ model_instance_pack,
29
33
  install_pack,
30
34
  fetch_pack_by_name,
31
35
  can_install_pack,
32
36
  uninstall_pack,
37
+ event_log_pack,
33
38
  } = require("@saltcorn/admin-models/models/pack");
34
- const { h5, pre, code, p, text, text_attr } = require("@saltcorn/markup/tags");
39
+ const { pre, code, p, text, text_attr } = require("@saltcorn/markup/tags");
35
40
  const Library = require("@saltcorn/data/models/library");
36
41
  const Trigger = require("@saltcorn/data/models/trigger");
37
42
  const Role = require("@saltcorn/data/models/role");
43
+ const fs = require("fs");
38
44
 
39
45
  /**
40
46
  * @type {object}
@@ -98,6 +104,52 @@ router.get(
98
104
  name: `role.${l.role}`,
99
105
  type: "Bool",
100
106
  }));
107
+ const tags = await Tag.find({});
108
+ const tagFields = tags.map((t) => ({
109
+ label: `${t.name} tag`,
110
+ name: `tag.${t.name}`,
111
+ type: "Bool",
112
+ }));
113
+ const models = await Model.find({});
114
+ const modelFields = models.map((m) => {
115
+ const modelTbl = Table.findOne({ id: m.table_id });
116
+ return {
117
+ label: `${m.name} model, table: ${
118
+ modelTbl.name || req.__("Table not found")
119
+ }`,
120
+ name: `model.${m.name}.${modelTbl.name}`,
121
+ type: "Bool",
122
+ };
123
+ });
124
+ const modelInstances = await ModelInstance.find({});
125
+ const modelInstanceFields = (
126
+ await Promise.all(
127
+ modelInstances.map(async (instance) => {
128
+ const model = await Model.findOne({ id: instance.model_id });
129
+ if (!model) {
130
+ req.flash(
131
+ "warning",
132
+ req.__(`Model with '${instance.model_id}' not found`)
133
+ );
134
+ return null;
135
+ }
136
+ const mTable = await Table.findOne({ id: model.table_id });
137
+ if (!mTable) {
138
+ req.flash(
139
+ "warning",
140
+ req.__(`Table of model '${model.name}' not found`)
141
+ );
142
+ return null;
143
+ }
144
+ return {
145
+ label: `${instance.name} model instance, model: ${model.name}, table: ${mTable.name}`,
146
+ name: `model_instance.${instance.name}.${model.name}.${mTable.name}`,
147
+ type: "Bool",
148
+ };
149
+ })
150
+ )
151
+ ).filter((f) => f);
152
+
101
153
  const form = new Form({
102
154
  action: "/packs/create",
103
155
  fields: [
@@ -108,6 +160,14 @@ router.get(
108
160
  ...trigFields,
109
161
  ...roleFields,
110
162
  ...libFields,
163
+ ...tagFields,
164
+ ...modelFields,
165
+ ...modelInstanceFields,
166
+ {
167
+ name: "with_event_logs",
168
+ label: req.__("Include Event Logs"),
169
+ type: "Bool",
170
+ },
111
171
  ],
112
172
  });
113
173
  res.sendWrap(req.__(`Create Pack`), {
@@ -140,7 +200,7 @@ router.post(
140
200
  "/create",
141
201
  isAdmin,
142
202
  error_catcher(async (req, res) => {
143
- var pack = {
203
+ const pack = {
144
204
  tables: [],
145
205
  views: [],
146
206
  plugins: [],
@@ -148,9 +208,13 @@ router.post(
148
208
  roles: [],
149
209
  library: [],
150
210
  triggers: [],
211
+ tags: [],
212
+ models: [],
213
+ model_instances: [],
214
+ event_logs: [],
151
215
  };
152
216
  for (const k of Object.keys(req.body)) {
153
- const [type, name] = k.split(".");
217
+ const [type, name, ...rest] = k.split(".");
154
218
  switch (type) {
155
219
  case "table":
156
220
  pack.tables.push(await table_pack(name));
@@ -173,7 +237,32 @@ router.post(
173
237
  case "trigger":
174
238
  pack.triggers.push(await trigger_pack(name));
175
239
  break;
176
-
240
+ case "tag":
241
+ pack.tags.push(await tag_pack(name));
242
+ break;
243
+ case "model": {
244
+ const table = rest[0];
245
+ if (!table) throw new Error(`Table for model '${name}' not found`);
246
+ pack.models.push(await model_pack(name, table));
247
+ break;
248
+ }
249
+ case "model_instance": {
250
+ const model = rest[0];
251
+ if (!model)
252
+ throw new Error(`Model of Model Instance '${name}' not found`);
253
+ const table = rest[1];
254
+ if (!table) throw new Error(`Table of Model '${model}' not found`);
255
+ pack.model_instances.push(
256
+ await model_instance_pack(name, model, table)
257
+ );
258
+ break;
259
+ }
260
+ case "with_event_logs":
261
+ const logs = await EventLog.find({});
262
+ pack.event_logs = await Promise.all(
263
+ logs.map(async (l) => await event_log_pack(l))
264
+ );
265
+ break;
177
266
  default:
178
267
  break;
179
268
  }
@@ -217,11 +306,33 @@ const install_pack_form = (req) =>
217
306
  action: "/packs/install",
218
307
  submitLabel: req.__("Install"),
219
308
  fields: [
309
+ {
310
+ name: "source",
311
+ label: req.__("Source"),
312
+ type: "String",
313
+ attributes: {
314
+ options: [
315
+ { label: "from text", name: "from_text" },
316
+ { label: "from file", name: "from_file" },
317
+ ],
318
+ },
319
+ default: "from_text",
320
+ required: true,
321
+ },
220
322
  {
221
323
  name: "pack",
222
324
  label: req.__("Pack"),
223
325
  type: "String",
224
326
  fieldview: "textarea",
327
+ showIf: { source: "from_text" },
328
+ },
329
+ {
330
+ name: "pack_file",
331
+ label: req.__("Pack file"),
332
+ class: "form-control",
333
+ type: "File",
334
+ sublabel: req.__("Upload a pack file"),
335
+ showIf: { source: "from_file" },
225
336
  },
226
337
  ],
227
338
  });
@@ -267,8 +378,22 @@ router.post(
267
378
  isAdmin,
268
379
  error_catcher(async (req, res) => {
269
380
  var pack, error;
381
+ const source = req.body.source || "from_text";
270
382
  try {
271
- pack = JSON.parse(req.body.pack);
383
+ switch (source) {
384
+ case "from_text":
385
+ pack = JSON.parse(req.body.pack);
386
+ break;
387
+ case "from_file":
388
+ if (req.files?.pack_file?.tempFilePath)
389
+ pack = JSON.parse(
390
+ fs.readFileSync(req.files?.pack_file?.tempFilePath)
391
+ );
392
+ else throw new Error(req.__("No file uploaded"));
393
+ break;
394
+ default:
395
+ throw new Error(req.__("Invalid source"));
396
+ }
272
397
  } catch (e) {
273
398
  error = e.message;
274
399
  }