@saltcorn/server 0.7.3-beta.7 → 0.7.4-beta.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/routes/admin.js CHANGED
@@ -18,7 +18,13 @@ const { spawn } = require("child_process");
18
18
  const User = require("@saltcorn/data/models/user");
19
19
  const path = require("path");
20
20
  const { getAllTenants } = require("@saltcorn/admin-models/models/tenant");
21
- const { post_btn, renderForm, mkTable, link } = require("@saltcorn/markup");
21
+ const {
22
+ post_btn,
23
+ renderForm,
24
+ mkTable,
25
+ link,
26
+ localeDateTime,
27
+ } = require("@saltcorn/markup");
22
28
  const {
23
29
  div,
24
30
  a,
@@ -42,10 +48,11 @@ const {
42
48
  select,
43
49
  option,
44
50
  fieldset,
45
- legend,
46
51
  ul,
47
52
  li,
48
53
  ol,
54
+ script,
55
+ domReady,
49
56
  } = require("@saltcorn/markup/tags");
50
57
  const db = require("@saltcorn/data/db");
51
58
  const {
@@ -61,6 +68,7 @@ const {
61
68
  restore,
62
69
  auto_backup_now,
63
70
  } = require("@saltcorn/admin-models/models/backup");
71
+ const Snapshot = require("@saltcorn/admin-models/models/snapshot");
64
72
  const {
65
73
  runConfigurationCheck,
66
74
  } = require("@saltcorn/admin-models/models/config-check");
@@ -87,6 +95,7 @@ const moment = require("moment");
87
95
  const View = require("@saltcorn/data/models/view");
88
96
  const { getConfigFile } = require("@saltcorn/data/db/connect");
89
97
  const os = require("os");
98
+ const Page = require("@saltcorn/data/models/page");
90
99
 
91
100
  /**
92
101
  * @type {object}
@@ -116,6 +125,7 @@ const site_id_form = (req) =>
116
125
  "page_custom_html",
117
126
  "development_mode",
118
127
  "log_sql",
128
+ "log_level",
119
129
  "plugins_store_endpoint",
120
130
  "packs_store_endpoint",
121
131
  ...(getConfigFile() ? ["multitenancy_enabled"] : []),
@@ -141,14 +151,10 @@ const email_form = async (req) => {
141
151
  ],
142
152
  action: "/admin/email",
143
153
  });
144
- form.submitButtonClass = "btn-outline-primary";
145
- form.submitLabel = req.__("Save");
146
- form.onChange =
147
- "remove_outline(this);$('#testemail').attr('href','#').removeClass('btn-primary').addClass('btn-outline-primary')";
148
154
  return form;
149
155
  };
150
156
 
151
- const app_files_table = (file, req) =>
157
+ const app_files_table = (files, req) =>
152
158
  mkTable(
153
159
  [
154
160
  {
@@ -162,7 +168,7 @@ const app_files_table = (file, req) =>
162
168
  key: (r) => link(`/files/download/${r.id}`, req.__("Download")),
163
169
  },
164
170
  ],
165
- [file]
171
+ files
166
172
  );
167
173
 
168
174
  /**
@@ -215,8 +221,10 @@ router.post(
215
221
  flash_restart_if_required(form, req);
216
222
  await save_config_from_form(form);
217
223
 
218
- req.flash("success", req.__("Site identity settings updated"));
219
- res.redirect("/admin");
224
+ if (!req.xhr) {
225
+ req.flash("success", req.__("Site identity settings updated"));
226
+ res.redirect("/admin");
227
+ } else res.json({ success: "ok" });
220
228
  }
221
229
  })
222
230
  );
@@ -309,7 +317,8 @@ router.post(
309
317
  } else {
310
318
  await save_config_from_form(form);
311
319
  req.flash("success", req.__("Email settings updated"));
312
- res.redirect("/admin/email");
320
+ if (!req.xhr) res.redirect("/admin/email");
321
+ else res.json({ success: "ok" });
313
322
  }
314
323
  })
315
324
  );
@@ -336,6 +345,9 @@ router.get(
336
345
  backupForm.values.auto_backup_expire_days = getState().getConfig(
337
346
  "auto_backup_expire_days"
338
347
  );
348
+ const aSnapshotForm = snapshotForm(req);
349
+ aSnapshotForm.values.snapshots_enabled =
350
+ getState().getConfig("snapshots_enabled");
339
351
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
340
352
 
341
353
  send_admin_page({
@@ -353,7 +365,7 @@ router.get(
353
365
  post_btn(
354
366
  "/admin/backup",
355
367
  i({ class: "fas fa-download me-2" }) +
356
- req.__("Download a backup"),
368
+ req.__("Download a backup"),
357
369
  req.csrfToken(),
358
370
  {
359
371
  btnClass: "btn-outline-primary",
@@ -372,17 +384,38 @@ router.get(
372
384
  },
373
385
  isRoot
374
386
  ? {
375
- type: "card",
376
- title: req.__("Automated backup"),
377
- contents: div(
378
- renderForm(backupForm, req.csrfToken()),
379
- a(
380
- { href: "/admin/auto-backup-list" },
381
- "Restore/download automated backups »"
382
- )
387
+ type: "card",
388
+ title: req.__("Automated backup"),
389
+ contents: div(
390
+ renderForm(backupForm, req.csrfToken()),
391
+ a(
392
+ { href: "/admin/auto-backup-list" },
393
+ "Restore/download automated backups »"
383
394
  ),
384
- }
395
+ script(
396
+ domReady(
397
+ `$('#btnBackupNow').prop('disabled', $('#inputauto_backup_frequency').val()==='Never');`
398
+ )
399
+ )
400
+ ),
401
+ }
385
402
  : { type: "blank", contents: "" },
403
+ {
404
+ type: "card",
405
+ title: req.__("Snapshots"),
406
+ contents: div(
407
+ p(
408
+ i(
409
+ "Snapshots store your application structure and definition, without the table data. Individual views and pages can be restored from snapshots from the <a href='/viewedit'>view</a> or <a href='/pageedit'>pages</a> overviews (\"Restore\" from individual page or view dropdowns)."
410
+ )
411
+ ),
412
+ renderForm(aSnapshotForm, req.csrfToken()),
413
+ a(
414
+ { href: "/admin/snapshot-list" },
415
+ "List/download snapshots &raquo;"
416
+ )
417
+ ),
418
+ },
386
419
  ],
387
420
  },
388
421
  });
@@ -458,6 +491,103 @@ router.get(
458
491
  })
459
492
  );
460
493
 
494
+ router.get(
495
+ "/snapshot-list",
496
+ isAdmin,
497
+ error_catcher(async (req, res) => {
498
+ const snaps = await Snapshot.find();
499
+ send_admin_page({
500
+ res,
501
+ req,
502
+ active_sub: "Backup",
503
+ contents: {
504
+ above: [
505
+ {
506
+ type: "card",
507
+ title: req.__("Download snapshots"),
508
+ contents: div(
509
+ ul(
510
+ snaps.map((snap) =>
511
+ li(
512
+ a(
513
+ {
514
+ href: `/admin/snapshot-download/${encodeURIComponent(
515
+ snap.id
516
+ )}`,
517
+ target: "_blank",
518
+ },
519
+ `${localeDateTime(snap.created)} (${moment(
520
+ snap.created
521
+ ).fromNow()})`
522
+ )
523
+ )
524
+ )
525
+ )
526
+ ),
527
+ },
528
+ ],
529
+ },
530
+ });
531
+ })
532
+ );
533
+
534
+ router.get(
535
+ "/snapshot-download/:id",
536
+ isAdmin,
537
+ error_catcher(async (req, res) => {
538
+ const { id } = req.params;
539
+ const snap = await Snapshot.findOne({ id });
540
+ res.send(snap.pack);
541
+ })
542
+ );
543
+
544
+ router.get(
545
+ "/snapshot-restore/:type/:name",
546
+ isAdmin,
547
+ error_catcher(async (req, res) => {
548
+ const { type, name } = req.params;
549
+ const snaps = await Snapshot.entity_history(type, name);
550
+ res.send(
551
+ mkTable(
552
+ [
553
+ {
554
+ label: "When",
555
+ key: (r) =>
556
+ `${localeDateTime(r.created)} (${moment(r.created).fromNow()})`,
557
+ },
558
+
559
+ {
560
+ label: req.__("Restore"),
561
+ key: (r) =>
562
+ post_btn(
563
+ `/admin/snapshot-restore/${type}/${name}/${r.id}`,
564
+ req.__("Restore"),
565
+ req.csrfToken()
566
+ ),
567
+ },
568
+ ],
569
+ snaps
570
+ )
571
+ );
572
+ })
573
+ );
574
+
575
+ router.post(
576
+ "/snapshot-restore/:type/:name/:id",
577
+ isAdmin,
578
+ error_catcher(async (req, res) => {
579
+ const { type, name, id } = req.params;
580
+ const snap = await Snapshot.findOne({ id });
581
+ await snap.restore_entity(type, name);
582
+ req.flash(
583
+ "success",
584
+ `${type} ${name} restored to snapshot saved ${moment(
585
+ snap.created
586
+ ).fromNow()}`
587
+ );
588
+ res.redirect(`/${type}edit`);
589
+ })
590
+ );
461
591
  router.get(
462
592
  "/auto-backup-download/:filename",
463
593
  isAdmin,
@@ -484,9 +614,8 @@ router.get(
484
614
  const autoBackupForm = (req) =>
485
615
  new Form({
486
616
  action: "/admin/set-auto-backup",
487
- submitButtonClass: "btn-outline-primary",
488
- onChange: "remove_outline(this)",
489
- submitLabel: "Save settings",
617
+ onChange: `saveAndContinue(this);$('#btnBackupNow').prop('disabled', $('#inputauto_backup_frequency').val()==='Never');`,
618
+ noSubmitButton: true,
490
619
  additionalButtons: [
491
620
  {
492
621
  label: "Backup now",
@@ -535,6 +664,43 @@ const autoBackupForm = (req) =>
535
664
  ],
536
665
  });
537
666
 
667
+ const snapshotForm = (req) =>
668
+ new Form({
669
+ action: "/admin/set-snapshot",
670
+ onChange: `saveAndContinue(this);`,
671
+ noSubmitButton: true,
672
+ additionalButtons: [
673
+ {
674
+ label: "Snapshot now",
675
+ id: "btnSnapNow",
676
+ class: "btn btn-outline-secondary",
677
+ onclick: "ajax_post('/admin/snapshot-now')",
678
+ },
679
+ ],
680
+ fields: [
681
+ {
682
+ type: "Bool",
683
+ label: req.__("Periodic snapshots enabled"),
684
+ name: "snapshots_enabled",
685
+ sublabel: req.__(
686
+ "Snapshot will be made every hour if there are changes"
687
+ ),
688
+ },
689
+ ],
690
+ });
691
+ router.post(
692
+ "/set-snapshot",
693
+ isAdmin,
694
+ error_catcher(async (req, res) => {
695
+ const form = await snapshotForm(req);
696
+ form.validate(req.body);
697
+
698
+ await save_config_from_form(form);
699
+ req.flash("success", req.__("Snapshot settings updated"));
700
+ if (!req.xhr) res.redirect("/admin/backup");
701
+ else res.json({ success: "ok" });
702
+ })
703
+ );
538
704
  router.post(
539
705
  "/set-auto-backup",
540
706
  isAdmin,
@@ -555,7 +721,8 @@ router.post(
555
721
  } else {
556
722
  await save_config_from_form(form);
557
723
  req.flash("success", req.__("Backup settings updated"));
558
- res.redirect("/admin/backup");
724
+ if (!req.xhr) res.redirect("/admin/backup");
725
+ else res.json({ success: "ok" });
559
726
  }
560
727
  })
561
728
  );
@@ -573,6 +740,22 @@ router.post(
573
740
  })
574
741
  );
575
742
 
743
+ router.post(
744
+ "/snapshot-now",
745
+ isAdmin,
746
+ error_catcher(async (req, res) => {
747
+ try {
748
+ const taken = await Snapshot.take_if_changed();
749
+ if (taken) req.flash("success", req.__("Snapshot successful"));
750
+ else
751
+ req.flash("success", req.__("No changes detected, snapshot skipped"));
752
+ } catch (e) {
753
+ req.flash("error", e.message);
754
+ }
755
+ res.json({ reload_page: true });
756
+ })
757
+ );
758
+
576
759
  /**
577
760
  * @name get/system
578
761
  * @function
@@ -648,47 +831,47 @@ router.get(
648
831
  th(req.__("Saltcorn version")),
649
832
  td(
650
833
  packagejson.version +
651
- (isRoot && can_update
652
- ? post_btn(
653
- "/admin/upgrade",
654
- req.__("Upgrade"),
655
- req.csrfToken(),
656
- {
657
- btnClass: "btn-primary btn-sm",
658
- formClass: "d-inline",
659
- }
660
- )
661
- : isRoot && is_latest
834
+ (isRoot && can_update
835
+ ? post_btn(
836
+ "/admin/upgrade",
837
+ req.__("Upgrade"),
838
+ req.csrfToken(),
839
+ {
840
+ btnClass: "btn-primary btn-sm",
841
+ formClass: "d-inline",
842
+ }
843
+ )
844
+ : isRoot && is_latest
662
845
  ? span(
663
- { class: "badge bg-primary ms-2" },
664
- req.__("Latest")
665
- ) +
666
- post_btn(
667
- "/admin/check-for-upgrade",
668
- req.__("Check for updates"),
669
- req.csrfToken(),
670
- {
671
- btnClass: "btn-primary btn-sm px-1 py-0",
672
- formClass: "d-inline",
673
- }
674
- )
846
+ { class: "badge bg-primary ms-2" },
847
+ req.__("Latest")
848
+ ) +
849
+ post_btn(
850
+ "/admin/check-for-upgrade",
851
+ req.__("Check for updates"),
852
+ req.csrfToken(),
853
+ {
854
+ btnClass: "btn-primary btn-sm px-1 py-0",
855
+ formClass: "d-inline",
856
+ }
857
+ )
675
858
  : "")
676
859
  )
677
860
  ),
678
861
  git_commit &&
679
- tr(
680
- th(req.__("git commit")),
681
- td(
682
- a(
683
- {
684
- href:
685
- "https://github.com/saltcorn/saltcorn/commit/" +
686
- git_commit,
687
- },
688
- git_commit.substring(0, 6)
689
- )
862
+ tr(
863
+ th(req.__("git commit")),
864
+ td(
865
+ a(
866
+ {
867
+ href:
868
+ "https://github.com/saltcorn/saltcorn/commit/" +
869
+ git_commit,
870
+ },
871
+ git_commit.substring(0, 6)
690
872
  )
691
- ),
873
+ )
874
+ ),
692
875
  tr(th(req.__("Node.js version")), td(process.version)),
693
876
  tr(
694
877
  th(req.__("Database")),
@@ -798,7 +981,7 @@ router.post(
798
981
  res.attachment(fileName);
799
982
  const file = fs.createReadStream(fileName);
800
983
  file.on("end", function () {
801
- fs.unlink(fileName, function () {});
984
+ fs.unlink(fileName, function () { });
802
985
  });
803
986
  file.pipe(res);
804
987
  })
@@ -821,7 +1004,7 @@ router.post(
821
1004
  );
822
1005
  if (err) req.flash("error", err);
823
1006
  else req.flash("success", req.__("Successfully restored backup"));
824
- fs.unlink(newPath, function () {});
1007
+ fs.unlink(newPath, function () { });
825
1008
  res.redirect(`/admin`);
826
1009
  })
827
1010
  );
@@ -896,7 +1079,7 @@ const clearAllForm = (req) =>
896
1079
  {
897
1080
  type: "Bool",
898
1081
  name: "plugins",
899
- label: req.__("Plugins"),
1082
+ label: req.__("Modules"),
900
1083
  default: true,
901
1084
  },
902
1085
  ],
@@ -966,8 +1149,8 @@ router.post(
966
1149
  req.__(
967
1150
  "LetsEncrypt SSL enabled. Restart for changes to take effect."
968
1151
  ) +
969
- " " +
970
- a({ href: "/admin/system" }, req.__("Restart here"))
1152
+ " " +
1153
+ a({ href: "/admin/system" }, req.__("Restart here"))
971
1154
  );
972
1155
  res.redirect("/useradmin/ssl");
973
1156
  } catch (e) {
@@ -1038,13 +1221,13 @@ router.get(
1038
1221
  contents: div(
1039
1222
  pass
1040
1223
  ? div(
1041
- { class: "alert alert-success", role: "alert" },
1042
- i({ class: "fas fa-check-circle fa-lg me-2" }),
1043
- h5(
1044
- { class: "d-inline" },
1045
- req.__("No errors detected during configuration check")
1046
- )
1224
+ { class: "alert alert-success", role: "alert" },
1225
+ i({ class: "fas fa-check-circle fa-lg me-2" }),
1226
+ h5(
1227
+ { class: "d-inline" },
1228
+ req.__("No errors detected during configuration check")
1047
1229
  )
1230
+ )
1048
1231
  : errors.map(mkError)
1049
1232
  ),
1050
1233
  },
@@ -1058,19 +1241,66 @@ router.get(
1058
1241
  })
1059
1242
  );
1060
1243
 
1244
+ const buildDialogScript = () => {
1245
+ return `<script>
1246
+ function swapEntryInputs(activeTab, activeInput, disabledTab, disabledInput) {
1247
+ activeTab.addClass("active");
1248
+ activeInput.removeClass("d-none");
1249
+ activeInput.addClass("d-block");
1250
+ activeInput.attr("name", "entryPoint");
1251
+ disabledTab.removeClass("active");
1252
+ disabledInput.removeClass("d-block");
1253
+ disabledInput.addClass("d-none");
1254
+ disabledInput.removeAttr("name");
1255
+ }
1256
+
1257
+ function showEntrySelect(type) {
1258
+ const viewNavLin = $("#viewNavLinkID");
1259
+ const pageNavLink = $("#pageNavLinkID");
1260
+ const viewInp = $("#viewInputID");
1261
+ const pageInp = $("#pageInputID");
1262
+ if (type === "page") {
1263
+ swapEntryInputs(pageNavLink, pageInp, viewNavLin, viewInp);
1264
+ }
1265
+ else if (type === "view") {
1266
+ swapEntryInputs(viewNavLin, viewInp, pageNavLink, pageInp);
1267
+ }
1268
+ $("#entryPointTypeID").attr("value", type);
1269
+ }
1270
+
1271
+ function handleMessages() {
1272
+ notifyAlert("This is still under development and might run longer.")
1273
+ ${
1274
+ getState().getConfig("apple_team_id") &&
1275
+ getState().getConfig("apple_team_id") !== "null"
1276
+ ? ""
1277
+ : `
1278
+ if ($("#iOSCheckboxId")[0].checked) {
1279
+ notifyAlert(
1280
+ "No 'Apple Team ID' is configured, I will try to build a project for the iOS simulator."
1281
+ );
1282
+ }`
1283
+ }
1284
+ }
1285
+ </script>`;
1286
+ };
1287
+
1061
1288
  router.get(
1062
1289
  "/build-mobile-app",
1063
1290
  isAdmin,
1064
1291
  error_catcher(async (req, res) => {
1065
- const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
1066
1292
  const views = await View.find();
1067
- const execBuildMsg =
1068
- "This is still under development and might run longer.";
1293
+ const pages = await Page.find();
1069
1294
 
1070
1295
  send_admin_page({
1071
1296
  res,
1072
1297
  req,
1073
1298
  active_sub: "Mobile app",
1299
+ headers: [
1300
+ {
1301
+ headerTag: buildDialogScript(),
1302
+ },
1303
+ ],
1074
1304
  contents: {
1075
1305
  above: [
1076
1306
  {
@@ -1088,34 +1318,77 @@ router.get(
1088
1318
  name: "_csrf",
1089
1319
  value: req.csrfToken(),
1090
1320
  }),
1321
+ input({
1322
+ type: "hidden",
1323
+ name: "entryPointType",
1324
+ value: "view",
1325
+ id: "entryPointTypeID",
1326
+ }),
1091
1327
  div(
1092
1328
  { class: "container ps-2" },
1093
1329
  div(
1094
1330
  { class: "row pb-2" },
1095
- div({ class: "col-sm-4 fw-bold" }, "Entry view"),
1096
- div({ class: "col-sm-4 fw-bold" }, "Platform"),
1331
+ div({ class: "col-sm-4 fw-bold" }, req.__("Entry point")),
1332
+ div({ class: "col-sm-4 fw-bold" }, req.__("Platform")),
1097
1333
  div(
1098
1334
  {
1099
1335
  class: "col-sm-1 fw-bold d-flex justify-content-center",
1100
1336
  },
1101
- "docker"
1337
+ req.__("docker")
1102
1338
  )
1103
1339
  ),
1104
1340
  div(
1105
1341
  { class: "row" },
1106
1342
  div(
1107
1343
  { class: "col-sm-4" },
1344
+ // 'view/page' tabs
1345
+ ul(
1346
+ { class: "nav nav-pills" },
1347
+ li(
1348
+ {
1349
+ class: "nav-item",
1350
+ onClick: "showEntrySelect('view')",
1351
+ },
1352
+ div(
1353
+ { class: "nav-link active", id: "viewNavLinkID" },
1354
+ req.__("View")
1355
+ )
1356
+ ),
1357
+ li(
1358
+ {
1359
+ class: "nav-item",
1360
+ onClick: "showEntrySelect('page')",
1361
+ },
1362
+ div(
1363
+ { class: "nav-link", id: "pageNavLinkID" },
1364
+ req.__("Page")
1365
+ )
1366
+ )
1367
+ ),
1368
+ // select entry-view
1108
1369
  select(
1109
1370
  {
1110
1371
  class: "form-control",
1111
- name: "entryView",
1112
- id: "entryViewInput",
1372
+ name: "entryPoint",
1373
+ id: "viewInputID",
1113
1374
  },
1114
1375
  views
1115
1376
  .map((view) =>
1116
1377
  option({ value: view.name }, view.name)
1117
1378
  )
1118
1379
  .join(",")
1380
+ ),
1381
+ // select entry-page
1382
+ select(
1383
+ {
1384
+ class: "form-control d-none",
1385
+ id: "pageInputID",
1386
+ },
1387
+ pages
1388
+ .map((page) =>
1389
+ option({ value: page.name }, page.name)
1390
+ )
1391
+ .join(",")
1119
1392
  )
1120
1393
  ),
1121
1394
  div(
@@ -1125,7 +1398,7 @@ router.get(
1125
1398
  { class: "container ps-0" },
1126
1399
  div(
1127
1400
  { class: "row" },
1128
- div({ class: "col-sm-8" }, "android"),
1401
+ div({ class: "col-sm-8" }, req.__("android")),
1129
1402
  div(
1130
1403
  { class: "col-sm" },
1131
1404
  input({
@@ -1138,7 +1411,7 @@ router.get(
1138
1411
  ),
1139
1412
  div(
1140
1413
  { class: "row" },
1141
- div({ class: "col-sm-8" }, "iOS"),
1414
+ div({ class: "col-sm-8" }, req.__("iOS")),
1142
1415
  div(
1143
1416
  { class: "col-sm" },
1144
1417
  input({
@@ -1170,7 +1443,7 @@ router.get(
1170
1443
  for: "appNameInputId",
1171
1444
  class: "form-label fw-bold",
1172
1445
  },
1173
- "App file"
1446
+ req.__("App file")
1174
1447
  ),
1175
1448
  input({
1176
1449
  type: "text",
@@ -1190,14 +1463,14 @@ router.get(
1190
1463
  for: "serverURLInputId",
1191
1464
  class: "form-label fw-bold",
1192
1465
  },
1193
- "Server URL"
1466
+ req.__("Server URL")
1194
1467
  ),
1195
1468
  input({
1196
1469
  type: "text",
1197
1470
  class: "form-control",
1198
1471
  name: "serverURL",
1199
1472
  id: "serverURLInputId",
1200
- placeholder: "http://10.0.2.2:3000",
1473
+ placeholder: getState().getConfig("base_url") || "",
1201
1474
  })
1202
1475
  )
1203
1476
  )
@@ -1205,12 +1478,12 @@ router.get(
1205
1478
  button(
1206
1479
  {
1207
1480
  type: "submit",
1208
- onClick: `notifyAlert('${execBuildMsg}'); press_store_button(this);`,
1481
+ onClick: `handleMessages(); press_store_button(this);`,
1209
1482
  class: "btn btn-warning",
1210
1483
  },
1211
1484
  i({ class: "fas fa-hammer pe-2" }),
1212
1485
 
1213
- "Build mobile app"
1486
+ req.__("Build mobile app")
1214
1487
  )
1215
1488
  )
1216
1489
  ),
@@ -1226,7 +1499,8 @@ router.post(
1226
1499
  isAdmin,
1227
1500
  error_catcher(async (req, res) => {
1228
1501
  let {
1229
- entryView,
1502
+ entryPoint,
1503
+ entryPointType,
1230
1504
  androidPlatform,
1231
1505
  iOSPlatform,
1232
1506
  useDocker,
@@ -1244,12 +1518,20 @@ router.post(
1244
1518
  req.flash("error", req.__("Only the android build supports docker."));
1245
1519
  return res.redirect("/admin/build-mobile-app");
1246
1520
  }
1247
- if (appFile && !appFile.endsWith(".apk")) appFile = `${appFile}.apk`;
1521
+ if (!serverURL || serverURL.length == 0) {
1522
+ serverURL = getState().getConfig("base_url") || "";
1523
+ }
1524
+ if (!serverURL.startsWith("http")) {
1525
+ req.flash("error", req.__("Please enter a valid server URL."));
1526
+ return res.redirect("/admin/build-mobile-app");
1527
+ }
1248
1528
  const appOut = path.join(__dirname, "..", "mobile-app-out");
1249
1529
  const spawnParams = [
1250
1530
  "build-app",
1251
- "-v",
1252
- entryView,
1531
+ "-e",
1532
+ entryPoint,
1533
+ "-t",
1534
+ entryPointType,
1253
1535
  "-c",
1254
1536
  appOut,
1255
1537
  "-b",
@@ -1257,7 +1539,13 @@ router.post(
1257
1539
  ];
1258
1540
  if (useDocker) spawnParams.push("-d");
1259
1541
  if (androidPlatform) spawnParams.push("-p", "android");
1260
- if (iOSPlatform) spawnParams.push("-p", "ios");
1542
+ if (iOSPlatform) {
1543
+ spawnParams.push("-p", "ios");
1544
+ const teamId = getState().getConfig("apple_team_id");
1545
+ if (!teamId || teamId === "null") {
1546
+ spawnParams.push("--buildForEmulator");
1547
+ }
1548
+ }
1261
1549
  if (appFile) spawnParams.push("-a", appFile);
1262
1550
  if (serverURL) spawnParams.push("-s", serverURL);
1263
1551
  const child = spawn("saltcorn", spawnParams, {
@@ -1275,10 +1563,13 @@ router.post(
1275
1563
  });
1276
1564
  child.on("exit", async function (exitCode, signal) {
1277
1565
  if (exitCode === 0) {
1278
- const file = await File.from_existing_file(
1279
- appOut,
1280
- appFile ? appFile : "app-debug.apk",
1281
- req.user.id
1566
+ const files = await Promise.all(
1567
+ fs
1568
+ .readdirSync(appOut)
1569
+ .map(
1570
+ async (outFile) =>
1571
+ await File.from_existing_file(appOut, outFile, req.user.id)
1572
+ )
1282
1573
  );
1283
1574
  res.sendWrap(req.__(`Admin`), {
1284
1575
  above: [
@@ -1287,7 +1578,7 @@ router.post(
1287
1578
  title: req.__("Build Result"),
1288
1579
  contents: div("The build was successfully"),
1289
1580
  },
1290
- app_files_table(file, req),
1581
+ files.length > 0 ? app_files_table(files, req) : "",
1291
1582
  ],
1292
1583
  });
1293
1584
  } else