@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/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
  ],
package/routes/plugins.js CHANGED
@@ -57,7 +57,11 @@ const fs = require("fs");
57
57
  const path = require("path");
58
58
  const { get_latest_npm_version } = require("@saltcorn/data/models/config");
59
59
  const { flash_restart } = require("../markup/admin.js");
60
- const { sleep, removeNonWordChars } = require("@saltcorn/data/utils");
60
+ const {
61
+ sleep,
62
+ removeNonWordChars,
63
+ getFetchProxyOptions,
64
+ } = require("@saltcorn/data/utils");
61
65
  const { loadAllPlugins } = require("../load_plugins");
62
66
  const npmFetch = require("npm-registry-fetch");
63
67
  const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
@@ -609,7 +613,8 @@ router.get(
609
613
  } else {
610
614
  try {
611
615
  const pkgInfo = await npmFetch.json(
612
- `https://registry.npmjs.org/${plugin.location}`
616
+ `https://registry.npmjs.org/${plugin.location}`,
617
+ getFetchProxyOptions()
613
618
  );
614
619
  if (!pkgInfo?.versions)
615
620
  throw new Error(req.__("Unable to fetch versions"));
@@ -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/tables.js CHANGED
@@ -56,7 +56,7 @@ const {
56
56
  } = require("@saltcorn/data/models/discovery");
57
57
  const { getState } = require("@saltcorn/data/db/state");
58
58
  const { cardHeaderTabs } = require("@saltcorn/markup/layout_utils");
59
- const { tablesList, viewsList } = require("./common_lists");
59
+ const { tablesList, viewsList, getTriggerList } = require("./common_lists");
60
60
  const {
61
61
  InvalidConfiguration,
62
62
  removeAllWhiteSpace,
@@ -757,6 +757,8 @@ router.get(
757
757
  const triggers = table.id ? Trigger.find({ table_id: table.id }) : [];
758
758
  triggers.sort(comparingCaseInsensitive("name"));
759
759
  let fieldCard;
760
+ const nPrimaryKeys = fields.filter((f) => f.primary_key).length;
761
+
760
762
  if (fields.length === 0) {
761
763
  fieldCard = [
762
764
  h4(req.__(`No fields defined in %s table`, table.name)),
@@ -818,28 +820,25 @@ router.get(
818
820
  { hover: true }
819
821
  );
820
822
  fieldCard = [
823
+ nPrimaryKeys > 1 &&
824
+ div(
825
+ { class: "alert alert-danger", role: "alert" },
826
+ i({ class: "fas fa-exclamation-triangle" }),
827
+ "This table has composite primary keys which is not supported in Saltcorn. A procedure to introduce a single autoincrementing primary key is available.",
828
+ post_btn(
829
+ `/table/repair-composite-primary/${table.id}`,
830
+ "Add autoincrementing primary key",
831
+ req.csrfToken(),
832
+ { btnClass: "btn-danger" }
833
+ )
834
+ ),
835
+
821
836
  tableHtml,
822
837
  inbound_refs.length > 0
823
838
  ? req.__("Inbound keys: ") +
824
839
  inbound_refs.map((tnm) => link(`/table/${tnm}`, tnm)).join(", ") +
825
840
  "<br>"
826
841
  : "",
827
- triggers.length
828
- ? req.__("Table triggers: ") +
829
- triggers
830
- .map((t) =>
831
- link(
832
- `/actions/configure/${
833
- t.id
834
- }?on_done_redirect=${encodeURIComponent(
835
- `table/${table.name}`
836
- )}`,
837
- t.name
838
- )
839
- )
840
- .join(", ") +
841
- "<br>"
842
- : "",
843
842
  !table.external &&
844
843
  !table.provider_name &&
845
844
  a(
@@ -851,7 +850,8 @@ router.get(
851
850
  ),
852
851
  ];
853
852
  }
854
- var viewCard;
853
+ let viewCard;
854
+ let triggerCard = "";
855
855
  if (fields.length > 0) {
856
856
  const views = await View.find(
857
857
  table.id ? { table_id: table.id } : { exttable_name: table.name }
@@ -884,6 +884,25 @@ router.get(
884
884
  req.__("Create view")
885
885
  ),
886
886
  };
887
+
888
+ triggerCard = {
889
+ type: "card",
890
+ id: "table-triggers",
891
+ title: req.__("Triggers on table"),
892
+ contents:
893
+ (triggers.length
894
+ ? await getTriggerList(triggers, req)
895
+ : p("Triggers run actions in response to events on this table")) +
896
+ a(
897
+ {
898
+ href: `/actions/new?table=${encodeURIComponent(
899
+ table.name
900
+ )}&on_done_redirect=${encodeURIComponent(`table/${table.name}`)}`,
901
+ class: "btn btn-primary",
902
+ },
903
+ req.__("Create trigger")
904
+ ),
905
+ };
887
906
  }
888
907
  const models = await Model.find({ table_id: table.id });
889
908
  const modelCard = div(
@@ -1078,6 +1097,7 @@ router.get(
1078
1097
  ]
1079
1098
  : []),
1080
1099
  ...(viewCard ? [viewCard] : []),
1100
+ ...(triggerCard ? [triggerCard] : []),
1081
1101
  {
1082
1102
  type: "card",
1083
1103
  title: req.__("Edit table properties"),
@@ -1111,7 +1131,7 @@ router.post(
1111
1131
  const v = req.body;
1112
1132
  if (typeof v.id === "undefined" && typeof v.external === "undefined") {
1113
1133
  // insert
1114
- v.name = v.name.trim()
1134
+ v.name = v.name.trim();
1115
1135
  const { name, ...rest } = v;
1116
1136
  const alltables = await Table.find({});
1117
1137
  const existing_tables = [
@@ -1162,6 +1182,7 @@ router.post(
1162
1182
  let notify = "";
1163
1183
  if (!rest.versioned) rest.versioned = false;
1164
1184
  if (!rest.has_sync_info) rest.has_sync_info = false;
1185
+ rest.is_user_group = !!rest.is_user_group;
1165
1186
  if (rest.ownership_field_id === "_formula") {
1166
1187
  rest.ownership_field_id = null;
1167
1188
  const fmlValidRes = expressionValidator(rest.ownership_formula);
@@ -1922,7 +1943,7 @@ router.post(
1922
1943
  const table = Table.findOne({ name });
1923
1944
 
1924
1945
  try {
1925
- await table.deleteRows({}, req.user);
1946
+ await table.deleteRows({}, req.user, true);
1926
1947
  req.flash("success", req.__("Deleted all rows"));
1927
1948
  } catch (e) {
1928
1949
  req.flash("error", e.message);
@@ -2074,3 +2095,20 @@ router.post(
2074
2095
  respondWorkflow(table, workflow, wfres, req, res);
2075
2096
  })
2076
2097
  );
2098
+
2099
+ router.post(
2100
+ "/repair-composite-primary/:id",
2101
+ isAdmin,
2102
+ error_catcher(async (req, res) => {
2103
+ const { id } = req.params;
2104
+
2105
+ const table = Table.findOne({ id });
2106
+ if (!table) {
2107
+ req.flash("error", `Table not found`);
2108
+ res.redirect(`/table`);
2109
+ return;
2110
+ }
2111
+ await table.repairCompositePrimary();
2112
+ res.redirect(`/table/${table.id}`);
2113
+ })
2114
+ );
package/routes/tenant.js CHANGED
@@ -58,6 +58,7 @@ const {
58
58
  save_config_from_form,
59
59
  } = require("../markup/admin.js");
60
60
  const { getConfig } = require("@saltcorn/data/models/config");
61
+ const path = require("path");
61
62
  //const {quote} = require("@saltcorn/db-common");
62
63
  // todo add button backup / restore for particular tenant (available in admin tenants screens)
63
64
  //const {
@@ -318,7 +319,53 @@ router.post(
318
319
  if (hasTemplate) {
319
320
  new_url_create += "auth/create_first_user";
320
321
  }
321
-
322
+ const letsencrypt = getState().getConfig("letsencrypt", false);
323
+ if (letsencrypt) {
324
+ let altname = await tenant_letsencrypt_name(subdomain);
325
+ const tenant_letsencrypt_sites = getState().getConfig(
326
+ "tenant_letsencrypt_sites",
327
+ []
328
+ );
329
+ const has_cert = tenant_letsencrypt_sites.includes(altname);
330
+ if (!has_cert) {
331
+ const file_store = db.connectObj.file_store;
332
+ const admin_users = await User.find(
333
+ { role_id: 1 },
334
+ { orderBy: "id" }
335
+ );
336
+ // greenlock logic
337
+ const Greenlock = require("greenlock");
338
+ const greenlock = Greenlock.create({
339
+ packageRoot: path.resolve(__dirname, ".."),
340
+ configDir: path.join(file_store, "greenlock.d"),
341
+ maintainerEmail: admin_users[0].email,
342
+ });
343
+
344
+ await greenlock.sites.add({
345
+ subject: altname,
346
+ altnames: [altname],
347
+ });
348
+ // letsencrypt
349
+ const tenant_letsencrypt_sites = getState().getConfig(
350
+ "tenant_letsencrypt_sites",
351
+ []
352
+ );
353
+ await getState().setConfig("tenant_letsencrypt_sites", [
354
+ altname,
355
+ ...tenant_letsencrypt_sites,
356
+ ]);
357
+ if (req.user?.role_id === 1) {
358
+ req.flash(
359
+ "success",
360
+ req.__(
361
+ "Tenant created. Certificate will be acquired on first visit."
362
+ )
363
+ );
364
+ res.redirect("/tenant/list");
365
+ return;
366
+ }
367
+ }
368
+ }
322
369
  res.sendWrap(
323
370
  req.__("Create application"),
324
371
  div(
@@ -374,6 +421,7 @@ router.get(
374
421
  return;
375
422
  }
376
423
  const tens = await db.select("_sc_tenants");
424
+ const locale = getState().getConfig("default_locale", "en");
377
425
  send_infoarch_page({
378
426
  res,
379
427
  req,
@@ -400,7 +448,8 @@ router.get(
400
448
  },
401
449
  {
402
450
  label: req.__("Created"),
403
- key: (r) => (r.created ? localeDateTime(r.created) : ""),
451
+ key: (r) =>
452
+ r.created ? localeDateTime(r.created, {}, locale) : "",
404
453
  },
405
454
  {
406
455
  label: req.__("Information"),
@@ -444,6 +493,7 @@ const tenant_settings_form = (req) =>
444
493
  "tenant_template",
445
494
  "tenant_baseurl",
446
495
  "tenant_create_unauth_redirect",
496
+ "tenant_inherit_cfgs",
447
497
  { section_header: req.__("Tenant application capabilities") },
448
498
  "tenants_install_git",
449
499
  "tenants_set_npm_modules",
@@ -628,7 +678,7 @@ router.get(
628
678
  []
629
679
  );
630
680
  const has_cert = tenant_letsencrypt_sites.includes(altname);
631
-
681
+
632
682
  // get list of files
633
683
  let files;
634
684
  await db.runWithTenant(subdomain, async () => {
@@ -151,14 +151,6 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
151
151
  "The view name is part of the URL when it is shown alone."
152
152
  ),
153
153
  }),
154
- new Field({
155
- label: req.__("Description"),
156
- name: "description",
157
- type: "String",
158
- sublabel: req.__(
159
- "Description allows you to give more information about the view."
160
- ),
161
- }),
162
154
  new Field({
163
155
  label: req.__("View pattern"),
164
156
  name: "viewtemplate",
@@ -200,6 +192,14 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
200
192
  required: true,
201
193
  options: roles.map((r) => ({ value: r.id, label: r.role })),
202
194
  }),
195
+ new Field({
196
+ label: req.__("Description"),
197
+ name: "description",
198
+ type: "String",
199
+ sublabel: req.__(
200
+ "Description allows you to give more information about the view."
201
+ ),
202
+ }),
203
203
  new Field({
204
204
  name: "page_title",
205
205
  label: req.__("Page title"),
@@ -161,10 +161,10 @@ describe("Stable versioning install", () => {
161
161
  name: "@christianhugoch/empty_sc_test_plugin",
162
162
  });
163
163
  expect(dbPlugin).not.toBe(null);
164
- expect(dbPlugin.version).toBe("0.0.1");
164
+ expect(dbPlugin.version).toBe("0.1.0");
165
165
  });
166
166
 
167
- it("installs a fixed version", async () => {
167
+ it("installs and upgrades a fixed version", async () => {
168
168
  const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
169
169
  await loadAndSaveNewPlugin(
170
170
  new Plugin({
@@ -178,7 +178,7 @@ describe("Stable versioning install", () => {
178
178
  name: "@christianhugoch/empty_sc_test_plugin",
179
179
  });
180
180
  expect(dbPlugin).not.toBe(null);
181
- expect(dbPlugin.version).toBe("0.0.1");
181
+ expect(dbPlugin.version).toBe("0.1.0");
182
182
  });
183
183
 
184
184
  it("installs and downgrades a fixed version", async () => {
@@ -188,7 +188,7 @@ describe("Stable versioning install", () => {
188
188
  name: "@christianhugoch/empty_sc_test_plugin",
189
189
  location: "@christianhugoch/empty_sc_test_plugin",
190
190
  source: "npm",
191
- version: "0.0.6",
191
+ version: "0.2.0",
192
192
  }),
193
193
  true
194
194
  );
@@ -196,7 +196,7 @@ describe("Stable versioning install", () => {
196
196
  name: "@christianhugoch/empty_sc_test_plugin",
197
197
  });
198
198
  expect(dbPlugin).not.toBe(null);
199
- expect(dbPlugin.version).toBe("0.0.1");
199
+ expect(dbPlugin.version).toBe("0.1.0");
200
200
  });
201
201
  });
202
202
 
@@ -245,7 +245,7 @@ describe("Stable versioning upgrade", () => {
245
245
  expect(newPlugin.version).toBe("0.0.3");
246
246
  });
247
247
 
248
- it("upgrades to latest with downgrade", async () => {
248
+ it("upgrades to latest with downgrade to supported", async () => {
249
249
  const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
250
250
  await loadAndSaveNewPlugin(
251
251
  new Plugin({
@@ -276,7 +276,7 @@ describe("Stable versioning upgrade", () => {
276
276
  name: "@christianhugoch/empty_sc_test_plugin",
277
277
  });
278
278
  expect(newPlugin).not.toBe(null);
279
- expect(newPlugin.version).toBe("0.0.1");
279
+ expect(newPlugin.version).toBe("0.1.0");
280
280
  });
281
281
 
282
282
  it("upgrades to fixed version", async () => {
@@ -313,7 +313,7 @@ describe("Stable versioning upgrade", () => {
313
313
  expect(newPlugin.version).toBe("0.0.3");
314
314
  });
315
315
 
316
- it("upgrades to fixed version with downgrade", async () => {
316
+ it("upgrades to fixed version with downgrade to supported", async () => {
317
317
  const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
318
318
  await loadAndSaveNewPlugin(
319
319
  new Plugin({
@@ -336,7 +336,7 @@ describe("Stable versioning upgrade", () => {
336
336
  name: "@christianhugoch/empty_sc_test_plugin",
337
337
  location: "@christianhugoch/empty_sc_test_plugin",
338
338
  source: "npm",
339
- version: "0.0.6",
339
+ version: "0.2.0",
340
340
  }),
341
341
  true
342
342
  );
@@ -344,6 +344,6 @@ describe("Stable versioning upgrade", () => {
344
344
  name: "@christianhugoch/empty_sc_test_plugin",
345
345
  });
346
346
  expect(newPlugin).not.toBe(null);
347
- expect(newPlugin.version).toBe("0.0.1");
347
+ expect(newPlugin.version).toBe("0.1.0");
348
348
  });
349
349
  });
@@ -365,7 +365,7 @@ describe("Upgrade plugin to supported version", () => {
365
365
  expect(upgradedPlugin.version).toBe("0.0.3");
366
366
  });
367
367
 
368
- it("upgrades to the most current fixed version", async () => {
368
+ it("upgrades to latest as fixed version", async () => {
369
369
  const oldPlugin = await setupPluginVersion(
370
370
  "@christianhugoch/empty_sc_test_plugin_two",
371
371
  "0.0.1"
@@ -389,7 +389,7 @@ describe("Upgrade plugin to supported version", () => {
389
389
  expect(upgradedPlugin.version).toBe("0.0.3");
390
390
  });
391
391
 
392
- it("upgrades with a downgrade of the latest version", async () => {
392
+ it("upgrades with a downgrade of latest", async () => {
393
393
  const oldPlugin = await setupPluginVersion(
394
394
  "@christianhugoch/empty_sc_test_plugin",
395
395
  "0.0.1"
@@ -404,10 +404,10 @@ describe("Upgrade plugin to supported version", () => {
404
404
  const upgradedPlugin = await Plugin.findOne({
405
405
  name: "@christianhugoch/empty_sc_test_plugin",
406
406
  });
407
- expect(upgradedPlugin.version).toBe("0.0.1");
407
+ expect(upgradedPlugin.version).toBe("0.1.0");
408
408
  });
409
409
 
410
- it("upgrades with a downgrade of the most current fixed version", async () => {
410
+ it("upgrades with a downgrade of latest as fixed version", async () => {
411
411
  const oldPlugin = await setupPluginVersion(
412
412
  "@christianhugoch/empty_sc_test_plugin",
413
413
  "0.0.1"
@@ -421,13 +421,13 @@ describe("Upgrade plugin to supported version", () => {
421
421
  "@christianhugoch/empty_sc_test_plugin"
422
422
  )}`
423
423
  )
424
- .send("version=0.1.0")
424
+ .send("version=0.2.0")
425
425
  .set("Cookie", loginCookie)
426
426
  .expect(toRedirect("/plugins"));
427
427
  const upgradedPlugin = await Plugin.findOne({
428
428
  name: "@christianhugoch/empty_sc_test_plugin",
429
429
  });
430
- expect(upgradedPlugin.version).toBe("0.0.1");
430
+ expect(upgradedPlugin.version).toBe("0.1.0");
431
431
  });
432
432
  });
433
433
 
package/wrapper.js CHANGED
@@ -193,7 +193,9 @@ const get_headers = (req, version_tag, description, extras = []) => {
193
193
  state.logLevel
194
194
  }, _sc_globalCsrf = "${req.csrfToken()}", _sc_version_tag = "${version_tag}"${
195
195
  locale ? `, _sc_locale = "${locale}"` : ""
196
- };</script>`,
196
+ }, _sc_lightmode = ${JSON.stringify(
197
+ state.getLightDarkMode?.(req.user) || "light"
198
+ )};</script>`,
197
199
  },
198
200
  { css: `/static_assets/${version_tag}/saltcorn.css` },
199
201
  { script: `/static_assets/${version_tag}/saltcorn-common.js` },