@saltcorn/server 1.1.0-beta.11 → 1.1.0-beta.13

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
  }
@@ -460,6 +460,7 @@ function saveAndContinue(e, k, event) {
460
460
  data: form_data,
461
461
  success: function (res) {
462
462
  ajax_indicator(false);
463
+ form.removeAttr("data-unsaved-changes");
463
464
  form.parent().find(".full-form-error").text("");
464
465
  if (res.id && form.find("input[name=id")) {
465
466
  form.append(
@@ -890,9 +891,9 @@ function build_mobile_app(button) {
890
891
 
891
892
  if (
892
893
  params.useDocker &&
893
- !window.cordovaBuilderAvailable &&
894
+ !window.capacitorBuilderAvailable &&
894
895
  !confirm(
895
- "Docker is selected but the Cordova builder seems not to be installed. " +
896
+ "Docker is selected but the Capacitor builder seems not to be installed. " +
896
897
  "Do you really want to continue?"
897
898
  )
898
899
  ) {
@@ -935,11 +936,11 @@ function build_mobile_app(button) {
935
936
  });
936
937
  }
937
938
 
938
- function pull_cordova_builder() {
939
- ajax_post("/admin/mobile-app/pull-cordova-builder", {
939
+ function pull_capacitor_builder() {
940
+ ajax_post("/admin/mobile-app/pull-capacitor-builder", {
940
941
  success: () => {
941
942
  notifyAlert(
942
- "Pulling the the cordova-builder. " +
943
+ "Pulling the the capacitor-builder. " +
943
944
  "To see the progress, open the logs viewer with the System logging verbosity set to 'All'."
944
945
  );
945
946
  },
@@ -989,12 +990,12 @@ function check_xcodebuild() {
989
990
  });
990
991
  }
991
992
 
992
- function check_cordova_builder() {
993
- $.ajax("/admin/mobile-app/check-cordova-builder", {
993
+ function check_capacitor_builder() {
994
+ $.ajax("/admin/mobile-app/check-capacitor-builder", {
994
995
  type: "GET",
995
996
  success: function (res) {
996
- window.cordovaBuilderAvailable = !!res.installed;
997
- if (window.cordovaBuilderAvailable) {
997
+ window.capacitorBuilderAvailable = !!res.installed;
998
+ if (window.capacitorBuilderAvailable) {
998
999
  $("#dockerBuilderStatusId").html(
999
1000
  `<span>
1000
1001
  installed<i class="ps-2 fas fa-check text-success"></i>
@@ -1088,7 +1089,7 @@ function toggle_tbl_sync() {
1088
1089
  function toggle_android_platform() {
1089
1090
  if ($("#androidCheckboxId")[0].checked === true) {
1090
1091
  $("#dockerCheckboxId").attr("hidden", false);
1091
- $("#dockerCheckboxId").attr("checked", window.cordovaBuilderAvailable);
1092
+ $("#dockerCheckboxId").attr("checked", window.capacitorBuilderAvailable);
1092
1093
  $("#dockerLabelId").removeClass("d-none");
1093
1094
  } else {
1094
1095
  $("#dockerCheckboxId").attr("hidden", true);
@@ -1189,6 +1190,14 @@ function installPWA() {
1189
1190
  }
1190
1191
  }
1191
1192
 
1193
+ function check_unsaved_form(event, script_tag) {
1194
+ const form = $(script_tag).parent().find("form");
1195
+ if (form.attr("data-unsaved-changes")) {
1196
+ event.preventDefault();
1197
+ event.returnValue = true;
1198
+ }
1199
+ }
1200
+
1192
1201
  (() => {
1193
1202
  const e = document.querySelector("[data-sidebar-toggler]");
1194
1203
  let closed = localStorage.getItem("sidebarClosed") === "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/fields.js CHANGED
@@ -362,10 +362,12 @@ const fieldFlow = (req) =>
362
362
  name: req.__("Attributes"),
363
363
  contextField: "attributes",
364
364
  onlyWhen: (context) => {
365
- if (context.calculated) return false;
365
+ const type = getState().types[context.type];
366
+ if (context.calculated && !type?.setTypeAttributesForCalculatedFields)
367
+ return false;
368
+
366
369
  if (context.type === "File") return true;
367
370
  if (new Field(context).is_fkey) return false;
368
- const type = getState().types[context.type];
369
371
  if (!type) return false;
370
372
  const attrs = Field.getTypeAttributes(
371
373
  type.attributes,
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
  {
@@ -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",