@saltcorn/server 1.1.0-beta.8 → 1.1.0

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
@@ -91,10 +91,7 @@ const {
91
91
  } = require("../markup/admin.js");
92
92
  const packagejson = require("../package.json");
93
93
  const Form = require("@saltcorn/data/models/form");
94
- const {
95
- get_latest_npm_version,
96
- isFixedConfig,
97
- } = require("@saltcorn/data/models/config");
94
+ const { get_latest_npm_version } = require("@saltcorn/data/models/config");
98
95
  const { getMailTransport } = require("@saltcorn/data/models/email");
99
96
  const {
100
97
  getBaseDomain,
@@ -107,7 +104,10 @@ const PageGroup = require("@saltcorn/data/models/page_group");
107
104
  const { getConfigFile } = require("@saltcorn/data/db/connect");
108
105
  const os = require("os");
109
106
  const Page = require("@saltcorn/data/models/page");
110
- const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
107
+ const {
108
+ getSafeSaltcornCmd,
109
+ getFetchProxyOptions,
110
+ } = require("@saltcorn/data/utils");
111
111
  const stream = require("stream");
112
112
  const Crash = require("@saltcorn/data/models/crash");
113
113
  const { get_help_markup } = require("../help/index.js");
@@ -155,6 +155,7 @@ admin_config_route({
155
155
  field_names: [
156
156
  "site_name",
157
157
  "timezone",
158
+ "default_locale",
158
159
  "base_url",
159
160
  ...(getConfigFile() ? ["multitenancy_enabled"] : []),
160
161
  { section_header: "Logo image" },
@@ -534,6 +535,7 @@ router.get(
534
535
  {},
535
536
  { orderBy: "created", orderDesc: true, fields: ["id", "created", "hash"] }
536
537
  );
538
+ const locale = getState().getConfig("default_locale", "en");
537
539
  send_admin_page({
538
540
  res,
539
541
  req,
@@ -555,9 +557,11 @@ router.get(
555
557
  )}`,
556
558
  target: "_blank",
557
559
  },
558
- `${localeDateTime(snap.created)} (${moment(
559
- snap.created
560
- ).fromNow()})`
560
+ `${localeDateTime(
561
+ snap.created,
562
+ {},
563
+ locale
564
+ )} (${moment(snap.created).fromNow()})`
561
565
  )
562
566
  )
563
567
  )
@@ -595,6 +599,7 @@ router.get(
595
599
  error_catcher(async (req, res) => {
596
600
  const { type, name } = req.params;
597
601
  const snaps = await Snapshot.entity_history(type, name);
602
+ const locale = getState().getConfig("default_locale", "en");
598
603
  res.set("Page-Title", `Restore ${text(name)}`);
599
604
  res.send(
600
605
  mkTable(
@@ -602,7 +607,9 @@ router.get(
602
607
  {
603
608
  label: req.__("When"),
604
609
  key: (r) =>
605
- `${localeDateTime(r.created)} (${moment(r.created).fromNow()})`,
610
+ `${localeDateTime(r.created, {}, locale)} (${moment(
611
+ r.created
612
+ ).fromNow()})`,
606
613
  },
607
614
 
608
615
  {
@@ -981,39 +988,6 @@ router.post(
981
988
  })
982
989
  );
983
990
 
984
- router.post(
985
- "/save-config",
986
- isAdmin,
987
- error_catcher(async (req, res) => {
988
- const state = getState();
989
-
990
- //TODO check this is a config key
991
- const validKeyName = (k) =>
992
- k !== "_csrf" && k !== "constructor" && k !== "__proto__";
993
-
994
- for (const [k, v] of Object.entries(req.body)) {
995
- if (!isFixedConfig(k) && typeof v !== "undefined" && validKeyName(k)) {
996
- //TODO read value from type
997
- await state.setConfig(k, v);
998
- }
999
- }
1000
-
1001
- // checkboxes that are false are not sent in post body. Check here
1002
- const { boolcheck } = req.query;
1003
- const boolchecks =
1004
- typeof boolcheck === "undefined"
1005
- ? []
1006
- : Array.isArray(boolcheck)
1007
- ? boolcheck
1008
- : [boolcheck];
1009
- for (const k of boolchecks) {
1010
- if (typeof req.body[k] === "undefined" && validKeyName(k))
1011
- await state.setConfig(k, false);
1012
- }
1013
- res.json({ success: "ok" });
1014
- })
1015
- );
1016
-
1017
991
  /**
1018
992
  * Do Auto backup now
1019
993
  */
@@ -1284,10 +1258,14 @@ router.post(
1284
1258
  })
1285
1259
  );
1286
1260
 
1287
- const pullCordovaBuilder = (req, res) => {
1288
- const child = spawn("docker", ["pull", "saltcorn/cordova-builder"], {
1289
- stdio: ["ignore", "pipe", "pipe"],
1290
- });
1261
+ const pullCapacitorBuilder = (req, res, version) => {
1262
+ const child = spawn(
1263
+ "docker",
1264
+ ["pull", `saltcorn/capacitor-builder:${version}`],
1265
+ {
1266
+ stdio: ["ignore", "pipe", "pipe"],
1267
+ }
1268
+ );
1291
1269
  return new Promise((resolve, reject) => {
1292
1270
  child.stdout.on("data", (data) => {
1293
1271
  res.write(data);
@@ -1359,7 +1337,8 @@ router.get(
1359
1337
  error_catcher(async (req, res) => {
1360
1338
  try {
1361
1339
  const pkgInfo = await npmFetch.json(
1362
- "https://registry.npmjs.org/@saltcorn/cli"
1340
+ "https://registry.npmjs.org/@saltcorn/cli",
1341
+ getFetchProxyOptions()
1363
1342
  );
1364
1343
  if (!pkgInfo?.versions)
1365
1344
  throw new Error(req.__("Unable to fetch versions"));
@@ -1543,9 +1522,9 @@ const doInstall = async (req, res, version, deepClean, runPull) => {
1543
1522
  }
1544
1523
  if (runPull) {
1545
1524
  res.write(
1546
- req.__("Pulling the cordova-builder docker image...") + "\n"
1525
+ req.__("Pulling the capacitor-builder docker image...") + "\n"
1547
1526
  );
1548
- const pullCode = await pullCordovaBuilder(req, res);
1527
+ const pullCode = await pullCapacitorBuilder(req, res, version);
1549
1528
  res.write(req.__("Pull done with code %s", pullCode) + "\n");
1550
1529
  if (pullCode === 0) {
1551
1530
  res.write(req.__("Pruning docker...") + "\n");
@@ -1741,8 +1720,8 @@ router.post(
1741
1720
 
1742
1721
  let altname = await tenant_letsencrypt_name(subdomain);
1743
1722
 
1744
- if (!altname || domain) {
1745
- req.json({ error: "Set Base URL for both tenant and root first." });
1723
+ if (!altname || !domain) {
1724
+ res.json({ error: "Set Base URL for both tenant and root first." });
1746
1725
  return;
1747
1726
  }
1748
1727
 
@@ -1759,13 +1738,14 @@ router.post(
1759
1738
 
1760
1739
  await greenlock.sites.add({
1761
1740
  subject: altname,
1741
+ altnames: [altname],
1762
1742
  });
1763
1743
  // letsencrypt
1764
1744
  const tenant_letsencrypt_sites = getState().getConfig(
1765
1745
  "tenant_letsencrypt_sites",
1766
1746
  []
1767
1747
  );
1768
- await getState().setConfig(tenant_letsencrypt_sites, [
1748
+ await getState().setConfig("tenant_letsencrypt_sites", [
1769
1749
  altname,
1770
1750
  ...tenant_letsencrypt_sites,
1771
1751
  ]);
@@ -1775,12 +1755,10 @@ router.post(
1775
1755
  notify: "Certificate added, please restart server",
1776
1756
  });
1777
1757
  } catch (e) {
1778
- req.flash("error", e.message);
1779
- res.redirect("/useradmin/ssl");
1758
+ res.json({ error: e.message });
1780
1759
  }
1781
1760
  } else {
1782
- req.flash("error", req.__("Not possible for tenant"));
1783
- res.redirect("/useradmin/ssl");
1761
+ res.json({ error: req.__("Not possible for tenant") });
1784
1762
  }
1785
1763
  })
1786
1764
  );
@@ -1849,7 +1827,7 @@ router.post(
1849
1827
  "tenant_letsencrypt_sites",
1850
1828
  []
1851
1829
  );
1852
- await getState().setConfig(tenant_letsencrypt_sites, [
1830
+ await getState().setConfig("tenant_letsencrypt_sites", [
1853
1831
  ...altnames,
1854
1832
  ...tenant_letsencrypt_sites,
1855
1833
  ]);
@@ -1991,9 +1969,9 @@ router.get(
1991
1969
  });
1992
1970
  })
1993
1971
  );
1994
- const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
1972
+ const buildDialogScript = (capacitorBuilderAvailable, isSbadmin2) =>
1995
1973
  `<script>
1996
- var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1974
+ var capacitorBuilderAvailable = ${capacitorBuilderAvailable};
1997
1975
  var isSbadmin2 = ${isSbadmin2};
1998
1976
  function showEntrySelect(type) {
1999
1977
  for( const currentType of ["view", "page", "pagegroup"]) {
@@ -2018,11 +1996,26 @@ const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
2018
1996
  function handleMessages() {
2019
1997
  notifyAlert("Building the app, please wait.", true)
2020
1998
  }
1999
+ const versionPattern = /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/;
2000
+ ${domReady(`
2001
+ const versionInput = document.getElementById('appVersionInputId');
2002
+ if (versionInput) {
2003
+ versionInput.addEventListener('change', () => {
2004
+ const version = versionInput.value;
2005
+ if ((version !== '0.0.0' && versionPattern.test(version)) || version === "")
2006
+ versionInput.classList.remove('is-invalid');
2007
+ else
2008
+ versionInput.classList.add('is-invalid');
2009
+ });
2010
+ }
2011
+ else
2012
+ console.error('versionInput not found');
2013
+ `)}
2021
2014
  </script>`;
2022
2015
 
2023
2016
  const imageAvailable = async () => {
2024
2017
  try {
2025
- const image = new Docker().getImage("saltcorn/cordova-builder");
2018
+ const image = new Docker().getImage("saltcorn/capacitor-builder");
2026
2019
  await image.inspect();
2027
2020
  return true;
2028
2021
  } catch (e) {
@@ -2405,9 +2398,15 @@ router.get(
2405
2398
  class: "form-control",
2406
2399
  name: "appVersion",
2407
2400
  id: "appVersionInputId",
2408
- placeholder: "1.0.0",
2401
+ placeholder: "0.0.1",
2409
2402
  value: builderSettings.appVersion || "",
2410
- })
2403
+ }),
2404
+ div(
2405
+ { class: "invalid-feedback" },
2406
+ req.__(
2407
+ "Please enter a version in the format 'x.y.z' (e.g. 0.0.1 with numbers from 0 to 999) or leave it empty."
2408
+ )
2409
+ )
2411
2410
  )
2412
2411
  ),
2413
2412
  // server url
@@ -2818,10 +2817,10 @@ router.get(
2818
2817
  div(
2819
2818
  label(
2820
2819
  { class: "form-label fw-bold" },
2821
- req.__("Cordova builder") +
2820
+ req.__("Capacitor builder") +
2822
2821
  a(
2823
2822
  {
2824
- href: "javascript:ajax_modal('/admin/help/Cordova Builder?')",
2823
+ href: "javascript:ajax_modal('/admin/help/Capacitor Builder?')",
2825
2824
  },
2826
2825
  i({ class: "fas fa-question-circle ps-1" })
2827
2826
  )
@@ -2849,9 +2848,8 @@ router.get(
2849
2848
  { class: "col-sm-4" },
2850
2849
  button(
2851
2850
  {
2852
- id: "pullCordovaBtnId",
2853
2851
  type: "button",
2854
- onClick: `pull_cordova_builder(this);`,
2852
+ onClick: `pull_capacitor_builder(this);`,
2855
2853
  class: "btn btn-warning",
2856
2854
  },
2857
2855
  req.__("pull")
@@ -2859,7 +2857,7 @@ router.get(
2859
2857
  span(
2860
2858
  {
2861
2859
  role: "button",
2862
- onClick: "check_cordova_builder()",
2860
+ onClick: "check_capacitor_builder()",
2863
2861
  },
2864
2862
  span({ class: "ps-3" }, req.__("refresh")),
2865
2863
  i({ class: "ps-2 fas fa-undo" })
@@ -3271,6 +3269,16 @@ router.post(
3271
3269
  error: req.__("Please enter a valid server URL."),
3272
3270
  });
3273
3271
  }
3272
+ if (
3273
+ (appVersion && !/^\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(appVersion)) ||
3274
+ appVersion === "0.0.0"
3275
+ ) {
3276
+ return res.json({
3277
+ error: req.__(
3278
+ "Please enter a version in the format 'x.y.z' (e.g. 0.0.1 with numbers from 0 to 999) or leave it empty."
3279
+ ),
3280
+ });
3281
+ }
3274
3282
  if (iOSPlatform && !provisioningProfile) {
3275
3283
  return res.json({
3276
3284
  error: req.__(
@@ -3349,36 +3357,37 @@ router.post(
3349
3357
  });
3350
3358
  const childOutputs = [];
3351
3359
  child.stdout.on("data", (data) => {
3352
- // console.log(data.toString());
3353
- if (data) childOutputs.push(data.toString());
3360
+ const outMsg = data.toString();
3361
+ getState().log(5, outMsg);
3362
+ if (data) childOutputs.push(outMsg);
3354
3363
  });
3355
3364
  child.stderr.on("data", (data) => {
3356
- // console.log(data.toString());
3357
- childOutputs.push(data ? data.toString() : req.__("An error occurred"));
3365
+ const errMsg = data ? data.toString() : req.__("An error occurred");
3366
+ getState().log(5, errMsg);
3367
+ childOutputs.push(errMsg);
3358
3368
  });
3359
3369
  child.on("exit", (exitCode, signal) => {
3360
3370
  const logFile = exitCode === 0 ? "logs.txt" : "error_logs.txt";
3361
- fs.writeFile(
3362
- path.join(buildDir, logFile),
3363
- childOutputs.join("\n"),
3364
- async (error) => {
3365
- if (error) {
3366
- console.log(`unable to write '${logFile}' to '${buildDir}'`);
3367
- console.log(error);
3368
- } else {
3369
- // no transaction, '/build-mobile-app/finished' filters for valid attributes
3370
- await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3371
- }
3371
+ const exitMsg = childOutputs.join("\n");
3372
+ fs.writeFile(path.join(buildDir, logFile), exitMsg, async (error) => {
3373
+ if (error) {
3374
+ console.log(`unable to write '${logFile}' to '${buildDir}'`);
3375
+ console.log(error);
3376
+ } else {
3377
+ // no transaction, '/build-mobile-app/finished' filters for valid attributes
3378
+ await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3372
3379
  }
3373
- );
3380
+ });
3374
3381
  });
3375
3382
  child.on("error", (msg) => {
3376
3383
  const message = msg.message ? msg.message : msg.code;
3377
3384
  const stack = msg.stack ? msg.stack : "";
3378
3385
  const logFile = "error_logs.txt";
3386
+ const errMsg = [message, stack].join("\n");
3387
+ getState().log(5, msg);
3379
3388
  fs.writeFile(
3380
3389
  path.join(buildDir, "error_logs.txt"),
3381
- [message, stack].join("\n"),
3390
+ errMsg,
3382
3391
  async (error) => {
3383
3392
  if (error) {
3384
3393
  console.log(`unable to write logFile to '${buildDir}'`);
@@ -3394,13 +3403,13 @@ router.post(
3394
3403
  );
3395
3404
 
3396
3405
  router.post(
3397
- "/mobile-app/pull-cordova-builder",
3406
+ "/mobile-app/pull-capacitor-builder",
3398
3407
  isAdmin,
3399
3408
  error_catcher(async (req, res) => {
3400
3409
  const state = getState();
3401
3410
  const child = spawn(
3402
3411
  `${process.env.DOCKER_BIN ? `${process.env.DOCKER_BIN}/` : ""}docker`,
3403
- ["pull", "saltcorn/cordova-builder:latest"],
3412
+ ["pull", `saltcorn/capacitor-builder:${state.scVersion}`],
3404
3413
  {
3405
3414
  stdio: ["ignore", "pipe", "pipe"],
3406
3415
  cwd: ".",
@@ -3415,11 +3424,11 @@ router.post(
3415
3424
  child.on("exit", (exitCode, signal) => {
3416
3425
  state.log(
3417
3426
  2,
3418
- `"pull cordova-builder exit with code: ${exitCode} and signal: ${signal}`
3427
+ `"pull capacitor-builder exit with code: ${exitCode} and signal: ${signal}`
3419
3428
  );
3420
3429
  });
3421
3430
  child.on("error", (msg) => {
3422
- state.log(1, `pull cordova-builder error: ${msg}`);
3431
+ state.log(1, `pull capacitor-builder error: ${msg}`);
3423
3432
  });
3424
3433
 
3425
3434
  res.json({});
@@ -3427,7 +3436,7 @@ router.post(
3427
3436
  );
3428
3437
 
3429
3438
  router.get(
3430
- "/mobile-app/check-cordova-builder",
3439
+ "/mobile-app/check-capacitor-builder",
3431
3440
  isAdmin,
3432
3441
  error_catcher(async (req, res) => {
3433
3442
  const installed = await imageAvailable();
package/routes/api.js CHANGED
@@ -484,6 +484,57 @@ router.post(
484
484
  })
485
485
  );
486
486
 
487
+ /**
488
+ * Delete Table row by ID using POST
489
+ * @name delete/:tableName/:id
490
+ * @function
491
+ * @memberof module:routes/api~apiRouter
492
+ */
493
+ router.post(
494
+ "/:tableName/delete/:id",
495
+ // in case of primary key different from id - id will be string "undefined"
496
+ error_catcher(async (req, res, next) => {
497
+ const { tableName, id } = req.params;
498
+ const table = Table.findOne({ name: tableName });
499
+ if (!table) {
500
+ getState().log(3, `API DELETE ${tableName} not found`);
501
+ res.status(404).json({ error: req.__("Not found") });
502
+ return;
503
+ }
504
+ await passport.authenticate(
505
+ "api-bearer",
506
+ { session: false },
507
+ async function (err, user, info) {
508
+ if (accessAllowedWrite(req, user, table)) {
509
+ try {
510
+ if (id === "undefined") {
511
+ const pk_name = table.pk_name;
512
+ //const fields = table.getFields();
513
+ const row = req.body;
514
+ //readState(row, fields);
515
+ await table.deleteRows(
516
+ { [pk_name]: row[pk_name] },
517
+ user || req.user || { role_id: 100 }
518
+ );
519
+ } else
520
+ await table.deleteRows(
521
+ { id },
522
+ user || req.user || { role_id: 100 }
523
+ );
524
+ res.json({ success: true });
525
+ } catch (e) {
526
+ getState().log(2, `API DELETE ${table.name} error: ${e.message}`);
527
+ res.status(400).json({ error: e.message });
528
+ }
529
+ } else {
530
+ getState().log(3, `API DELETE ${table.name} not authorized`);
531
+ res.status(401).json({ error: req.__("Not authorized") });
532
+ }
533
+ }
534
+ )(req, res, next);
535
+ })
536
+ );
537
+
487
538
  /**
488
539
  * Update Table row directed by ID using POST
489
540
  * POST api/<table>/id
package/routes/config.js CHANGED
@@ -5,31 +5,9 @@
5
5
  */
6
6
  const Router = require("express-promise-router");
7
7
 
8
- const Field = require("@saltcorn/data/models/field");
9
- const File = require("@saltcorn/data/models/file");
10
- const Table = require("@saltcorn/data/models/table");
11
- const View = require("@saltcorn/data/models/view");
12
- const Form = require("@saltcorn/data/models/form");
13
- const { isAdmin, setTenant, error_catcher } = require("./utils.js");
8
+ const { isAdmin, error_catcher } = require("./utils.js");
14
9
  const { getState } = require("@saltcorn/data/db/state");
15
10
 
16
- const {
17
- mkTable,
18
- renderForm,
19
- link,
20
- post_btn,
21
- post_delete_btn,
22
- } = require("@saltcorn/markup");
23
- const {
24
- getConfig,
25
- setConfig,
26
- getAllConfigOrDefaults,
27
- deleteConfig,
28
- configTypes,
29
- isFixedConfig,
30
- } = require("@saltcorn/data/models/config");
31
- const { table, tbody, tr, th, td, div } = require("@saltcorn/markup/tags");
32
-
33
11
  /**
34
12
  * @type {object}
35
13
  * @const
@@ -52,7 +30,43 @@ router.post(
52
30
  error_catcher(async (req, res) => {
53
31
  const { key } = req.params;
54
32
  await getState().deleteConfig(key);
55
- req.flash("success", req.__(`Configuration key %s deleted`, key));
56
- res.redirect(`/admin`);
33
+ if (req.xhr) res.json({ success: "ok" });
34
+ else {
35
+ req.flash("success", req.__(`Configuration key %s deleted`, key));
36
+ res.redirect(`/admin`);
37
+ }
38
+ })
39
+ );
40
+
41
+ router.post(
42
+ "/save",
43
+ isAdmin,
44
+ error_catcher(async (req, res) => {
45
+ const state = getState();
46
+
47
+ //TODO check this is a config key
48
+ const validKeyName = (k) =>
49
+ k !== "_csrf" && k !== "constructor" && k !== "__proto__";
50
+
51
+ for (const [k, v] of Object.entries(req.body)) {
52
+ if (!state.isFixedConfig(k) && typeof v !== "undefined" && validKeyName(k)) {
53
+ //TODO read value from type
54
+ await state.setConfig(k, v);
55
+ }
56
+ }
57
+
58
+ // checkboxes that are false are not sent in post body. Check here
59
+ const { boolcheck } = req.query;
60
+ const boolchecks =
61
+ typeof boolcheck === "undefined"
62
+ ? []
63
+ : Array.isArray(boolcheck)
64
+ ? boolcheck
65
+ : [boolcheck];
66
+ for (const k of boolchecks) {
67
+ if (typeof req.body[k] === "undefined" && validKeyName(k))
68
+ await state.setConfig(k, false);
69
+ }
70
+ res.json({ success: "ok" });
57
71
  })
58
72
  );
@@ -81,6 +81,31 @@ const logSettingsForm = async (req) => {
81
81
  input_type: "date",
82
82
  attributes: { minDate: new Date(), maxDate: hoursFuture(24 * 7 * 2) },
83
83
  },
84
+ {
85
+ input_type: "section_header",
86
+ label: req.__("Delete old workflow runs with status after days"),
87
+ },
88
+ {
89
+ name: "delete_finished_workflows_days",
90
+ label: req.__("Finished"),
91
+ type: "Integer",
92
+ },
93
+ {
94
+ name: "delete_error_workflows_days",
95
+ label: req.__("Error"),
96
+ type: "Integer",
97
+ },
98
+ {
99
+ name: "delete_waiting_workflows_days",
100
+ label: req.__("Waiting"),
101
+ type: "Integer",
102
+ },
103
+
104
+ {
105
+ name: "delete_running_workflows_days",
106
+ label: req.__("Running"),
107
+ type: "Integer",
108
+ },
84
109
  {
85
110
  input_type: "section_header",
86
111
  label: req.__("Which events should be logged?"),
@@ -143,6 +168,10 @@ router.get(
143
168
  "next_weekly_event",
144
169
  {}
145
170
  );
171
+ ["error", "finished", "running", "waiting"].forEach((k) => {
172
+ let cfgk = `delete_${k}_workflows_days`;
173
+ form.values[cfgk] = getState().getConfig(cfgk);
174
+ });
146
175
 
147
176
  send_events_page({
148
177
  res,
@@ -348,6 +377,13 @@ router.post(
348
377
  delete form.values[k];
349
378
  }
350
379
  }
380
+ for (const status of ["error", "finished", "running", "waiting"]) {
381
+ let k = `delete_${status}_workflows_days`;
382
+ if (form.values[k]) {
383
+ await getState().setConfig(k, form.values[k]);
384
+ delete form.values[k];
385
+ }
386
+ }
351
387
 
352
388
  await getState().setConfig("event_log_settings", form.values);
353
389
 
@@ -424,6 +460,7 @@ router.get(
424
460
  error_catcher(async (req, res) => {
425
461
  const { id } = req.params;
426
462
  const ev = await EventLog.findOneWithUser(id);
463
+ const locale = getState().getConfig("default_locale", "en");
427
464
  send_events_page({
428
465
  res,
429
466
  req,
@@ -435,7 +472,10 @@ router.get(
435
472
  table(
436
473
  { class: "table eventlog" },
437
474
  tbody(
438
- tr(th(req.__("When")), td(localeDateTime(ev.occur_at))),
475
+ tr(
476
+ th(req.__("When")),
477
+ td(localeDateTime(ev.occur_at, {}, locale))
478
+ ),
439
479
  tr(th(req.__("Type")), td(ev.event_type)),
440
480
  tr(th(req.__("Channel")), td(ev.channel)),
441
481
  tr(th(req.__("User")), td(ev.email))
package/routes/fields.js CHANGED
@@ -301,6 +301,10 @@ const fieldFlow = (req) =>
301
301
  if (context.id) {
302
302
  const field = await Field.findOne({ id: context.id });
303
303
  try {
304
+ if (fldRow.label && field.label != fldRow.label) {
305
+ fldRow.name = Field.labelToName(fldRow.label);
306
+ }
307
+
304
308
  await field.update(fldRow);
305
309
  } catch (e) {
306
310
  return {
@@ -362,10 +366,12 @@ const fieldFlow = (req) =>
362
366
  name: req.__("Attributes"),
363
367
  contextField: "attributes",
364
368
  onlyWhen: (context) => {
365
- if (context.calculated) return false;
369
+ const type = getState().types[context.type];
370
+ if (context.calculated && !type?.setTypeAttributesForCalculatedFields)
371
+ return false;
372
+
366
373
  if (context.type === "File") return true;
367
374
  if (new Field(context).is_fkey) return false;
368
- const type = getState().types[context.type];
369
375
  if (!type) return false;
370
376
  const attrs = Field.getTypeAttributes(
371
377
  type.attributes,
@@ -549,7 +549,14 @@ const no_views_logged_in = async (req, res) => {
549
549
  * @returns {Promise<boolean>}
550
550
  */
551
551
  const get_config_response = async (role_id, res, req) => {
552
- const wrap = async (contents, homeCfg, title, description, no_menu) => {
552
+ const wrap = async (
553
+ contents,
554
+ homeCfg,
555
+ title,
556
+ description,
557
+ no_menu,
558
+ requestFluidLayout
559
+ ) => {
553
560
  if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
554
561
  else
555
562
  res.sendWrap(
@@ -558,6 +565,7 @@ const get_config_response = async (role_id, res, req) => {
558
565
  description: description || "",
559
566
  bodyClass: "page_" + db.sqlsanitize(homeCfg),
560
567
  no_menu,
568
+ requestFluidLayout,
561
569
  },
562
570
  contents
563
571
  );
@@ -578,7 +586,8 @@ const get_config_response = async (role_id, res, req) => {
578
586
  homeCfg,
579
587
  db_page.title,
580
588
  db_page.description,
581
- db_page.attributes?.no_menu
589
+ db_page.attributes?.no_menu,
590
+ db_page.attributes?.request_fluid_layout
582
591
  );
583
592
  else {
584
593
  const group = PageGroup.findOne({ name: homeCfg });
@@ -592,7 +601,8 @@ const get_config_response = async (role_id, res, req) => {
592
601
  homeCfg,
593
602
  eligible.title,
594
603
  eligible.description,
595
- eligible.attributes?.no_menu
604
+ eligible.attributes?.no_menu,
605
+ eligible.attributes?.request_fluid_layout
596
606
  );
597
607
  } else wrap(req.__("%s has no eligible page", group.name), homeCfg);
598
608
  } else res.redirect(homeCfg);