@saltcorn/server 0.9.6-beta.2 → 0.9.6-beta.20

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.
Files changed (49) hide show
  1. package/app.js +6 -1
  2. package/auth/admin.js +55 -53
  3. package/auth/routes.js +28 -10
  4. package/auth/testhelp.js +86 -0
  5. package/help/Field label.tmd +11 -0
  6. package/help/Field types.tmd +39 -0
  7. package/help/Ownership field.tmd +76 -0
  8. package/help/Ownership formula.tmd +75 -0
  9. package/help/Table roles.tmd +20 -0
  10. package/help/User groups.tmd +35 -0
  11. package/load_plugins.js +33 -5
  12. package/locales/en.json +29 -1
  13. package/locales/it.json +3 -2
  14. package/markup/admin.js +1 -0
  15. package/markup/forms.js +5 -1
  16. package/package.json +9 -9
  17. package/public/log_viewer_utils.js +32 -0
  18. package/public/mermaid.min.js +705 -306
  19. package/public/saltcorn-builder.css +23 -0
  20. package/public/saltcorn-common.js +248 -80
  21. package/public/saltcorn.css +80 -0
  22. package/public/saltcorn.js +86 -2
  23. package/restart_watcher.js +1 -0
  24. package/routes/actions.js +27 -0
  25. package/routes/admin.js +175 -64
  26. package/routes/api.js +6 -0
  27. package/routes/common_lists.js +42 -32
  28. package/routes/fields.js +70 -42
  29. package/routes/homepage.js +2 -0
  30. package/routes/index.js +2 -0
  31. package/routes/menu.js +69 -4
  32. package/routes/notifications.js +90 -10
  33. package/routes/pageedit.js +18 -13
  34. package/routes/plugins.js +11 -2
  35. package/routes/registry.js +289 -0
  36. package/routes/search.js +10 -4
  37. package/routes/tables.js +51 -27
  38. package/routes/tenant.js +4 -15
  39. package/routes/utils.js +25 -8
  40. package/routes/view.js +1 -1
  41. package/routes/viewedit.js +11 -7
  42. package/serve.js +27 -5
  43. package/tests/edit.test.js +426 -0
  44. package/tests/fields.test.js +21 -0
  45. package/tests/filter.test.js +68 -0
  46. package/tests/page.test.js +2 -2
  47. package/tests/plugins.test.js +2 -0
  48. package/tests/sync.test.js +59 -0
  49. package/wrapper.js +4 -1
package/routes/actions.js CHANGED
@@ -375,6 +375,7 @@ router.get(
375
375
 
376
376
  const form = await triggerForm(req, trigger);
377
377
  form.values = trigger;
378
+ form.onChange = `saveAndContinue(this)`;
378
379
  send_events_page({
379
380
  res,
380
381
  req,
@@ -383,6 +384,7 @@ router.get(
383
384
  contents: {
384
385
  type: "card",
385
386
  title: req.__("Edit trigger %s", id),
387
+ titleAjaxIndicator: true,
386
388
  contents: renderForm(form, req.csrfToken()),
387
389
  },
388
390
  });
@@ -464,6 +466,10 @@ router.post(
464
466
  ...form.values.configuration,
465
467
  };
466
468
  await Trigger.update(trigger.id, form.values); //{configuration: form.values});
469
+ if (req.xhr) {
470
+ res.json({ success: "ok" });
471
+ return;
472
+ }
467
473
  req.flash("success", req.__("Action information saved"));
468
474
  res.redirect(`/actions/`);
469
475
  }
@@ -881,3 +887,24 @@ router.get(
881
887
  }
882
888
  })
883
889
  );
890
+
891
+ /**
892
+ * @name post/clone/:id
893
+ * @function
894
+ * @memberof module:routes/actions~actionsRouter
895
+ * @function
896
+ */
897
+ router.post(
898
+ "/clone/:id",
899
+ isAdmin,
900
+ error_catcher(async (req, res) => {
901
+ const { id } = req.params;
902
+ const trig = await Trigger.findOne({ id });
903
+ const newtrig = await trig.clone();
904
+ req.flash(
905
+ "success",
906
+ req.__("Trigger %s duplicated as %s", trig.name, newtrig.name)
907
+ );
908
+ res.redirect(`/actions`);
909
+ })
910
+ );
package/routes/admin.js CHANGED
@@ -108,6 +108,7 @@ const stream = require("stream");
108
108
  const Crash = require("@saltcorn/data/models/crash");
109
109
  const { get_help_markup } = require("../help/index.js");
110
110
  const Docker = require("dockerode");
111
+ const npmFetch = require("npm-registry-fetch");
111
112
 
112
113
  const router = new Router();
113
114
  module.exports = router;
@@ -1004,6 +1005,7 @@ router.get(
1004
1005
  "custom_ssl_certificate",
1005
1006
  false
1006
1007
  );
1008
+ const rndid = `bs${Math.round(Math.random() * 100000)}`;
1007
1009
  let expiry = "";
1008
1010
  if (custom_ssl_certificate && X509Certificate) {
1009
1011
  const { validTo } = new X509Certificate(custom_ssl_certificate);
@@ -1062,7 +1064,8 @@ router.get(
1062
1064
  " ",
1063
1065
  req.__("Clear all"),
1064
1066
  " »"
1065
- )
1067
+ ),
1068
+ hr()
1066
1069
  ),
1067
1070
  },
1068
1071
  {
@@ -1075,32 +1078,45 @@ router.get(
1075
1078
  tr(
1076
1079
  th(req.__("Saltcorn version")),
1077
1080
  td(
1078
- packagejson.version +
1079
- (isRoot && can_update
1080
- ? post_btn(
1081
- "/admin/upgrade",
1082
- req.__("Upgrade"),
1083
- req.csrfToken(),
1084
- {
1085
- btnClass: "btn-primary btn-sm",
1086
- formClass: "d-inline",
1087
- }
1088
- )
1089
- : isRoot && is_latest
1090
- ? span(
1091
- { class: "badge bg-primary ms-2" },
1092
- req.__("Latest")
1093
- ) +
1094
- post_btn(
1095
- "/admin/check-for-upgrade",
1096
- req.__("Check for updates"),
1097
- req.csrfToken(),
1098
- {
1099
- btnClass: "btn-primary btn-sm px-1 py-0",
1100
- formClass: "d-inline",
1101
- }
1102
- )
1103
- : "")
1081
+ packagejson.version,
1082
+ isRoot && can_update
1083
+ ? post_btn(
1084
+ "/admin/upgrade",
1085
+ req.__("Upgrade"),
1086
+ req.csrfToken(),
1087
+ {
1088
+ btnClass: "btn-primary btn-sm",
1089
+ formClass: "d-inline",
1090
+ }
1091
+ )
1092
+ : isRoot && is_latest
1093
+ ? span(
1094
+ { class: "badge bg-primary ms-2" },
1095
+ req.__("Latest")
1096
+ ) +
1097
+ post_btn(
1098
+ "/admin/check-for-upgrade",
1099
+ req.__("Check updates"),
1100
+ req.csrfToken(),
1101
+ {
1102
+ btnClass: "btn-primary btn-sm px-1 py-0",
1103
+ formClass: "d-inline",
1104
+ }
1105
+ )
1106
+ : "",
1107
+ !git_commit &&
1108
+ a(
1109
+ {
1110
+ id: rndid,
1111
+ class: "btn btn-sm btn-secondary ms-1 px-1 py-0",
1112
+ onClick: "press_store_button(this, true)",
1113
+ href:
1114
+ `javascript:ajax_modal('/admin/install_dialog', ` +
1115
+ `{ onOpen: () => { restore_old_button('${rndid}'); }, ` +
1116
+ ` onError: (res) => { selectVersionError(res, '${rndid}') } });`,
1117
+ },
1118
+ req.__("Choose version")
1119
+ )
1104
1120
  )
1105
1121
  ),
1106
1122
  git_commit &&
@@ -1222,6 +1238,137 @@ const pullCordovaBuilder = (req, res) => {
1222
1238
  });
1223
1239
  };
1224
1240
 
1241
+ /*
1242
+ * fetch available saltcorn versions and show a dialog to select one
1243
+ */
1244
+ router.get(
1245
+ "/install_dialog",
1246
+ isAdmin,
1247
+ error_catcher(async (req, res) => {
1248
+ try {
1249
+ const pkgInfo = await npmFetch.json(
1250
+ "https://registry.npmjs.org/@saltcorn/cli"
1251
+ );
1252
+ if (!pkgInfo?.versions)
1253
+ throw new Error(req.__("Unable to fetch versions"));
1254
+ const versions = Object.keys(pkgInfo.versions);
1255
+ if (versions.length === 0) throw new Error(req.__("No versions found"));
1256
+ res.set("Page-Title", req.__("%s versions", "Saltcorn"));
1257
+ let selected = packagejson.version;
1258
+ res.send(
1259
+ form(
1260
+ {
1261
+ action: `/admin/install`,
1262
+ method: "post",
1263
+ },
1264
+ input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
1265
+ div(
1266
+ { class: "form-group" },
1267
+ label(
1268
+ {
1269
+ for: "version_select",
1270
+ class: "form-label fw-bold",
1271
+ },
1272
+ req.__("Version")
1273
+ ),
1274
+ select(
1275
+ {
1276
+ id: "version_select",
1277
+ class: "form-control form-select",
1278
+ name: "version",
1279
+ },
1280
+ versions.map((version) =>
1281
+ option({
1282
+ id: `${version}_opt`,
1283
+ value: version,
1284
+ label: version,
1285
+ selected: version === selected,
1286
+ })
1287
+ )
1288
+ )
1289
+ ),
1290
+ div(
1291
+ { class: "d-flex justify-content-end" },
1292
+ button(
1293
+ {
1294
+ type: "button",
1295
+ class: "btn btn-secondary me-2",
1296
+ "data-bs-dismiss": "modal",
1297
+ },
1298
+ req.__("Close")
1299
+ ),
1300
+ button(
1301
+ {
1302
+ type: "submit",
1303
+ class: "btn btn-primary",
1304
+ onClick: "press_store_button(this)",
1305
+ },
1306
+ req.__("Install")
1307
+ )
1308
+ )
1309
+ )
1310
+ );
1311
+ } catch (error) {
1312
+ getState().log(
1313
+ 2,
1314
+ `GET /install_dialog: ${error.message || "unknown error"}`
1315
+ );
1316
+ return res.status(500).json({ error: error.message || "unknown error" });
1317
+ }
1318
+ })
1319
+ );
1320
+
1321
+ const doInstall = async (req, res, version, runPull) => {
1322
+ if (db.getTenantSchema() !== db.connectObj.default_schema) {
1323
+ req.flash("error", req.__("Not possible for tenant"));
1324
+ res.redirect("/admin");
1325
+ } else {
1326
+ res.write(
1327
+ version === "latest"
1328
+ ? req.__("Starting upgrade, please wait...\n")
1329
+ : req.__("Installing %s, please wait...\n", version)
1330
+ );
1331
+ const child = spawn(
1332
+ "npm",
1333
+ ["install", "-g", `@saltcorn/cli@${version}`, "--unsafe"],
1334
+ {
1335
+ stdio: ["ignore", "pipe", "pipe"],
1336
+ }
1337
+ );
1338
+ child.stdout.on("data", (data) => {
1339
+ res.write(data);
1340
+ });
1341
+ child.stderr?.on("data", (data) => {
1342
+ res.write(data);
1343
+ });
1344
+ child.on("exit", async function (code, signal) {
1345
+ if (code === 0 && runPull) {
1346
+ res.write(req.__("Pulling the cordova-builder docker image...") + "\n");
1347
+ const pullCode = await pullCordovaBuilder(req, res);
1348
+ res.write(req.__("Pull done with code %s", pullCode) + "\n");
1349
+ }
1350
+ res.end(
1351
+ version === "latest"
1352
+ ? req.__(
1353
+ `Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
1354
+ )
1355
+ : req.__(
1356
+ `Install done with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
1357
+ )
1358
+ );
1359
+ setTimeout(() => {
1360
+ getState().processSend("RestartServer");
1361
+ process.exit(0);
1362
+ }, 100);
1363
+ });
1364
+ }
1365
+ };
1366
+
1367
+ router.post("/install", isAdmin, async (req, res) => {
1368
+ const { version } = req.body;
1369
+ await doInstall(req, res, version, false);
1370
+ });
1371
+
1225
1372
  /**
1226
1373
  * Do Upgrade
1227
1374
  * @name post/upgrade
@@ -1232,43 +1379,7 @@ router.post(
1232
1379
  "/upgrade",
1233
1380
  isAdmin,
1234
1381
  error_catcher(async (req, res) => {
1235
- if (db.getTenantSchema() !== db.connectObj.default_schema) {
1236
- req.flash("error", req.__("Not possible for tenant"));
1237
- res.redirect("/admin");
1238
- } else {
1239
- res.write(req.__("Starting upgrade, please wait...\n"));
1240
- const child = spawn(
1241
- "npm",
1242
- ["install", "-g", "@saltcorn/cli@latest", "--unsafe"],
1243
- {
1244
- stdio: ["ignore", "pipe", "pipe"],
1245
- }
1246
- );
1247
- child.stdout.on("data", (data) => {
1248
- res.write(data);
1249
- });
1250
- child.stderr?.on("data", (data) => {
1251
- res.write(data);
1252
- });
1253
- child.on("exit", async function (code, signal) {
1254
- if (code === 0) {
1255
- res.write(
1256
- req.__("Pulling the cordova-builder docker image...") + "\n"
1257
- );
1258
- const pullCode = await pullCordovaBuilder(req, res);
1259
- res.write(req.__("Pull done with code %s", pullCode) + "\n");
1260
- }
1261
- res.end(
1262
- req.__(
1263
- `Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
1264
- )
1265
- );
1266
- setTimeout(() => {
1267
- getState().processSend("RestartServer");
1268
- process.exit(0);
1269
- }, 100);
1270
- });
1271
- }
1382
+ await doInstall(req, res, "latest", true);
1272
1383
  })
1273
1384
  );
1274
1385
  /**
package/routes/api.js CHANGED
@@ -260,6 +260,12 @@ router.get(
260
260
  );
261
261
  if (!table) {
262
262
  getState().log(3, `API get ${tableName} table not found`);
263
+ getState().log(
264
+ 6,
265
+ `API get failure additonal info: URL=${req.originalUrl}${
266
+ getState().getConfig("log_ip_address", false) ? ` IP=${req.ip}` : ""
267
+ }`
268
+ );
263
269
  res.status(404).json({ error: req.__("Not found") });
264
270
  return;
265
271
  }
@@ -304,6 +304,18 @@ const viewsList = async (
304
304
  ? `set_state_field('_sortby', 'name', this)`
305
305
  : undefined,
306
306
  },
307
+ {
308
+ label: "",
309
+ key: (r) =>
310
+ r.id && r.viewtemplateObj?.configuration_workflow
311
+ ? link(
312
+ `/viewedit/config/${encodeURIComponent(
313
+ r.name
314
+ )}${on_done_redirect_str}`,
315
+ req.__("Configure")
316
+ )
317
+ : "",
318
+ },
307
319
  ...(tagId
308
320
  ? []
309
321
  : [
@@ -340,18 +352,6 @@ const viewsList = async (
340
352
  ? editViewRoleForm(row, roles, req, on_done_redirect_str)
341
353
  : "admin",
342
354
  },
343
- {
344
- label: "",
345
- key: (r) =>
346
- r.id && r.viewtemplateObj?.configuration_workflow
347
- ? link(
348
- `/viewedit/config/${encodeURIComponent(
349
- r.name
350
- )}${on_done_redirect_str}`,
351
- req.__("Configure")
352
- )
353
- : "",
354
- },
355
355
  !tagId
356
356
  ? {
357
357
  label: "",
@@ -428,13 +428,6 @@ const page_dropdown = (page, req) =>
428
428
  },
429
429
  '<i class="fas fa-running"></i>&nbsp;' + req.__("Run")
430
430
  ),
431
- a(
432
- {
433
- class: "dropdown-item",
434
- href: `/pageedit/edit-properties/${encodeURIComponent(page.name)}`,
435
- },
436
- '<i class="fas fa-edit"></i>&nbsp;' + req.__("Edit properties")
437
- ),
438
431
  post_dropdown_item(
439
432
  `/pageedit/add-to-menu/${page.id}`,
440
433
  '<i class="fas fa-bars"></i>&nbsp;' + req.__("Add to menu"),
@@ -507,6 +500,22 @@ const getPageList = async (
507
500
  label: req.__("Name"),
508
501
  key: (r) => link(`/page/${encodeURIComponent(r.name)}`, r.name),
509
502
  },
503
+ {
504
+ label: "",
505
+ key: (r) =>
506
+ link(
507
+ `/pageedit/edit/${encodeURIComponent(r.name)}`,
508
+ req.__("Configure")
509
+ ),
510
+ },
511
+ {
512
+ label: "",
513
+ key: (r) =>
514
+ link(
515
+ `/pageedit/edit-properties/${encodeURIComponent(r.name)}`,
516
+ req.__("Edit")
517
+ ),
518
+ },
510
519
  ...(tagId
511
520
  ? []
512
521
  : [
@@ -522,11 +531,7 @@ const getPageList = async (
522
531
  label: req.__("Role to access"),
523
532
  key: (row) => editPageRoleForm(row, roles, req),
524
533
  },
525
- {
526
- label: req.__("Edit"),
527
- key: (r) =>
528
- link(`/pageedit/edit/${encodeURIComponent(r.name)}`, req.__("Edit")),
529
- },
534
+
530
535
  !tagId
531
536
  ? {
532
537
  label: "",
@@ -600,6 +605,11 @@ const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
600
605
  },
601
606
  '<i class="fas fa-undo-alt"></i>&nbsp;' + req.__("Restore")
602
607
  ),
608
+ post_dropdown_item(
609
+ `/actions/clone/${trigger.id}`,
610
+ '<i class="far fa-copy"></i>&nbsp;' + req.__("Duplicate"),
611
+ req
612
+ ),
603
613
  div({ class: "dropdown-divider" }),
604
614
 
605
615
  post_dropdown_item(
@@ -634,6 +644,14 @@ const getTriggerList = async (
634
644
  return mkTable(
635
645
  [
636
646
  { label: req.__("Name"), key: "name" },
647
+ {
648
+ label: req.__("Test run"),
649
+ key: (r) => link(`/actions/testrun/${r.id}`, req.__("Test run")),
650
+ },
651
+ {
652
+ label: req.__("Configure"),
653
+ key: (r) => link(`/actions/configure/${r.id}`, req.__("Configure")),
654
+ },
637
655
  ...(tagId
638
656
  ? []
639
657
  : [
@@ -667,14 +685,6 @@ const getTriggerList = async (
667
685
  ? a({ href: `/table/${r.table_name}` }, r.table_name)
668
686
  : r.channel,
669
687
  },
670
- {
671
- label: req.__("Test run"),
672
- key: (r) => link(`/actions/testrun/${r.id}`, req.__("Test run")),
673
- },
674
- {
675
- label: req.__("Configure"),
676
- key: (r) => link(`/actions/configure/${r.id}`, req.__("Configure")),
677
- },
678
688
  !tagId
679
689
  ? {
680
690
  label: "",
package/routes/fields.js CHANGED
@@ -84,6 +84,10 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
84
84
  sublabel: req.__("Name of the field"),
85
85
  type: "String",
86
86
  attributes: { autofocus: true },
87
+ help: {
88
+ topic: "Field label",
89
+ context: {},
90
+ },
87
91
  validator(s) {
88
92
  if (!s || s === "") return req.__("Missing label");
89
93
  if (!id && existing_names.includes(Field.labelToName(s)))
@@ -104,6 +108,10 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
104
108
  "The type determines the kind of data that can be stored in the field"
105
109
  ),
106
110
  input_type: "select",
111
+ help: {
112
+ topic: "Field types",
113
+ context: {},
114
+ },
107
115
  options: isPrimary
108
116
  ? primaryTypes
109
117
  : getState().type_names.concat(fkey_opts || []),
@@ -255,6 +263,7 @@ const fieldFlow = (req) =>
255
263
  expression = "__aggregation";
256
264
  attributes.agg_relation = context.agg_relation;
257
265
  attributes.agg_field = context.agg_field;
266
+ attributes.agg_order_by = context.agg_order_by;
258
267
  attributes.aggwhere = context.aggwhere;
259
268
  attributes.aggregate = context.aggregate;
260
269
  const [table, ref] = context.agg_relation.split(".");
@@ -435,46 +444,64 @@ const fieldFlow = (req) =>
435
444
 
436
445
  const { child_field_list, child_relations } =
437
446
  await table.get_child_relations(true);
438
- const agg_field_opts = child_relations.map(
439
- ({ table, key_field, through }) => {
440
- const aggKey =
441
- (through ? `${through.name}->` : "") +
442
- `${table.name}.${key_field.name}`;
443
- aggStatOptions[aggKey] = [
444
- "Count",
445
- "CountUnique",
446
- "Avg",
447
- "Sum",
448
- "Max",
449
- "Min",
450
- "Array_Agg",
451
- ];
452
- table.fields.forEach((f) => {
453
- if (f.type && f.type.name === "Date") {
454
- aggStatOptions[aggKey].push(`Latest ${f.name}`);
455
- aggStatOptions[aggKey].push(`Earliest ${f.name}`);
456
- }
457
- });
458
- return {
459
- name: `agg_field`,
460
- label: req.__("On Field"),
461
- type: "String",
462
- required: true,
463
- attributes: {
464
- options: table.fields
465
- .filter((f) => !f.calculated || f.stored)
466
- .map((f) => ({
467
- label: f.name,
468
- name: `${f.name}@${f.type_name}`,
469
- })),
470
- },
471
- showIf: {
472
- agg_relation: aggKey,
473
- expression_type: "Aggregation",
474
- },
475
- };
476
- }
477
- );
447
+ const agg_field_opts = [];
448
+ const agg_order_opts = [];
449
+ child_relations.forEach(({ table, key_field, through }) => {
450
+ const aggKey =
451
+ (through ? `${through.name}->` : "") +
452
+ `${table.name}.${key_field.name}`;
453
+ aggStatOptions[aggKey] = [
454
+ "Count",
455
+ "CountUnique",
456
+ "Avg",
457
+ "Sum",
458
+ "Max",
459
+ "Min",
460
+ "Array_Agg",
461
+ ];
462
+ table.fields.forEach((f) => {
463
+ if (f.type && f.type.name === "Date") {
464
+ aggStatOptions[aggKey].push(`Latest ${f.name}`);
465
+ aggStatOptions[aggKey].push(`Earliest ${f.name}`);
466
+ }
467
+ });
468
+ agg_field_opts.push({
469
+ name: `agg_field`,
470
+ label: req.__("On Field"),
471
+ type: "String",
472
+ required: true,
473
+ attributes: {
474
+ options: table.fields
475
+ .filter((f) => !f.calculated || f.stored)
476
+ .map((f) => ({
477
+ label: f.name,
478
+ name: `${f.name}@${f.type_name}`,
479
+ })),
480
+ },
481
+ showIf: {
482
+ agg_relation: aggKey,
483
+ expression_type: "Aggregation",
484
+ },
485
+ });
486
+ agg_order_opts.push({
487
+ name: `agg_order_by`,
488
+ label: req.__("Order by"),
489
+ type: "String",
490
+ attributes: {
491
+ options: table.fields
492
+ .filter((f) => !f.calculated || f.stored)
493
+ .map((f) => ({
494
+ label: f.name,
495
+ name: f.name,
496
+ })),
497
+ },
498
+ showIf: {
499
+ agg_relation: aggKey,
500
+ expression_type: "Aggregation",
501
+ aggregate: "Array_Agg",
502
+ },
503
+ });
504
+ });
478
505
  return new Form({
479
506
  fields: [
480
507
  {
@@ -520,6 +547,7 @@ const fieldFlow = (req) =>
520
547
  required: false,
521
548
  showIf: { expression_type: "Aggregation" },
522
549
  },
550
+ ...agg_order_opts,
523
551
  {
524
552
  name: "model",
525
553
  label: req.__("Model"),
@@ -1129,7 +1157,7 @@ router.post(
1129
1157
  const jf = table.getField(ref);
1130
1158
  const jtable = Table.findOne(jf.reftable_name);
1131
1159
  const jrow = await jtable.getRow(
1132
- { [jtable.pk_name]: row[ref] },
1160
+ { [jtable.pk_name]: row[ref]?.[jtable.pk_name] || row[ref] },
1133
1161
  { forUser: req.user, forPublic: !req.user }
1134
1162
  );
1135
1163
  row[ref] = jrow;
@@ -1157,7 +1185,7 @@ router.post(
1157
1185
  else res.send(fv.run(result, req, { row, ...configuration }));
1158
1186
  } catch (e) {
1159
1187
  console.error("show-calculated error", e);
1160
- return res.status(400).send(`Error: ${e.message}`);
1188
+ return res.status(200).send(``);
1161
1189
  }
1162
1190
  })
1163
1191
  );
@@ -459,8 +459,10 @@ const welcome_page = async (req) => {
459
459
  viewCard(views, req),
460
460
  pageCard(pages, req),
461
461
  ],
462
+ class: "welcome-page-row1",
462
463
  },
463
464
  {
465
+ class: "welcome-page-row2",
464
466
  besides: [
465
467
  {
466
468
  type: "card",
package/routes/index.js CHANGED
@@ -36,6 +36,7 @@ const roleadmin = require("../auth/roleadmin");
36
36
  const tags = require("./tags");
37
37
  const tagentries = require("./tag_entries");
38
38
  const diagram = require("./diagram");
39
+ const registry = require("./registry");
39
40
  const sync = require("./sync");
40
41
 
41
42
  module.exports =
@@ -78,5 +79,6 @@ module.exports =
78
79
  app.use("/tag", tags);
79
80
  app.use("/tag-entries", tagentries);
80
81
  app.use("/diagram", diagram);
82
+ app.use("/registry-editor", registry);
81
83
  app.use("/sync", sync);
82
84
  };