@saltcorn/server 1.1.0-beta.10 → 1.1.0-beta.12

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.
@@ -727,6 +727,22 @@ function doMobileTransforms() {
727
727
  ],
728
728
  };
729
729
 
730
+ // change /plugins or plugins to sc_plugins
731
+ // capacitor reserves the plugins prefix for cordova plugins
732
+ const normalisePluginsPrefix = (path) => {
733
+ if (path.startsWith("/plugins/") || path.startsWith("plugins/"))
734
+ return path.replace(/\/?plugins\//, "sc_plugins/");
735
+ return path;
736
+ };
737
+ $("link").each(function () {
738
+ const path = $(this).attr("href");
739
+ if (path) $(this).attr("href", normalisePluginsPrefix(path));
740
+ });
741
+ $("script").each(function () {
742
+ const path = $(this).attr("src");
743
+ if (path) $(this).attr("src", normalisePluginsPrefix(path));
744
+ });
745
+
730
746
  $("a").each(function () {
731
747
  let path = $(this).attr("href") || "";
732
748
  if (path.startsWith("http")) {
@@ -1140,11 +1156,13 @@ function initialize_page() {
1140
1156
  }
1141
1157
  }
1142
1158
  }
1159
+
1143
1160
  setTimeout(() => {
1144
1161
  $("#toasts-area")
1145
1162
  .find(".show[rendered='server-side'][type='success']")
1146
1163
  .removeClass("show");
1147
1164
  }, 5000);
1165
+
1148
1166
  $(".lazy-accoordion").on("show.bs.collapse", function (e) {
1149
1167
  const $e = $(e.target).find("[data-sc-view-source]");
1150
1168
  if ($.trim($e.html()) == "") {
@@ -607,4 +607,10 @@ button.monospace-copy-btn {
607
607
 
608
608
  i[class^="unicode-"], i[class*=" unicode-"] {
609
609
  font-style: normal;
610
+ }
611
+
612
+ .tabulator.table-dark:not(.thead-light) .tabulator-footer, .tabulator.table-dark:not(.thead-light) .tabulator-footer .tabulator-col {
613
+ background-color: #212529;
614
+ border-color: #32383e;
615
+ color: #fff;
610
616
  }
@@ -890,9 +890,9 @@ function build_mobile_app(button) {
890
890
 
891
891
  if (
892
892
  params.useDocker &&
893
- !window.cordovaBuilderAvailable &&
893
+ !window.capacitorBuilderAvailable &&
894
894
  !confirm(
895
- "Docker is selected but the Cordova builder seems not to be installed. " +
895
+ "Docker is selected but the Capacitor builder seems not to be installed. " +
896
896
  "Do you really want to continue?"
897
897
  )
898
898
  ) {
@@ -935,11 +935,11 @@ function build_mobile_app(button) {
935
935
  });
936
936
  }
937
937
 
938
- function pull_cordova_builder() {
939
- ajax_post("/admin/mobile-app/pull-cordova-builder", {
938
+ function pull_capacitor_builder() {
939
+ ajax_post("/admin/mobile-app/pull-capacitor-builder", {
940
940
  success: () => {
941
941
  notifyAlert(
942
- "Pulling the the cordova-builder. " +
942
+ "Pulling the the capacitor-builder. " +
943
943
  "To see the progress, open the logs viewer with the System logging verbosity set to 'All'."
944
944
  );
945
945
  },
@@ -989,12 +989,12 @@ function check_xcodebuild() {
989
989
  });
990
990
  }
991
991
 
992
- function check_cordova_builder() {
993
- $.ajax("/admin/mobile-app/check-cordova-builder", {
992
+ function check_capacitor_builder() {
993
+ $.ajax("/admin/mobile-app/check-capacitor-builder", {
994
994
  type: "GET",
995
995
  success: function (res) {
996
- window.cordovaBuilderAvailable = !!res.installed;
997
- if (window.cordovaBuilderAvailable) {
996
+ window.capacitorBuilderAvailable = !!res.installed;
997
+ if (window.capacitorBuilderAvailable) {
998
998
  $("#dockerBuilderStatusId").html(
999
999
  `<span>
1000
1000
  installed<i class="ps-2 fas fa-check text-success"></i>
@@ -1088,7 +1088,7 @@ function toggle_tbl_sync() {
1088
1088
  function toggle_android_platform() {
1089
1089
  if ($("#androidCheckboxId")[0].checked === true) {
1090
1090
  $("#dockerCheckboxId").attr("hidden", false);
1091
- $("#dockerCheckboxId").attr("checked", window.cordovaBuilderAvailable);
1091
+ $("#dockerCheckboxId").attr("checked", window.capacitorBuilderAvailable);
1092
1092
  $("#dockerLabelId").removeClass("d-none");
1093
1093
  } else {
1094
1094
  $("#dockerCheckboxId").attr("hidden", true);
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,
@@ -988,39 +985,6 @@ router.post(
988
985
  })
989
986
  );
990
987
 
991
- router.post(
992
- "/save-config",
993
- isAdmin,
994
- error_catcher(async (req, res) => {
995
- const state = getState();
996
-
997
- //TODO check this is a config key
998
- const validKeyName = (k) =>
999
- k !== "_csrf" && k !== "constructor" && k !== "__proto__";
1000
-
1001
- for (const [k, v] of Object.entries(req.body)) {
1002
- if (!isFixedConfig(k) && typeof v !== "undefined" && validKeyName(k)) {
1003
- //TODO read value from type
1004
- await state.setConfig(k, v);
1005
- }
1006
- }
1007
-
1008
- // checkboxes that are false are not sent in post body. Check here
1009
- const { boolcheck } = req.query;
1010
- const boolchecks =
1011
- typeof boolcheck === "undefined"
1012
- ? []
1013
- : Array.isArray(boolcheck)
1014
- ? boolcheck
1015
- : [boolcheck];
1016
- for (const k of boolchecks) {
1017
- if (typeof req.body[k] === "undefined" && validKeyName(k))
1018
- await state.setConfig(k, false);
1019
- }
1020
- res.json({ success: "ok" });
1021
- })
1022
- );
1023
-
1024
988
  /**
1025
989
  * Do Auto backup now
1026
990
  */
@@ -1291,10 +1255,14 @@ router.post(
1291
1255
  })
1292
1256
  );
1293
1257
 
1294
- const pullCordovaBuilder = (req, res) => {
1295
- const child = spawn("docker", ["pull", "saltcorn/cordova-builder"], {
1296
- stdio: ["ignore", "pipe", "pipe"],
1297
- });
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
+ );
1298
1266
  return new Promise((resolve, reject) => {
1299
1267
  child.stdout.on("data", (data) => {
1300
1268
  res.write(data);
@@ -1550,9 +1518,9 @@ const doInstall = async (req, res, version, deepClean, runPull) => {
1550
1518
  }
1551
1519
  if (runPull) {
1552
1520
  res.write(
1553
- req.__("Pulling the cordova-builder docker image...") + "\n"
1521
+ req.__("Pulling the capacitor-builder docker image...") + "\n"
1554
1522
  );
1555
- const pullCode = await pullCordovaBuilder(req, res);
1523
+ const pullCode = await pullCapacitorBuilder(req, res, version);
1556
1524
  res.write(req.__("Pull done with code %s", pullCode) + "\n");
1557
1525
  if (pullCode === 0) {
1558
1526
  res.write(req.__("Pruning docker...") + "\n");
@@ -1997,9 +1965,9 @@ router.get(
1997
1965
  });
1998
1966
  })
1999
1967
  );
2000
- const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
1968
+ const buildDialogScript = (capacitorBuilderAvailable, isSbadmin2) =>
2001
1969
  `<script>
2002
- var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1970
+ var capacitorBuilderAvailable = ${capacitorBuilderAvailable};
2003
1971
  var isSbadmin2 = ${isSbadmin2};
2004
1972
  function showEntrySelect(type) {
2005
1973
  for( const currentType of ["view", "page", "pagegroup"]) {
@@ -2028,7 +1996,7 @@ const buildDialogScript = (cordovaBuilderAvailable, isSbadmin2) =>
2028
1996
 
2029
1997
  const imageAvailable = async () => {
2030
1998
  try {
2031
- const image = new Docker().getImage("saltcorn/cordova-builder");
1999
+ const image = new Docker().getImage("saltcorn/capacitor-builder");
2032
2000
  await image.inspect();
2033
2001
  return true;
2034
2002
  } catch (e) {
@@ -2824,10 +2792,10 @@ router.get(
2824
2792
  div(
2825
2793
  label(
2826
2794
  { class: "form-label fw-bold" },
2827
- req.__("Cordova builder") +
2795
+ req.__("Capacitor builder") +
2828
2796
  a(
2829
2797
  {
2830
- href: "javascript:ajax_modal('/admin/help/Cordova Builder?')",
2798
+ href: "javascript:ajax_modal('/admin/help/Capacitor Builder?')",
2831
2799
  },
2832
2800
  i({ class: "fas fa-question-circle ps-1" })
2833
2801
  )
@@ -2855,9 +2823,8 @@ router.get(
2855
2823
  { class: "col-sm-4" },
2856
2824
  button(
2857
2825
  {
2858
- id: "pullCordovaBtnId",
2859
2826
  type: "button",
2860
- onClick: `pull_cordova_builder(this);`,
2827
+ onClick: `pull_capacitor_builder(this);`,
2861
2828
  class: "btn btn-warning",
2862
2829
  },
2863
2830
  req.__("pull")
@@ -2865,7 +2832,7 @@ router.get(
2865
2832
  span(
2866
2833
  {
2867
2834
  role: "button",
2868
- onClick: "check_cordova_builder()",
2835
+ onClick: "check_capacitor_builder()",
2869
2836
  },
2870
2837
  span({ class: "ps-3" }, req.__("refresh")),
2871
2838
  i({ class: "ps-2 fas fa-undo" })
@@ -3355,36 +3322,37 @@ router.post(
3355
3322
  });
3356
3323
  const childOutputs = [];
3357
3324
  child.stdout.on("data", (data) => {
3358
- // console.log(data.toString());
3359
- if (data) childOutputs.push(data.toString());
3325
+ const outMsg = data.toString();
3326
+ getState().log(5, outMsg);
3327
+ if (data) childOutputs.push(outMsg);
3360
3328
  });
3361
3329
  child.stderr.on("data", (data) => {
3362
- // console.log(data.toString());
3363
- 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);
3364
3333
  });
3365
3334
  child.on("exit", (exitCode, signal) => {
3366
3335
  const logFile = exitCode === 0 ? "logs.txt" : "error_logs.txt";
3367
- fs.writeFile(
3368
- path.join(buildDir, logFile),
3369
- childOutputs.join("\n"),
3370
- async (error) => {
3371
- if (error) {
3372
- console.log(`unable to write '${logFile}' to '${buildDir}'`);
3373
- console.log(error);
3374
- } else {
3375
- // no transaction, '/build-mobile-app/finished' filters for valid attributes
3376
- await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3377
- }
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);
3378
3344
  }
3379
- );
3345
+ });
3380
3346
  });
3381
3347
  child.on("error", (msg) => {
3382
3348
  const message = msg.message ? msg.message : msg.code;
3383
3349
  const stack = msg.stack ? msg.stack : "";
3384
3350
  const logFile = "error_logs.txt";
3351
+ const errMsg = [message, stack].join("\n");
3352
+ getState().log(5, msg);
3385
3353
  fs.writeFile(
3386
3354
  path.join(buildDir, "error_logs.txt"),
3387
- [message, stack].join("\n"),
3355
+ errMsg,
3388
3356
  async (error) => {
3389
3357
  if (error) {
3390
3358
  console.log(`unable to write logFile to '${buildDir}'`);
@@ -3400,13 +3368,13 @@ router.post(
3400
3368
  );
3401
3369
 
3402
3370
  router.post(
3403
- "/mobile-app/pull-cordova-builder",
3371
+ "/mobile-app/pull-capacitor-builder",
3404
3372
  isAdmin,
3405
3373
  error_catcher(async (req, res) => {
3406
3374
  const state = getState();
3407
3375
  const child = spawn(
3408
3376
  `${process.env.DOCKER_BIN ? `${process.env.DOCKER_BIN}/` : ""}docker`,
3409
- ["pull", "saltcorn/cordova-builder:latest"],
3377
+ ["pull", `saltcorn/capacitor-builder:${state.scVersion}`],
3410
3378
  {
3411
3379
  stdio: ["ignore", "pipe", "pipe"],
3412
3380
  cwd: ".",
@@ -3421,11 +3389,11 @@ router.post(
3421
3389
  child.on("exit", (exitCode, signal) => {
3422
3390
  state.log(
3423
3391
  2,
3424
- `"pull cordova-builder exit with code: ${exitCode} and signal: ${signal}`
3392
+ `"pull capacitor-builder exit with code: ${exitCode} and signal: ${signal}`
3425
3393
  );
3426
3394
  });
3427
3395
  child.on("error", (msg) => {
3428
- state.log(1, `pull cordova-builder error: ${msg}`);
3396
+ state.log(1, `pull capacitor-builder error: ${msg}`);
3429
3397
  });
3430
3398
 
3431
3399
  res.json({});
@@ -3433,7 +3401,7 @@ router.post(
3433
3401
  );
3434
3402
 
3435
3403
  router.get(
3436
- "/mobile-app/check-cordova-builder",
3404
+ "/mobile-app/check-capacitor-builder",
3437
3405
  isAdmin,
3438
3406
  error_catcher(async (req, res) => {
3439
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
  );
package/routes/list.js CHANGED
@@ -268,6 +268,7 @@ router.get(
268
268
  clipboard: false,
269
269
  cellClick: "__delete_tabulator_row",
270
270
  });
271
+ const isDark = getState().getLightDarkMode(req.user) === "dark";
271
272
  res.sendWrap(
272
273
  {
273
274
  title: req.__(`%s data table`, table.name),
@@ -297,6 +298,13 @@ router.get(
297
298
  {
298
299
  css: `/static_assets/${db.connectObj.version_tag}/flatpickr.min.css`,
299
300
  },
301
+ ...(isDark
302
+ ? [
303
+ {
304
+ css: `/static_assets/${db.connectObj.version_tag}/flatpickr-dark.css`,
305
+ },
306
+ ]
307
+ : []),
300
308
  ],
301
309
  },
302
310
  {
@@ -431,7 +439,7 @@ router.get(
431
439
  div({
432
440
  id: "jsGrid",
433
441
  class:
434
- getState().getLightDarkMode() === "dark"
442
+ getState().getLightDarkMode(req.user) === "dark"
435
443
  ? "table-dark"
436
444
  : undefined,
437
445
  })
@@ -7,6 +7,7 @@ const {
7
7
  domReady,
8
8
  a,
9
9
  div,
10
+ h4,
10
11
  i,
11
12
  text,
12
13
  button,
@@ -40,6 +41,7 @@ const {
40
41
  install_pack,
41
42
  } = require("@saltcorn/admin-models/models/pack");
42
43
  const Trigger = require("@saltcorn/data/models/trigger");
44
+ const { getState } = require("@saltcorn/data/db/state");
43
45
  /**
44
46
  * @type {object}
45
47
  * @const
@@ -79,7 +81,17 @@ router.get(
79
81
  {},
80
82
  { orderBy: "name", nocase: true }
81
83
  );
82
- let tables, views, pages, triggers;
84
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
85
+
86
+ const all_configs_obj = await getState().getAllConfigOrDefaults();
87
+ const all_configs = Object.entries(all_configs_obj)
88
+ .map(([name, v]) => ({
89
+ ...v,
90
+ name,
91
+ }))
92
+ .filter((c) => isRoot || !c.root_only);
93
+
94
+ let tables, views, pages, triggers, configs;
83
95
  if (q) {
84
96
  const qlower = q.toLowerCase();
85
97
  const includesQ = (s) => s.toLowerCase().includes(qlower);
@@ -100,11 +112,13 @@ router.get(
100
112
  const pack = await trigger_pack(t);
101
113
  return includesQ(JSON.stringify(pack));
102
114
  });
115
+ configs = all_configs.filter((c) => includesQ(JSON.stringify(c)));
103
116
  } else {
104
117
  tables = all_tables;
105
118
  views = all_views;
106
119
  pages = all_pages;
107
120
  triggers = all_triggers;
121
+ configs = all_configs;
108
122
  }
109
123
  const li_link = (etype1, ename1) =>
110
124
  li(
@@ -124,7 +138,7 @@ router.get(
124
138
  action: `/registry-editor?etype=${etype}&ename=${encodeURIComponent(
125
139
  ename
126
140
  )}${qlink}`,
127
-
141
+ formStyle: "vert",
128
142
  values: { regval: JSON.stringify(jsonVal, null, 2) },
129
143
  fields: [
130
144
  {
@@ -177,6 +191,14 @@ router.get(
177
191
  const ppack = await page_pack(all_pages.find((v) => v.name === ename));
178
192
  edContents = renderForm(mkForm(ppack), req.csrfToken());
179
193
  break;
194
+ case "config":
195
+ const config = all_configs.find((t) => t.name === ename);
196
+ edContents =
197
+ h4(config.label) +
198
+ (config.blurb || "") +
199
+ (config.sublabel || "") +
200
+ renderForm(mkForm(config.value), req.csrfToken());
201
+ break;
180
202
  case "trigger":
181
203
  const trigger = all_triggers.find((t) => t.name === ename);
182
204
  const trpack = await trigger_pack(trigger);
@@ -282,6 +304,16 @@ router.get(
282
304
  triggers.map((t) => li_link("trigger", t.name))
283
305
  )
284
306
  )
307
+ ),
308
+ li(
309
+ details(
310
+ { open: q || etype === "CONFIG" }, //
311
+ summary("Configuration"),
312
+ ul(
313
+ { class: "ps-3" },
314
+ configs.map((t) => li_link("config", t.name))
315
+ )
316
+ )
285
317
  )
286
318
  )
287
319
  ),
@@ -309,7 +341,14 @@ router.post(
309
341
  const qlink = q ? `&q=${encodeURIComponent(q)}` : "";
310
342
 
311
343
  const entVal = JSON.parse(req.body.regval);
312
- let pack = { plugins: [], tables: [], views: [], pages: [], triggers: [] };
344
+ let pack = {
345
+ plugins: [],
346
+ tables: [],
347
+ views: [],
348
+ pages: [],
349
+ triggers: [],
350
+ config: {},
351
+ };
313
352
 
314
353
  switch (etype) {
315
354
  case "table":
@@ -324,6 +363,9 @@ router.post(
324
363
  case "trigger":
325
364
  pack.triggers = [entVal];
326
365
  break;
366
+ case "config":
367
+ pack.config[ename] = entVal;
368
+ break;
327
369
  }
328
370
  await install_pack(pack);
329
371
  res.redirect(
package/routes/tenant.js CHANGED
@@ -493,6 +493,7 @@ const tenant_settings_form = (req) =>
493
493
  "tenant_template",
494
494
  "tenant_baseurl",
495
495
  "tenant_create_unauth_redirect",
496
+ "tenant_inherit_cfgs",
496
497
  { section_header: req.__("Tenant application capabilities") },
497
498
  "tenants_install_git",
498
499
  "tenants_set_npm_modules",
package/serve.js CHANGED
@@ -121,7 +121,11 @@ const initMaster = async ({ disableMigrate }, useClusterAdaptor = true) => {
121
121
  if (getState().getConfig("log_sql", false)) db.set_sql_logging();
122
122
  if (db.is_it_multi_tenant()) {
123
123
  const tenants = await getAllTenants();
124
- await init_multi_tenant(loadAllPlugins, disableMigrate, tenants);
124
+ await init_multi_tenant(
125
+ async () => await loadAllPlugins(true),
126
+ disableMigrate,
127
+ tenants
128
+ );
125
129
  }
126
130
  eachTenant(async () => {
127
131
  const state = getState();
package/wrapper.js CHANGED
@@ -194,7 +194,7 @@ const get_headers = (req, version_tag, description, extras = []) => {
194
194
  }, _sc_globalCsrf = "${req.csrfToken()}", _sc_version_tag = "${version_tag}"${
195
195
  locale ? `, _sc_locale = "${locale}"` : ""
196
196
  }, _sc_lightmode = ${JSON.stringify(
197
- state.getLightDarkMode(req.user)
197
+ state.getLightDarkMode?.(req.user) || "light"
198
198
  )};</script>`,
199
199
  },
200
200
  { css: `/static_assets/${version_tag}/saltcorn.css` },