@saltcorn/server 1.1.0-beta.2 → 1.1.0-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.
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,
@@ -155,6 +152,7 @@ admin_config_route({
155
152
  field_names: [
156
153
  "site_name",
157
154
  "timezone",
155
+ "default_locale",
158
156
  "base_url",
159
157
  ...(getConfigFile() ? ["multitenancy_enabled"] : []),
160
158
  { section_header: "Logo image" },
@@ -534,6 +532,7 @@ router.get(
534
532
  {},
535
533
  { orderBy: "created", orderDesc: true, fields: ["id", "created", "hash"] }
536
534
  );
535
+ const locale = getState().getConfig("default_locale", "en");
537
536
  send_admin_page({
538
537
  res,
539
538
  req,
@@ -555,9 +554,11 @@ router.get(
555
554
  )}`,
556
555
  target: "_blank",
557
556
  },
558
- `${localeDateTime(snap.created)} (${moment(
559
- snap.created
560
- ).fromNow()})`
557
+ `${localeDateTime(
558
+ snap.created,
559
+ {},
560
+ locale
561
+ )} (${moment(snap.created).fromNow()})`
561
562
  )
562
563
  )
563
564
  )
@@ -595,6 +596,7 @@ router.get(
595
596
  error_catcher(async (req, res) => {
596
597
  const { type, name } = req.params;
597
598
  const snaps = await Snapshot.entity_history(type, name);
599
+ const locale = getState().getConfig("default_locale", "en");
598
600
  res.set("Page-Title", `Restore ${text(name)}`);
599
601
  res.send(
600
602
  mkTable(
@@ -602,7 +604,9 @@ router.get(
602
604
  {
603
605
  label: req.__("When"),
604
606
  key: (r) =>
605
- `${localeDateTime(r.created)} (${moment(r.created).fromNow()})`,
607
+ `${localeDateTime(r.created, {}, locale)} (${moment(
608
+ r.created
609
+ ).fromNow()})`,
606
610
  },
607
611
 
608
612
  {
@@ -981,39 +985,6 @@ router.post(
981
985
  })
982
986
  );
983
987
 
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
988
  /**
1018
989
  * Do Auto backup now
1019
990
  */
@@ -1284,10 +1255,14 @@ router.post(
1284
1255
  })
1285
1256
  );
1286
1257
 
1287
- const pullCordovaBuilder = (req, res) => {
1288
- const child = spawn("docker", ["pull", "saltcorn/cordova-builder"], {
1289
- stdio: ["ignore", "pipe", "pipe"],
1290
- });
1258
+ const pullCapacitorBuilder = (req, res, version) => {
1259
+ const child = spawn(
1260
+ "docker",
1261
+ ["pull", `saltcorn/capacitor-builder:${version}`],
1262
+ {
1263
+ stdio: ["ignore", "pipe", "pipe"],
1264
+ }
1265
+ );
1291
1266
  return new Promise((resolve, reject) => {
1292
1267
  child.stdout.on("data", (data) => {
1293
1268
  res.write(data);
@@ -1543,9 +1518,9 @@ const doInstall = async (req, res, version, deepClean, runPull) => {
1543
1518
  }
1544
1519
  if (runPull) {
1545
1520
  res.write(
1546
- req.__("Pulling the cordova-builder docker image...") + "\n"
1521
+ req.__("Pulling the capacitor-builder docker image...") + "\n"
1547
1522
  );
1548
- const pullCode = await pullCordovaBuilder(req, res);
1523
+ const pullCode = await pullCapacitorBuilder(req, res, version);
1549
1524
  res.write(req.__("Pull done with code %s", pullCode) + "\n");
1550
1525
  if (pullCode === 0) {
1551
1526
  res.write(req.__("Pruning docker...") + "\n");
@@ -1741,8 +1716,8 @@ router.post(
1741
1716
 
1742
1717
  let altname = await tenant_letsencrypt_name(subdomain);
1743
1718
 
1744
- if (!altname || domain) {
1745
- req.json({ error: "Set Base URL for both tenant and root first." });
1719
+ if (!altname || !domain) {
1720
+ res.json({ error: "Set Base URL for both tenant and root first." });
1746
1721
  return;
1747
1722
  }
1748
1723
 
@@ -1759,13 +1734,14 @@ router.post(
1759
1734
 
1760
1735
  await greenlock.sites.add({
1761
1736
  subject: altname,
1737
+ altnames: [altname],
1762
1738
  });
1763
1739
  // letsencrypt
1764
1740
  const tenant_letsencrypt_sites = getState().getConfig(
1765
1741
  "tenant_letsencrypt_sites",
1766
1742
  []
1767
1743
  );
1768
- await getState().setConfig(tenant_letsencrypt_sites, [
1744
+ await getState().setConfig("tenant_letsencrypt_sites", [
1769
1745
  altname,
1770
1746
  ...tenant_letsencrypt_sites,
1771
1747
  ]);
@@ -1775,12 +1751,10 @@ router.post(
1775
1751
  notify: "Certificate added, please restart server",
1776
1752
  });
1777
1753
  } catch (e) {
1778
- req.flash("error", e.message);
1779
- res.redirect("/useradmin/ssl");
1754
+ res.json({ error: e.message });
1780
1755
  }
1781
1756
  } else {
1782
- req.flash("error", req.__("Not possible for tenant"));
1783
- res.redirect("/useradmin/ssl");
1757
+ res.json({ error: req.__("Not possible for tenant") });
1784
1758
  }
1785
1759
  })
1786
1760
  );
@@ -1849,7 +1823,7 @@ router.post(
1849
1823
  "tenant_letsencrypt_sites",
1850
1824
  []
1851
1825
  );
1852
- await getState().setConfig(tenant_letsencrypt_sites, [
1826
+ await getState().setConfig("tenant_letsencrypt_sites", [
1853
1827
  ...altnames,
1854
1828
  ...tenant_letsencrypt_sites,
1855
1829
  ]);
@@ -1991,9 +1965,9 @@ router.get(
1991
1965
  });
1992
1966
  })
1993
1967
  );
1994
- const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
1968
+ const buildDialogScript = (capacitorBuilderAvailable, isSbadmin2) =>
1995
1969
  `<script>
1996
- var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1970
+ var capacitorBuilderAvailable = ${capacitorBuilderAvailable};
1997
1971
  var isSbadmin2 = ${isSbadmin2};
1998
1972
  function showEntrySelect(type) {
1999
1973
  for( const currentType of ["view", "page", "pagegroup"]) {
@@ -2022,7 +1996,7 @@ const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
2022
1996
 
2023
1997
  const imageAvailable = async () => {
2024
1998
  try {
2025
- const image = new Docker().getImage("saltcorn/cordova-builder");
1999
+ const image = new Docker().getImage("saltcorn/capacitor-builder");
2026
2000
  await image.inspect();
2027
2001
  return true;
2028
2002
  } catch (e) {
@@ -2818,10 +2792,10 @@ router.get(
2818
2792
  div(
2819
2793
  label(
2820
2794
  { class: "form-label fw-bold" },
2821
- req.__("Cordova builder") +
2795
+ req.__("Capacitor builder") +
2822
2796
  a(
2823
2797
  {
2824
- href: "javascript:ajax_modal('/admin/help/Cordova Builder?')",
2798
+ href: "javascript:ajax_modal('/admin/help/Capacitor Builder?')",
2825
2799
  },
2826
2800
  i({ class: "fas fa-question-circle ps-1" })
2827
2801
  )
@@ -2849,9 +2823,8 @@ router.get(
2849
2823
  { class: "col-sm-4" },
2850
2824
  button(
2851
2825
  {
2852
- id: "pullCordovaBtnId",
2853
2826
  type: "button",
2854
- onClick: `pull_cordova_builder(this);`,
2827
+ onClick: `pull_capacitor_builder(this);`,
2855
2828
  class: "btn btn-warning",
2856
2829
  },
2857
2830
  req.__("pull")
@@ -2859,7 +2832,7 @@ router.get(
2859
2832
  span(
2860
2833
  {
2861
2834
  role: "button",
2862
- onClick: "check_cordova_builder()",
2835
+ onClick: "check_capacitor_builder()",
2863
2836
  },
2864
2837
  span({ class: "ps-3" }, req.__("refresh")),
2865
2838
  i({ class: "ps-2 fas fa-undo" })
@@ -3349,36 +3322,37 @@ router.post(
3349
3322
  });
3350
3323
  const childOutputs = [];
3351
3324
  child.stdout.on("data", (data) => {
3352
- // console.log(data.toString());
3353
- if (data) childOutputs.push(data.toString());
3325
+ const outMsg = data.toString();
3326
+ getState().log(5, outMsg);
3327
+ if (data) childOutputs.push(outMsg);
3354
3328
  });
3355
3329
  child.stderr.on("data", (data) => {
3356
- // console.log(data.toString());
3357
- childOutputs.push(data ? data.toString() : req.__("An error occurred"));
3330
+ const errMsg = data ? data.toString() : req.__("An error occurred");
3331
+ getState().log(5, errMsg);
3332
+ childOutputs.push(errMsg);
3358
3333
  });
3359
3334
  child.on("exit", (exitCode, signal) => {
3360
3335
  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
- }
3336
+ const exitMsg = childOutputs.join("\n");
3337
+ fs.writeFile(path.join(buildDir, logFile), exitMsg, async (error) => {
3338
+ if (error) {
3339
+ console.log(`unable to write '${logFile}' to '${buildDir}'`);
3340
+ console.log(error);
3341
+ } else {
3342
+ // no transaction, '/build-mobile-app/finished' filters for valid attributes
3343
+ await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3372
3344
  }
3373
- );
3345
+ });
3374
3346
  });
3375
3347
  child.on("error", (msg) => {
3376
3348
  const message = msg.message ? msg.message : msg.code;
3377
3349
  const stack = msg.stack ? msg.stack : "";
3378
3350
  const logFile = "error_logs.txt";
3351
+ const errMsg = [message, stack].join("\n");
3352
+ getState().log(5, msg);
3379
3353
  fs.writeFile(
3380
3354
  path.join(buildDir, "error_logs.txt"),
3381
- [message, stack].join("\n"),
3355
+ errMsg,
3382
3356
  async (error) => {
3383
3357
  if (error) {
3384
3358
  console.log(`unable to write logFile to '${buildDir}'`);
@@ -3394,13 +3368,13 @@ router.post(
3394
3368
  );
3395
3369
 
3396
3370
  router.post(
3397
- "/mobile-app/pull-cordova-builder",
3371
+ "/mobile-app/pull-capacitor-builder",
3398
3372
  isAdmin,
3399
3373
  error_catcher(async (req, res) => {
3400
3374
  const state = getState();
3401
3375
  const child = spawn(
3402
3376
  `${process.env.DOCKER_BIN ? `${process.env.DOCKER_BIN}/` : ""}docker`,
3403
- ["pull", "saltcorn/cordova-builder:latest"],
3377
+ ["pull", `saltcorn/capacitor-builder:${state.scVersion}`],
3404
3378
  {
3405
3379
  stdio: ["ignore", "pipe", "pipe"],
3406
3380
  cwd: ".",
@@ -3415,11 +3389,11 @@ router.post(
3415
3389
  child.on("exit", (exitCode, signal) => {
3416
3390
  state.log(
3417
3391
  2,
3418
- `"pull cordova-builder exit with code: ${exitCode} and signal: ${signal}`
3392
+ `"pull capacitor-builder exit with code: ${exitCode} and signal: ${signal}`
3419
3393
  );
3420
3394
  });
3421
3395
  child.on("error", (msg) => {
3422
- state.log(1, `pull cordova-builder error: ${msg}`);
3396
+ state.log(1, `pull capacitor-builder error: ${msg}`);
3423
3397
  });
3424
3398
 
3425
3399
  res.json({});
@@ -3427,7 +3401,7 @@ router.post(
3427
3401
  );
3428
3402
 
3429
3403
  router.get(
3430
- "/mobile-app/check-cordova-builder",
3404
+ "/mobile-app/check-capacitor-builder",
3431
3405
  isAdmin,
3432
3406
  error_catcher(async (req, res) => {
3433
3407
  const installed = await imageAvailable();
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
  ],