@saltcorn/server 1.1.0-beta.9 → 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");
@@ -1990,9 +1969,9 @@ router.get(
1990
1969
  });
1991
1970
  })
1992
1971
  );
1993
- const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
1972
+ const buildDialogScript = (capacitorBuilderAvailable, isSbadmin2) =>
1994
1973
  `<script>
1995
- var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1974
+ var capacitorBuilderAvailable = ${capacitorBuilderAvailable};
1996
1975
  var isSbadmin2 = ${isSbadmin2};
1997
1976
  function showEntrySelect(type) {
1998
1977
  for( const currentType of ["view", "page", "pagegroup"]) {
@@ -2017,11 +1996,26 @@ const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
2017
1996
  function handleMessages() {
2018
1997
  notifyAlert("Building the app, please wait.", true)
2019
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
+ `)}
2020
2014
  </script>`;
2021
2015
 
2022
2016
  const imageAvailable = async () => {
2023
2017
  try {
2024
- const image = new Docker().getImage("saltcorn/cordova-builder");
2018
+ const image = new Docker().getImage("saltcorn/capacitor-builder");
2025
2019
  await image.inspect();
2026
2020
  return true;
2027
2021
  } catch (e) {
@@ -2404,9 +2398,15 @@ router.get(
2404
2398
  class: "form-control",
2405
2399
  name: "appVersion",
2406
2400
  id: "appVersionInputId",
2407
- placeholder: "1.0.0",
2401
+ placeholder: "0.0.1",
2408
2402
  value: builderSettings.appVersion || "",
2409
- })
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
+ )
2410
2410
  )
2411
2411
  ),
2412
2412
  // server url
@@ -2817,10 +2817,10 @@ router.get(
2817
2817
  div(
2818
2818
  label(
2819
2819
  { class: "form-label fw-bold" },
2820
- req.__("Cordova builder") +
2820
+ req.__("Capacitor builder") +
2821
2821
  a(
2822
2822
  {
2823
- href: "javascript:ajax_modal('/admin/help/Cordova Builder?')",
2823
+ href: "javascript:ajax_modal('/admin/help/Capacitor Builder?')",
2824
2824
  },
2825
2825
  i({ class: "fas fa-question-circle ps-1" })
2826
2826
  )
@@ -2848,9 +2848,8 @@ router.get(
2848
2848
  { class: "col-sm-4" },
2849
2849
  button(
2850
2850
  {
2851
- id: "pullCordovaBtnId",
2852
2851
  type: "button",
2853
- onClick: `pull_cordova_builder(this);`,
2852
+ onClick: `pull_capacitor_builder(this);`,
2854
2853
  class: "btn btn-warning",
2855
2854
  },
2856
2855
  req.__("pull")
@@ -2858,7 +2857,7 @@ router.get(
2858
2857
  span(
2859
2858
  {
2860
2859
  role: "button",
2861
- onClick: "check_cordova_builder()",
2860
+ onClick: "check_capacitor_builder()",
2862
2861
  },
2863
2862
  span({ class: "ps-3" }, req.__("refresh")),
2864
2863
  i({ class: "ps-2 fas fa-undo" })
@@ -3270,6 +3269,16 @@ router.post(
3270
3269
  error: req.__("Please enter a valid server URL."),
3271
3270
  });
3272
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
+ }
3273
3282
  if (iOSPlatform && !provisioningProfile) {
3274
3283
  return res.json({
3275
3284
  error: req.__(
@@ -3348,36 +3357,37 @@ router.post(
3348
3357
  });
3349
3358
  const childOutputs = [];
3350
3359
  child.stdout.on("data", (data) => {
3351
- // console.log(data.toString());
3352
- if (data) childOutputs.push(data.toString());
3360
+ const outMsg = data.toString();
3361
+ getState().log(5, outMsg);
3362
+ if (data) childOutputs.push(outMsg);
3353
3363
  });
3354
3364
  child.stderr.on("data", (data) => {
3355
- // console.log(data.toString());
3356
- 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);
3357
3368
  });
3358
3369
  child.on("exit", (exitCode, signal) => {
3359
3370
  const logFile = exitCode === 0 ? "logs.txt" : "error_logs.txt";
3360
- fs.writeFile(
3361
- path.join(buildDir, logFile),
3362
- childOutputs.join("\n"),
3363
- async (error) => {
3364
- if (error) {
3365
- console.log(`unable to write '${logFile}' to '${buildDir}'`);
3366
- console.log(error);
3367
- } else {
3368
- // no transaction, '/build-mobile-app/finished' filters for valid attributes
3369
- await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3370
- }
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);
3371
3379
  }
3372
- );
3380
+ });
3373
3381
  });
3374
3382
  child.on("error", (msg) => {
3375
3383
  const message = msg.message ? msg.message : msg.code;
3376
3384
  const stack = msg.stack ? msg.stack : "";
3377
3385
  const logFile = "error_logs.txt";
3386
+ const errMsg = [message, stack].join("\n");
3387
+ getState().log(5, msg);
3378
3388
  fs.writeFile(
3379
3389
  path.join(buildDir, "error_logs.txt"),
3380
- [message, stack].join("\n"),
3390
+ errMsg,
3381
3391
  async (error) => {
3382
3392
  if (error) {
3383
3393
  console.log(`unable to write logFile to '${buildDir}'`);
@@ -3393,13 +3403,13 @@ router.post(
3393
3403
  );
3394
3404
 
3395
3405
  router.post(
3396
- "/mobile-app/pull-cordova-builder",
3406
+ "/mobile-app/pull-capacitor-builder",
3397
3407
  isAdmin,
3398
3408
  error_catcher(async (req, res) => {
3399
3409
  const state = getState();
3400
3410
  const child = spawn(
3401
3411
  `${process.env.DOCKER_BIN ? `${process.env.DOCKER_BIN}/` : ""}docker`,
3402
- ["pull", "saltcorn/cordova-builder:latest"],
3412
+ ["pull", `saltcorn/capacitor-builder:${state.scVersion}`],
3403
3413
  {
3404
3414
  stdio: ["ignore", "pipe", "pipe"],
3405
3415
  cwd: ".",
@@ -3414,11 +3424,11 @@ router.post(
3414
3424
  child.on("exit", (exitCode, signal) => {
3415
3425
  state.log(
3416
3426
  2,
3417
- `"pull cordova-builder exit with code: ${exitCode} and signal: ${signal}`
3427
+ `"pull capacitor-builder exit with code: ${exitCode} and signal: ${signal}`
3418
3428
  );
3419
3429
  });
3420
3430
  child.on("error", (msg) => {
3421
- state.log(1, `pull cordova-builder error: ${msg}`);
3431
+ state.log(1, `pull capacitor-builder error: ${msg}`);
3422
3432
  });
3423
3433
 
3424
3434
  res.json({});
@@ -3426,7 +3436,7 @@ router.post(
3426
3436
  );
3427
3437
 
3428
3438
  router.get(
3429
- "/mobile-app/check-cordova-builder",
3439
+ "/mobile-app/check-capacitor-builder",
3430
3440
  isAdmin,
3431
3441
  error_catcher(async (req, res) => {
3432
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);
package/routes/list.js CHANGED
@@ -26,6 +26,7 @@ const {
26
26
  const Table = require("@saltcorn/data/models/table");
27
27
  const { isAdmin, error_catcher } = require("./utils");
28
28
  const moment = require("moment");
29
+ const { getState } = require("@saltcorn/data/db/state");
29
30
 
30
31
  /**
31
32
  * @type {object}
@@ -267,9 +268,11 @@ router.get(
267
268
  clipboard: false,
268
269
  cellClick: "__delete_tabulator_row",
269
270
  });
271
+ const isDark = getState().getLightDarkMode(req.user) === "dark";
270
272
  res.sendWrap(
271
273
  {
272
274
  title: req.__(`%s data table`, table.name),
275
+ requestFluidLayout: true,
273
276
  headers: [
274
277
  //jsgrid - grid editor external component
275
278
  {
@@ -295,6 +298,13 @@ router.get(
295
298
  {
296
299
  css: `/static_assets/${db.connectObj.version_tag}/flatpickr.min.css`,
297
300
  },
301
+ ...(isDark
302
+ ? [
303
+ {
304
+ css: `/static_assets/${db.connectObj.version_tag}/flatpickr-dark.css`,
305
+ },
306
+ ]
307
+ : []),
298
308
  ],
299
309
  },
300
310
  {
@@ -426,7 +436,13 @@ router.get(
426
436
  ),
427
437
  div({ id: "jsGridNotify" }),
428
438
 
429
- div({ id: "jsGrid" })
439
+ div({
440
+ id: "jsGrid",
441
+ class:
442
+ getState().getLightDarkMode(req.user) === "dark"
443
+ ? "table-dark"
444
+ : undefined,
445
+ })
430
446
  ),
431
447
  },
432
448
  ],