@saltcorn/server 0.7.3-beta.2 → 0.7.3-beta.7

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,7 @@ 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 } = require("@saltcorn/markup");
21
+ const { post_btn, renderForm, mkTable, link } = require("@saltcorn/markup");
22
22
  const {
23
23
  div,
24
24
  a,
@@ -35,6 +35,17 @@ const {
35
35
  code,
36
36
  h5,
37
37
  pre,
38
+ button,
39
+ form,
40
+ label,
41
+ input,
42
+ select,
43
+ option,
44
+ fieldset,
45
+ legend,
46
+ ul,
47
+ li,
48
+ ol,
38
49
  } = require("@saltcorn/markup/tags");
39
50
  const db = require("@saltcorn/data/db");
40
51
  const {
@@ -75,6 +86,7 @@ const {
75
86
  const moment = require("moment");
76
87
  const View = require("@saltcorn/data/models/view");
77
88
  const { getConfigFile } = require("@saltcorn/data/db/connect");
89
+ const os = require("os");
78
90
 
79
91
  /**
80
92
  * @type {object}
@@ -136,6 +148,23 @@ const email_form = async (req) => {
136
148
  return form;
137
149
  };
138
150
 
151
+ const app_files_table = (file, req) =>
152
+ mkTable(
153
+ [
154
+ {
155
+ label: req.__("Filename"),
156
+ key: (r) => div(r.filename),
157
+ },
158
+ { label: req.__("Size (KiB)"), key: "size_kb", align: "right" },
159
+ { label: req.__("Media type"), key: (r) => r.mimetype },
160
+ {
161
+ label: req.__("Download"),
162
+ key: (r) => link(`/files/download/${r.id}`, req.__("Download")),
163
+ },
164
+ ],
165
+ [file]
166
+ );
167
+
139
168
  /**
140
169
  * Router get /
141
170
  * @name get
@@ -345,7 +374,13 @@ router.get(
345
374
  ? {
346
375
  type: "card",
347
376
  title: req.__("Automated backup"),
348
- contents: div(renderForm(backupForm, req.csrfToken())),
377
+ contents: div(
378
+ renderForm(backupForm, req.csrfToken()),
379
+ a(
380
+ { href: "/admin/auto-backup-list" },
381
+ "Restore/download automated backups »"
382
+ )
383
+ ),
349
384
  }
350
385
  : { type: "blank", contents: "" },
351
386
  ],
@@ -354,6 +389,93 @@ router.get(
354
389
  })
355
390
  );
356
391
 
392
+ /**
393
+ * @name get/backup
394
+ * @function
395
+ * @memberof module:routes/admin~routes/adminRouter
396
+ */
397
+ router.get(
398
+ "/auto-backup-list",
399
+ isAdmin,
400
+ error_catcher(async (req, res) => {
401
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
402
+ if (!isRoot) {
403
+ res.redirect("/admin/backup");
404
+ return;
405
+ }
406
+ const auto_backup_directory = getState().getConfig("auto_backup_directory");
407
+ const fileNms = await fs.promises.readdir(auto_backup_directory);
408
+ const backupFiles = fileNms.filter(
409
+ (fnm) => fnm.startsWith("sc-backup") && fnm.endsWith(".zip")
410
+ );
411
+ send_admin_page({
412
+ res,
413
+ req,
414
+ active_sub: "Backup",
415
+ contents: {
416
+ above: [
417
+ {
418
+ type: "card",
419
+ title: req.__("Download automated backup"),
420
+ contents: div(
421
+ ul(
422
+ backupFiles.map((fnm) =>
423
+ li(
424
+ a(
425
+ {
426
+ href: `/admin/auto-backup-download/${encodeURIComponent(
427
+ fnm
428
+ )}`,
429
+ },
430
+ fnm
431
+ )
432
+ )
433
+ )
434
+ )
435
+ ),
436
+ },
437
+ {
438
+ type: "card",
439
+ title: req.__("Restoring automated backup"),
440
+ contents: div(
441
+ ol(
442
+ li("Download one of the backups above"),
443
+ li(
444
+ a({ href: "/admin/clear-all" }, "Clear this application"),
445
+ " ",
446
+ "(tick all boxes)"
447
+ ),
448
+ li(
449
+ "When prompted to create the first user, click the link to restore a backup"
450
+ ),
451
+ li("Select the downloaded backup file")
452
+ )
453
+ ),
454
+ },
455
+ ],
456
+ },
457
+ });
458
+ })
459
+ );
460
+
461
+ router.get(
462
+ "/auto-backup-download/:filename",
463
+ isAdmin,
464
+ error_catcher(async (req, res) => {
465
+ const { filename } = req.params;
466
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
467
+ if (
468
+ !isRoot ||
469
+ !(filename.startsWith("sc-backup") && filename.endsWith(".zip"))
470
+ ) {
471
+ res.redirect("/admin/backup");
472
+ return;
473
+ }
474
+ const auto_backup_directory = getState().getConfig("auto_backup_directory");
475
+ res.download(path.join(auto_backup_directory, filename), filename);
476
+ })
477
+ );
478
+
357
479
  /**
358
480
  * Auto backup Form
359
481
  * @param {object} req
@@ -503,6 +625,7 @@ router.get(
503
625
  " ",
504
626
  req.__("Configuration check")
505
627
  ),
628
+
506
629
  hr(),
507
630
 
508
631
  a(
@@ -935,6 +1058,272 @@ router.get(
935
1058
  })
936
1059
  );
937
1060
 
1061
+ router.get(
1062
+ "/build-mobile-app",
1063
+ isAdmin,
1064
+ error_catcher(async (req, res) => {
1065
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
1066
+ const views = await View.find();
1067
+ const execBuildMsg =
1068
+ "This is still under development and might run longer.";
1069
+
1070
+ send_admin_page({
1071
+ res,
1072
+ req,
1073
+ active_sub: "Mobile app",
1074
+ contents: {
1075
+ above: [
1076
+ {
1077
+ type: "card",
1078
+ title: req.__("Build mobile app"),
1079
+ contents: form(
1080
+ {
1081
+ action: "/admin/build-mobile-app",
1082
+ method: "post",
1083
+ },
1084
+
1085
+ fieldset(
1086
+ input({
1087
+ type: "hidden",
1088
+ name: "_csrf",
1089
+ value: req.csrfToken(),
1090
+ }),
1091
+ div(
1092
+ { class: "container ps-2" },
1093
+ div(
1094
+ { class: "row pb-2" },
1095
+ div({ class: "col-sm-4 fw-bold" }, "Entry view"),
1096
+ div({ class: "col-sm-4 fw-bold" }, "Platform"),
1097
+ div(
1098
+ {
1099
+ class: "col-sm-1 fw-bold d-flex justify-content-center",
1100
+ },
1101
+ "docker"
1102
+ )
1103
+ ),
1104
+ div(
1105
+ { class: "row" },
1106
+ div(
1107
+ { class: "col-sm-4" },
1108
+ select(
1109
+ {
1110
+ class: "form-control",
1111
+ name: "entryView",
1112
+ id: "entryViewInput",
1113
+ },
1114
+ views
1115
+ .map((view) =>
1116
+ option({ value: view.name }, view.name)
1117
+ )
1118
+ .join(",")
1119
+ )
1120
+ ),
1121
+ div(
1122
+ { class: "col-sm-4" },
1123
+
1124
+ div(
1125
+ { class: "container ps-0" },
1126
+ div(
1127
+ { class: "row" },
1128
+ div({ class: "col-sm-8" }, "android"),
1129
+ div(
1130
+ { class: "col-sm" },
1131
+ input({
1132
+ type: "checkbox",
1133
+ class: "form-check-input",
1134
+ name: "androidPlatform",
1135
+ id: "androidCheckboxId",
1136
+ })
1137
+ )
1138
+ ),
1139
+ div(
1140
+ { class: "row" },
1141
+ div({ class: "col-sm-8" }, "iOS"),
1142
+ div(
1143
+ { class: "col-sm" },
1144
+ input({
1145
+ type: "checkbox",
1146
+ class: "form-check-input",
1147
+ name: "iOSPlatform",
1148
+ id: "iOSCheckboxId",
1149
+ })
1150
+ )
1151
+ )
1152
+ )
1153
+ ),
1154
+ div(
1155
+ { class: "col-sm-1 d-flex justify-content-center" },
1156
+ input({
1157
+ type: "checkbox",
1158
+ class: "form-check-input",
1159
+ name: "useDocker",
1160
+ id: "dockerCheckboxId",
1161
+ })
1162
+ )
1163
+ ),
1164
+ div(
1165
+ { class: "row pb-2" },
1166
+ div(
1167
+ { class: "col-sm-8" },
1168
+ label(
1169
+ {
1170
+ for: "appNameInputId",
1171
+ class: "form-label fw-bold",
1172
+ },
1173
+ "App file"
1174
+ ),
1175
+ input({
1176
+ type: "text",
1177
+ class: "form-control",
1178
+ name: "appFile",
1179
+ id: "appFileInputId",
1180
+ placeholder: "app-debug",
1181
+ })
1182
+ )
1183
+ ),
1184
+ div(
1185
+ { class: "row pb-3" },
1186
+ div(
1187
+ { class: "col-sm-8" },
1188
+ label(
1189
+ {
1190
+ for: "serverURLInputId",
1191
+ class: "form-label fw-bold",
1192
+ },
1193
+ "Server URL"
1194
+ ),
1195
+ input({
1196
+ type: "text",
1197
+ class: "form-control",
1198
+ name: "serverURL",
1199
+ id: "serverURLInputId",
1200
+ placeholder: "http://10.0.2.2:3000",
1201
+ })
1202
+ )
1203
+ )
1204
+ ),
1205
+ button(
1206
+ {
1207
+ type: "submit",
1208
+ onClick: `notifyAlert('${execBuildMsg}'); press_store_button(this);`,
1209
+ class: "btn btn-warning",
1210
+ },
1211
+ i({ class: "fas fa-hammer pe-2" }),
1212
+
1213
+ "Build mobile app"
1214
+ )
1215
+ )
1216
+ ),
1217
+ },
1218
+ ],
1219
+ },
1220
+ });
1221
+ })
1222
+ );
1223
+
1224
+ router.post(
1225
+ "/build-mobile-app",
1226
+ isAdmin,
1227
+ error_catcher(async (req, res) => {
1228
+ let {
1229
+ entryView,
1230
+ androidPlatform,
1231
+ iOSPlatform,
1232
+ useDocker,
1233
+ appFile,
1234
+ serverURL,
1235
+ } = req.body;
1236
+ if (!androidPlatform && !iOSPlatform) {
1237
+ req.flash(
1238
+ "error",
1239
+ req.__("Please select at least one platform (android or iOS).")
1240
+ );
1241
+ return res.redirect("/admin/build-mobile-app");
1242
+ }
1243
+ if (!androidPlatform && useDocker) {
1244
+ req.flash("error", req.__("Only the android build supports docker."));
1245
+ return res.redirect("/admin/build-mobile-app");
1246
+ }
1247
+ if (appFile && !appFile.endsWith(".apk")) appFile = `${appFile}.apk`;
1248
+ const appOut = path.join(__dirname, "..", "mobile-app-out");
1249
+ const spawnParams = [
1250
+ "build-app",
1251
+ "-v",
1252
+ entryView,
1253
+ "-c",
1254
+ appOut,
1255
+ "-b",
1256
+ `${os.userInfo().homedir}/mobile_app_build`,
1257
+ ];
1258
+ if (useDocker) spawnParams.push("-d");
1259
+ if (androidPlatform) spawnParams.push("-p", "android");
1260
+ if (iOSPlatform) spawnParams.push("-p", "ios");
1261
+ if (appFile) spawnParams.push("-a", appFile);
1262
+ if (serverURL) spawnParams.push("-s", serverURL);
1263
+ const child = spawn("saltcorn", spawnParams, {
1264
+ stdio: ["ignore", "pipe", "pipe"],
1265
+ cwd: ".",
1266
+ });
1267
+ const childOutputs = [];
1268
+ child.stdout.on("data", (data) => {
1269
+ // console.log(data.toString());
1270
+ childOutputs.push(data.toString());
1271
+ });
1272
+ child.stderr.on("data", (data) => {
1273
+ // console.log(data.toString());
1274
+ childOutputs.push(data.toString());
1275
+ });
1276
+ child.on("exit", async function (exitCode, signal) {
1277
+ if (exitCode === 0) {
1278
+ const file = await File.from_existing_file(
1279
+ appOut,
1280
+ appFile ? appFile : "app-debug.apk",
1281
+ req.user.id
1282
+ );
1283
+ res.sendWrap(req.__(`Admin`), {
1284
+ above: [
1285
+ {
1286
+ type: "card",
1287
+ title: req.__("Build Result"),
1288
+ contents: div("The build was successfully"),
1289
+ },
1290
+ app_files_table(file, req),
1291
+ ],
1292
+ });
1293
+ } else
1294
+ res.sendWrap(req.__(`Admin`), {
1295
+ above: [
1296
+ {
1297
+ type: "card",
1298
+ title: req.__("Build Result"),
1299
+ contents: div(
1300
+ "Unable to build the app:",
1301
+ pre(code(childOutputs.join("<br/>")))
1302
+ ),
1303
+ },
1304
+ ],
1305
+ });
1306
+ });
1307
+ child.on("error", function (msg) {
1308
+ const message = msg.message ? msg.message : msg.code;
1309
+ const stack = msg.stack ? msg.stack : "";
1310
+ res.sendWrap(req.__(`Admin`), {
1311
+ above: [
1312
+ {
1313
+ type: "card",
1314
+ title: req.__("Build Result"),
1315
+ contents: div(
1316
+ p("Unable to build the app:"),
1317
+ pre(code(message)),
1318
+ pre(code(stack))
1319
+ ),
1320
+ },
1321
+ ],
1322
+ });
1323
+ });
1324
+ })
1325
+ );
1326
+
938
1327
  /**
939
1328
  * @name post/clear-all
940
1329
  * @function
package/routes/fields.js CHANGED
@@ -488,6 +488,7 @@ router.get(
488
488
  },
489
489
  {
490
490
  type: "card",
491
+ class: "mt-0",
491
492
  title: wizardCardTitle(field.label, wf, wfres),
492
493
  contents: renderForm(wfres.renderForm, req.csrfToken()),
493
494
  },
@@ -524,6 +525,7 @@ router.get(
524
525
  },
525
526
  {
526
527
  type: "card",
528
+ class: "mt-0",
527
529
  title: wizardCardTitle(req.__(`New field`), wf, wfres),
528
530
  contents: renderForm(wfres.renderForm, req.csrfToken()),
529
531
  },
@@ -589,6 +591,7 @@ router.post(
589
591
  },
590
592
  {
591
593
  type: "card",
594
+ class: "mt-0",
592
595
  title: wizardCardTitle(
593
596
  wfres.context.label || req.__("New field"),
594
597
  wf,
package/routes/files.js CHANGED
@@ -184,11 +184,11 @@ router.get(
184
184
  * @function
185
185
  */
186
186
  router.get(
187
- "/resize/:id/:width_str",
187
+ "/resize/:id/:width_str/:height_str?",
188
188
  error_catcher(async (req, res) => {
189
189
  const role = req.user && req.user.id ? req.user.role_id : 10;
190
190
  const user_id = req.user && req.user.id;
191
- const { id, width_str } = req.params;
191
+ const { id, width_str, height_str } = req.params;
192
192
  let file;
193
193
  if (typeof strictParseInt(id) !== "undefined")
194
194
  file = await File.findOne({ id });
@@ -208,15 +208,17 @@ router.get(
208
208
  if (file.s3_store) s3storage.serveObject(file, res, false);
209
209
  else {
210
210
  const width = strictParseInt(width_str);
211
+ const height = height_str ? strictParseInt(height_str) : null;
211
212
  if (!width) {
212
213
  res.sendFile(file.location);
213
214
  return;
214
215
  }
215
- const fnm = `${file.location}_w${width}`;
216
+ const fnm = `${file.location}_w${width}${height ? `_h${height}` : ""}`;
216
217
  if (!fs.existsSync(fnm)) {
217
218
  await resizer({
218
219
  fromFileName: file.location,
219
220
  width,
221
+ height,
220
222
  toFileName: fnm,
221
223
  });
222
224
  }
@@ -49,7 +49,7 @@ const tableTable = (tables, req) =>
49
49
  */
50
50
  const tableCard = (tables, req) => ({
51
51
  type: "card",
52
- class: "welcome-page-entity-list",
52
+ class: "welcome-page-entity-list mt-1",
53
53
  title: link("/table", req.__("Tables")),
54
54
  contents:
55
55
  (tables.length <= 1
@@ -102,7 +102,7 @@ const viewTable = (views, req) =>
102
102
  const viewCard = (views, req) => ({
103
103
  type: "card",
104
104
  title: link("/viewedit", req.__("Views")),
105
- class: "welcome-page-entity-list",
105
+ class: "welcome-page-entity-list mt-1",
106
106
  bodyClass: "py-0 pe-0",
107
107
  contents:
108
108
  (views.length <= 1
@@ -156,7 +156,7 @@ const pageTable = (pages, req) =>
156
156
  const pageCard = (pages, req) => ({
157
157
  type: "card",
158
158
  title: link("/pageedit", req.__("Pages")),
159
- class: "welcome-page-entity-list",
159
+ class: "welcome-page-entity-list mt-1",
160
160
  contents:
161
161
  (pages.length <= 1
162
162
  ? p(
@@ -369,9 +369,9 @@ const welcome_page = async (req) => {
369
369
  above: [
370
370
  {
371
371
  besides: [
372
- pageCard(pages, req),
373
- viewCard(views, req),
374
372
  tableCard(tables, req),
373
+ viewCard(views, req),
374
+ pageCard(pages, req),
375
375
  ],
376
376
  },
377
377
  {
@@ -380,7 +380,7 @@ const welcome_page = async (req) => {
380
380
  type: "card",
381
381
  //title: req.__("Install pack"),
382
382
  bodyClass: "py-0 pe-0",
383
- class: "welcome-page-entity-list",
383
+ class: "welcome-page-entity-list mt-2",
384
384
 
385
385
  tabContents:
386
386
  triggers.length > 0
@@ -399,7 +399,7 @@ const welcome_page = async (req) => {
399
399
  type: "card",
400
400
  //title: req.__("Learn"),
401
401
  bodyClass: "py-0 pe-0",
402
- class: "welcome-page-entity-list",
402
+ class: "welcome-page-entity-list mt-2",
403
403
  tabContents:
404
404
  users.length > 4
405
405
  ? {
package/routes/menu.js CHANGED
@@ -84,7 +84,15 @@ const menuForm = async (req) => {
84
84
  input_type: "select",
85
85
  class: "menutype item-menu",
86
86
  required: true,
87
- options: ["View", "Page", "Link", "Header", "Dynamic", "Search"],
87
+ options: [
88
+ "View",
89
+ "Page",
90
+ "Link",
91
+ "Header",
92
+ "Dynamic",
93
+ "Search",
94
+ "Separator",
95
+ ],
88
96
  },
89
97
  {
90
98
  name: "text",
@@ -92,6 +100,9 @@ const menuForm = async (req) => {
92
100
  class: "item-menu",
93
101
  input_type: "text",
94
102
  required: true,
103
+ showIf: {
104
+ type: ["View", "Page", "Link", "Header", "Dynamic", "Search"],
105
+ },
95
106
  },
96
107
  {
97
108
  name: "icon_btn",
@@ -271,6 +282,7 @@ const menuEditorScript = (menu_items) => `
271
282
  {
272
283
  listOptions: sortableListOptions,
273
284
  iconPicker: iconPickerOptions,
285
+ getLabelText: (item) => item?.text || item?.type,
274
286
  labelEdit: 'Edit&nbsp;<i class="fas fa-edit clickable"></i>',
275
287
  maxLevel: 1 // (Optional) Default is -1 (no level limit)
276
288
  // Valid levels are from [0, 1, 2, 3,...N]
@@ -326,6 +326,7 @@ router.get(
326
326
  {
327
327
  type: "card",
328
328
  title: req.__("Your pages"),
329
+ class: "mt-0",
329
330
  contents: getPageList(pages, roles, req),
330
331
  },
331
332
  {
package/routes/plugins.js CHANGED
@@ -506,6 +506,7 @@ const plugin_store_html = (items, req) => {
506
506
  },
507
507
  {
508
508
  type: "card",
509
+ class: "mt-0",
509
510
  contents: div(
510
511
  { class: "d-flex justify-content-between" },
511
512
  storeNavPills(req),
package/routes/tables.js CHANGED
@@ -448,6 +448,7 @@ router.get(
448
448
  },
449
449
  {
450
450
  type: "card",
451
+ class: "mt-0",
451
452
  title: cardHeaderTabs([
452
453
  { label: req.__("Your tables"), href: "/table" },
453
454
  {
@@ -636,7 +637,9 @@ router.get(
636
637
  }
637
638
  var viewCard;
638
639
  if (fields.length > 0) {
639
- const views = await View.find({ table_id: table.id });
640
+ const views = await View.find(
641
+ table.external ? { exttable_name: table.name } : { table_id: table.id }
642
+ );
640
643
  var viewCardContents;
641
644
  if (views.length > 0) {
642
645
  viewCardContents = mkTable(
@@ -801,15 +804,12 @@ router.get(
801
804
  type: "breadcrumbs",
802
805
  crumbs: [
803
806
  { text: req.__("Tables"), href: "/table" },
804
- { text: table.name },
807
+ { text: span({ class: "fw-bold text-body" }, table.name) },
805
808
  ],
806
809
  },
807
- {
808
- type: "pageHeader",
809
- title: req.__(`%s table`, table.name),
810
- },
811
810
  {
812
811
  type: "card",
812
+ class: "mt-0",
813
813
  title: req.__("Fields"),
814
814
  contents: fieldCard,
815
815
  },
@@ -1076,6 +1076,7 @@ router.get(
1076
1076
  },
1077
1077
  {
1078
1078
  type: "card",
1079
+ class: "mt-0",
1079
1080
  title: cardHeaderTabs([
1080
1081
  { label: req.__("Your tables"), href: "/table", active: true },
1081
1082
  {
@@ -197,6 +197,7 @@ router.get(
197
197
  },
198
198
  {
199
199
  type: "card",
200
+ class: "mt-0",
200
201
  title: req.__("Your views"),
201
202
  contents: [
202
203
  viewMarkup,
@@ -378,7 +379,13 @@ router.get(
378
379
  },
379
380
  {
380
381
  type: "card",
381
- title: req.__(`Edit %s view`, viewname),
382
+ class: "mt-0",
383
+ title: req.__(
384
+ `%s view - %s on %s`,
385
+ viewname,
386
+ viewrow.viewtemplate,
387
+ viewrow.table_name
388
+ ),
382
389
  contents: renderForm(form, req.csrfToken()),
383
390
  },
384
391
  ],
@@ -415,6 +422,7 @@ router.get(
415
422
  },
416
423
  {
417
424
  type: "card",
425
+ class: "mt-0",
418
426
  title: req.__(`Create view`),
419
427
  contents: renderForm(form, req.csrfToken()),
420
428
  },
@@ -452,6 +460,7 @@ router.post(
452
460
  },
453
461
  {
454
462
  type: "card",
463
+ class: "mt-0",
455
464
  title: req.__(`Edit view`),
456
465
  contents: renderForm(form, req.csrfToken()),
457
466
  },
@@ -527,6 +536,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
527
536
  },
528
537
  {
529
538
  type: noCard ? "container" : "card",
539
+ class: !noCard && "mt-0",
530
540
  title: wfres.title,
531
541
  contents,
532
542
  },