@saltcorn/server 0.9.5-beta.6 → 0.9.5-beta.8

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/errors.js CHANGED
@@ -37,6 +37,7 @@ module.exports =
37
37
  const createCrash = severity <= 3;
38
38
  //console.error(err.stack);
39
39
  if (!(devmode && log_sql) && createCrash) await Crash.create(err, req);
40
+ else console.error(err);
40
41
 
41
42
  if (req.xhr) {
42
43
  res
@@ -0,0 +1,13 @@
1
+ The Cordova builder is a docker image with all dependencies to build Android apps.
2
+ It can be pulled from dockerhub while installing Saltcorn, or you can use the pull button.
3
+
4
+ Please make sure your server has a valid and accessible docker daemon running.
5
+
6
+ For this, either set up a standard docker installation
7
+ or use the [docker rootless mode](https://docs.docker.com/engine/security/rootless/) (recommended).
8
+
9
+ In a standard docker environment, you need root user privileges.
10
+ For this, you can add the user running Saltcorn to the docker group,
11
+ or if you already have the privileges, you are ready to go.
12
+
13
+ A docker daemon in rootless mode doesn't need any further configuration.
package/load_plugins.js CHANGED
@@ -25,14 +25,29 @@ const loadPlugin = async (plugin, force) => {
25
25
  typeof plugin.configuration === "string"
26
26
  ? JSON.parse(plugin.configuration)
27
27
  : plugin.configuration;
28
- // register plugin
29
- getState().registerPlugin(
30
- res.plugin_module.plugin_name || plugin.name,
31
- res.plugin_module,
32
- configuration,
33
- res.location,
34
- res.name
35
- );
28
+ try {
29
+ // register plugin
30
+ getState().registerPlugin(
31
+ res.plugin_module.plugin_name || plugin.name,
32
+ res.plugin_module,
33
+ configuration,
34
+ res.location,
35
+ res.name
36
+ );
37
+ } catch (error) {
38
+ if (force) {
39
+ // remove the install dir and try again
40
+ await loader.remove();
41
+ await loader.install(force);
42
+ getState().registerPlugin(
43
+ res.plugin_module.plugin_name || plugin.name,
44
+ res.plugin_module,
45
+ configuration,
46
+ res.location,
47
+ res.name
48
+ );
49
+ }
50
+ }
36
51
  if (res.plugin_module.onLoad) {
37
52
  try {
38
53
  await res.plugin_module.onLoad(plugin.configuration);
@@ -76,9 +91,15 @@ const loadAllPlugins = async (force) => {
76
91
  * @param plugin
77
92
  * @param force
78
93
  * @param noSignalOrDB
94
+ * @param __ translation function
79
95
  * @returns {Promise<void>}
80
96
  */
81
- const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
97
+ const loadAndSaveNewPlugin = async (
98
+ plugin,
99
+ force,
100
+ noSignalOrDB,
101
+ __ = (str) => str
102
+ ) => {
82
103
  const tenants_unsafe_plugins = getRootState().getConfig(
83
104
  "tenants_unsafe_plugins",
84
105
  false
@@ -104,8 +125,10 @@ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
104
125
  return;
105
126
  }
106
127
  }
128
+ const msgs = [];
107
129
  const loader = new PluginInstaller(plugin);
108
- const { version, plugin_module, location } = await loader.install(force);
130
+ const { version, plugin_module, location, loadedWithReload } =
131
+ await loader.install(force);
109
132
 
110
133
  // install dependecies
111
134
  for (const loc of plugin_module.dependencies || []) {
@@ -118,13 +141,43 @@ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
118
141
  );
119
142
  }
120
143
  }
121
- getState().registerPlugin(
122
- plugin_module.plugin_name || plugin.name,
123
- plugin_module,
124
- plugin.configuration,
125
- location,
126
- plugin.name
127
- );
144
+ let registeredWithReload = false;
145
+ try {
146
+ getState().registerPlugin(
147
+ plugin_module.plugin_name || plugin.name,
148
+ plugin_module,
149
+ plugin.configuration,
150
+ location,
151
+ plugin.name
152
+ );
153
+ } catch (error) {
154
+ if (force) {
155
+ getState().log(
156
+ 2,
157
+ `Error registering plugin ${plugin.name}. Removing and trying again.`
158
+ );
159
+ await loader.remove();
160
+ await loader.install(force);
161
+ getState().registerPlugin(
162
+ plugin_module.plugin_name || plugin.name,
163
+ plugin_module,
164
+ plugin.configuration,
165
+ location,
166
+ plugin.name
167
+ );
168
+ registeredWithReload = true;
169
+ } else {
170
+ throw error;
171
+ }
172
+ }
173
+ if (loadedWithReload || registeredWithReload) {
174
+ msgs.push(
175
+ __(
176
+ "The plugin was corrupted and had to be repaired. We recommend restarting your server.",
177
+ plugin.name
178
+ )
179
+ );
180
+ }
128
181
  if (plugin_module.onLoad) {
129
182
  try {
130
183
  await plugin_module.onLoad(plugin.configuration);
@@ -141,6 +194,7 @@ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
141
194
  force: false, // okay ??
142
195
  });
143
196
  }
197
+ return msgs;
144
198
  };
145
199
 
146
200
  module.exports = {
package/locales/en.json CHANGED
@@ -1384,5 +1384,12 @@
1384
1384
  "Optionally associate a table with this trigger": "Optionally associate a table with this trigger",
1385
1385
  "Delete table": "Delete table",
1386
1386
  "Signup role": "Signup role",
1387
- "The initial role of signed up users": "The initial role of signed up users"
1388
- }
1387
+ "The initial role of signed up users": "The initial role of signed up users",
1388
+ "Cordova builder": "Cordova builder",
1389
+ "not available": "not available",
1390
+ "pull": "pull",
1391
+ "refresh": "refresh",
1392
+ "installed": "installed",
1393
+ "Include table history in backup": "Include table history in backup",
1394
+ "The plugin was corrupted and had to be repaired. We recommend restarting your server.": "The plugin was corrupted and had to be repaired. We recommend restarting your server."
1395
+ }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.5-beta.6",
3
+ "version": "0.9.5-beta.8",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "0.9.5-beta.6",
11
- "@saltcorn/builder": "0.9.5-beta.6",
12
- "@saltcorn/data": "0.9.5-beta.6",
13
- "@saltcorn/admin-models": "0.9.5-beta.6",
14
- "@saltcorn/filemanager": "0.9.5-beta.6",
15
- "@saltcorn/markup": "0.9.5-beta.6",
16
- "@saltcorn/plugins-loader": "0.9.5-beta.6",
17
- "@saltcorn/sbadmin2": "0.9.5-beta.6",
10
+ "@saltcorn/base-plugin": "0.9.5-beta.8",
11
+ "@saltcorn/builder": "0.9.5-beta.8",
12
+ "@saltcorn/data": "0.9.5-beta.8",
13
+ "@saltcorn/admin-models": "0.9.5-beta.8",
14
+ "@saltcorn/filemanager": "0.9.5-beta.8",
15
+ "@saltcorn/markup": "0.9.5-beta.8",
16
+ "@saltcorn/plugins-loader": "0.9.5-beta.8",
17
+ "@saltcorn/sbadmin2": "0.9.5-beta.8",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -27,6 +27,7 @@
27
27
  "cors": "2.8.5",
28
28
  "csurf": "^1.11.0",
29
29
  "csv-stringify": "^5.5.0",
30
+ "dockerode": "~4.0.2",
30
31
  "express": "^4.17.1",
31
32
  "express-fileupload": "^1.1.8",
32
33
  "express-promise-router": "^3.0.3",
@@ -131,7 +131,23 @@ var logViewerHelpers = (() => {
131
131
 
132
132
  return {
133
133
  init_log_socket: () => {
134
- const socket = io({ transports: ["websocket"] });
134
+ let socket = null;
135
+ setTimeout(() => {
136
+ if (socket === null || socket.disconnected) {
137
+ notifyAlert({
138
+ type: "danger",
139
+ text: "Unable to connect to the server",
140
+ });
141
+ }
142
+ }, 5000);
143
+ try {
144
+ socket = io({ transports: ["websocket"] });
145
+ } catch (e) {
146
+ notifyAlert({
147
+ type: "danger",
148
+ text: "Unable to connect to the server " + e.message,
149
+ });
150
+ }
135
151
  startTrackingMsg();
136
152
  socket.on("connect", () => handleConnect(socket));
137
153
  socket.on("disconnect", handleDisconnect);
@@ -76,7 +76,7 @@ function valid_js_var_name(s) {
76
76
  return !!s.match(/^[a-zA-Z_$][a-zA-Z_$0-9]*$/);
77
77
  }
78
78
  function apply_showif() {
79
- const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
79
+ const isNode = getIsNode();
80
80
  $("[data-show-if]").each(function (ix, element) {
81
81
  var e = $(element);
82
82
  try {
@@ -571,7 +571,7 @@ function reload_on_init() {
571
571
  }
572
572
  function initialize_page() {
573
573
  if (window._sc_locale && window.dayjs) dayjs.locale(window._sc_locale);
574
- const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
574
+ const isNode = getIsNode();
575
575
  //console.log("init page");
576
576
  $(".blur-on-enter-keypress").bind("keyup", function (e) {
577
577
  if (e.keyCode === 13) e.target.blur();
@@ -877,7 +877,7 @@ function initialize_page() {
877
877
  $(initialize_page);
878
878
 
879
879
  function cancel_inline_edit(e, opts1) {
880
- const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
880
+ const isNode = getIsNode();
881
881
  var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
882
882
  var form = $(e.target).closest("form");
883
883
  form.replaceWith(opts.resetHtml);
@@ -885,7 +885,7 @@ function cancel_inline_edit(e, opts1) {
885
885
  }
886
886
 
887
887
  function inline_submit_success(e, form, opts) {
888
- const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
888
+ const isNode = getIsNode();
889
889
  const formDataArray = form.serializeArray();
890
890
  if (opts) {
891
891
  let fdEntry = formDataArray.find((f) => f.name == opts.key);
@@ -1025,6 +1025,15 @@ function tristateClick(e, required) {
1025
1025
  }
1026
1026
  }
1027
1027
 
1028
+ function getIsNode() {
1029
+ try {
1030
+ return typeof parent?.saltcorn?.data?.state === "undefined";
1031
+ } catch (e) {
1032
+ //probably in an iframe
1033
+ return true;
1034
+ }
1035
+ }
1036
+
1028
1037
  function buildToast(txt, type, spin) {
1029
1038
  const realtype = type === "error" ? "danger" : type;
1030
1039
  const icon =
@@ -1035,7 +1044,7 @@ function buildToast(txt, type, spin) {
1035
1044
  : realtype === "warning"
1036
1045
  ? "fa-exclamation-triangle"
1037
1046
  : "";
1038
- const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
1047
+ const isNode = getIsNode();
1039
1048
  const rndid = `tab${Math.floor(Math.random() * 16777215).toString(16)}`;
1040
1049
  return {
1041
1050
  id: rndid,
@@ -821,6 +821,17 @@ function build_mobile_app(button) {
821
821
  params.includedPlugins = Array.from(pluginsSelect.options)
822
822
  .filter((option) => !option.hidden)
823
823
  .map((option) => option.value);
824
+
825
+ if (
826
+ params.useDocker &&
827
+ !cordovaBuilderAvailable &&
828
+ !confirm(
829
+ "Docker is selected but the Cordova builder seems not to be installed. " +
830
+ "Do you really want to continue?"
831
+ )
832
+ ) {
833
+ return;
834
+ }
824
835
  ajax_post("/admin/build-mobile-app", {
825
836
  data: params,
826
837
  success: (data) => {
@@ -834,6 +845,41 @@ function build_mobile_app(button) {
834
845
  });
835
846
  }
836
847
 
848
+ function pull_cordova_builder() {
849
+ ajax_post("/admin/mobile-app/pull-cordova-builder", {
850
+ success: () => {
851
+ notifyAlert(
852
+ "Pulling the the cordova-builder. " +
853
+ "To see the progress, open the logs viewer with the System logging verbosity set to 'All'."
854
+ );
855
+ },
856
+ });
857
+ }
858
+
859
+ function check_cordova_builder() {
860
+ $.ajax("/admin/mobile-app/check-cordova-builder", {
861
+ type: "GET",
862
+ success: function (res) {
863
+ cordovaBuilderAvailable = !!res.installed;
864
+ if (cordovaBuilderAvailable) {
865
+ $("#dockerBuilderStatusId").html(
866
+ `<span>
867
+ installed<i class="ps-2 fas fa-check text-success"></i>
868
+ </span>
869
+ `
870
+ );
871
+ } else {
872
+ $("#dockerBuilderStatusId").html(
873
+ `<span>
874
+ not available<i class="ps-2 fas fa-times text-danger"></i>
875
+ </span>
876
+ `
877
+ );
878
+ }
879
+ },
880
+ });
881
+ }
882
+
837
883
  function move_to_synched() {
838
884
  const opts = $("#unsynched-tbls-select-id");
839
885
  $("#synched-tbls-select-id").removeAttr("selected");
@@ -897,7 +943,7 @@ function toggle_tbl_sync() {
897
943
  function toggle_android_platform() {
898
944
  if ($("#androidCheckboxId")[0].checked === true) {
899
945
  $("#dockerCheckboxId").attr("hidden", false);
900
- $("#dockerCheckboxId").attr("checked", true);
946
+ $("#dockerCheckboxId").attr("checked", cordovaBuilderAvailable);
901
947
  $("#dockerLabelId").removeClass("d-none");
902
948
  } else {
903
949
  $("#dockerCheckboxId").attr("hidden", true);
package/routes/admin.js CHANGED
@@ -107,6 +107,7 @@ const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
107
107
  const stream = require("stream");
108
108
  const Crash = require("@saltcorn/data/models/crash");
109
109
  const { get_help_markup } = require("../help/index.js");
110
+ const Docker = require("dockerode");
110
111
 
111
112
  const router = new Router();
112
113
  module.exports = router;
@@ -273,6 +274,8 @@ router.get(
273
274
  const aBackupFilePrefixForm = backupFilePrefixForm(req);
274
275
  aBackupFilePrefixForm.values.backup_file_prefix =
275
276
  getState().getConfig("backup_file_prefix");
277
+ aBackupFilePrefixForm.values.backup_history =
278
+ getState().getConfig("backup_history");
276
279
  //
277
280
  const backupForm = autoBackupForm(req);
278
281
  backupForm.values.auto_backup_frequency = getState().getConfig(
@@ -673,6 +676,13 @@ const backupFilePrefixForm = (req) =>
673
676
  sublabel: req.__("Backup file prefix"),
674
677
  default: "sc-backup-",
675
678
  },
679
+ {
680
+ type: "Bool",
681
+ label: req.__("History"),
682
+ name: "backup_history",
683
+ sublabel: req.__("Include table history in backup"),
684
+ default: true,
685
+ },
676
686
  ],
677
687
  });
678
688
 
@@ -1104,6 +1114,28 @@ router.post(
1104
1114
  })
1105
1115
  );
1106
1116
 
1117
+ const pullCordovaBuilder = (req, res) => {
1118
+ const child = spawn("docker", ["pull", "saltcorn/cordova-builder"], {
1119
+ stdio: ["ignore", "pipe", "pipe"],
1120
+ });
1121
+ return new Promise((resolve, reject) => {
1122
+ child.stdout.on("data", (data) => {
1123
+ res.write(data);
1124
+ });
1125
+ child.stderr?.on("data", (data) => {
1126
+ res.write(data);
1127
+ });
1128
+ child.on("exit", function (code, signal) {
1129
+ resolve(code);
1130
+ });
1131
+ child.on("error", (msg) => {
1132
+ const message = msg.message ? msg.message : msg.code;
1133
+ res.write(req.__("Error: ") + message + "\n");
1134
+ resolve(msg.code);
1135
+ });
1136
+ });
1137
+ };
1138
+
1107
1139
  /**
1108
1140
  * Do Upgrade
1109
1141
  * @name post/upgrade
@@ -1132,7 +1164,14 @@ router.post(
1132
1164
  child.stderr?.on("data", (data) => {
1133
1165
  res.write(data);
1134
1166
  });
1135
- child.on("exit", function (code, signal) {
1167
+ child.on("exit", async function (code, signal) {
1168
+ if (code === 0) {
1169
+ res.write(
1170
+ req.__("Pulling the cordova-builder docker image...") + "\n"
1171
+ );
1172
+ const pullCode = await pullCordovaBuilder(req, res);
1173
+ res.write(req.__("Pull done with code %s", pullCode) + "\n");
1174
+ }
1136
1175
  res.end(
1137
1176
  req.__(
1138
1177
  `Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
@@ -1481,8 +1520,9 @@ router.get(
1481
1520
  });
1482
1521
  })
1483
1522
  );
1484
- const buildDialogScript = () => {
1523
+ const buildDialogScript = (cordovaBuilderAvailable) => {
1485
1524
  return `<script>
1525
+ var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1486
1526
  function showEntrySelect(type) {
1487
1527
  for( const currentType of ["view", "page", "pagegroup"]) {
1488
1528
  const tab = $('#' + currentType + 'NavLinkID');
@@ -1519,6 +1559,17 @@ const buildDialogScript = () => {
1519
1559
  }
1520
1560
  </script>`;
1521
1561
  };
1562
+
1563
+ const imageAvailable = async () => {
1564
+ try {
1565
+ const image = new Docker().getImage("saltcorn/cordova-builder");
1566
+ await image.inspect();
1567
+ return true;
1568
+ } catch (e) {
1569
+ return false;
1570
+ }
1571
+ };
1572
+
1522
1573
  /**
1523
1574
  * Build mobile app
1524
1575
  */
@@ -1538,13 +1589,14 @@ router.get(
1538
1589
  );
1539
1590
  const builderSettings =
1540
1591
  getState().getConfig("mobile_builder_settings") || {};
1592
+ const dockerAvailable = await imageAvailable();
1541
1593
  send_admin_page({
1542
1594
  res,
1543
1595
  req,
1544
1596
  active_sub: "Mobile app",
1545
1597
  headers: [
1546
1598
  {
1547
- headerTag: buildDialogScript(),
1599
+ headerTag: buildDialogScript(dockerAvailable),
1548
1600
  },
1549
1601
  ],
1550
1602
  contents: {
@@ -2165,6 +2217,56 @@ router.get(
2165
2217
  )
2166
2218
  )
2167
2219
  )
2220
+ ),
2221
+ div(
2222
+ { class: "row pb-3 pt-3" },
2223
+ div(
2224
+ label(
2225
+ { class: "form-label fw-bold" },
2226
+ req.__("Cordova builder") +
2227
+ a(
2228
+ {
2229
+ href: "javascript:ajax_modal('/admin/help/Cordova Builder?')",
2230
+ },
2231
+ i({ class: "fas fa-question-circle ps-1" })
2232
+ )
2233
+ )
2234
+ ),
2235
+ div(
2236
+ { class: "col-sm-4" },
2237
+ div(
2238
+ {
2239
+ id: "dockerBuilderStatusId",
2240
+ class: "",
2241
+ },
2242
+ dockerAvailable
2243
+ ? span(
2244
+ req.__("installed"),
2245
+ i({ class: "ps-2 fas fa-check text-success" })
2246
+ )
2247
+ : span(
2248
+ req.__("not available"),
2249
+ i({ class: "ps-2 fas fa-times text-danger" })
2250
+ )
2251
+ )
2252
+ ),
2253
+ div(
2254
+ { class: "col-sm-4" },
2255
+ button(
2256
+ {
2257
+ id: "pullCordovaBtnId",
2258
+ type: "button",
2259
+ onClick: `pull_cordova_builder(this);`,
2260
+ class: "btn btn-warning",
2261
+ },
2262
+ req.__("pull")
2263
+ ),
2264
+ span(
2265
+ { role: "button", onClick: "check_cordova_builder()" },
2266
+ span({ class: "ps-3" }, req.__("refresh")),
2267
+ i({ class: "ps-2 fas fa-undo" })
2268
+ )
2269
+ )
2168
2270
  )
2169
2271
  ),
2170
2272
  button(
@@ -2419,6 +2521,48 @@ router.post(
2419
2521
  })
2420
2522
  );
2421
2523
 
2524
+ router.post(
2525
+ "/mobile-app/pull-cordova-builder",
2526
+ isAdmin,
2527
+ error_catcher(async (req, res) => {
2528
+ const state = getState();
2529
+ const child = spawn(
2530
+ "docker",
2531
+ ["image", "pull", "saltcorn/cordova-builder:latest"],
2532
+ {
2533
+ stdio: ["ignore", "pipe", "pipe"],
2534
+ cwd: ".",
2535
+ }
2536
+ );
2537
+ child.stdout.on("data", (data) => {
2538
+ state.log(5, data.toString());
2539
+ });
2540
+ child.stderr.on("data", (data) => {
2541
+ state.log(1, data.toString());
2542
+ });
2543
+ child.on("exit", (exitCode, signal) => {
2544
+ state.log(
2545
+ 2,
2546
+ `"pull cordova-builder exit with code: ${exitCode} and signal: ${signal}`
2547
+ );
2548
+ });
2549
+ child.on("error", (msg) => {
2550
+ state.log(1, `pull cordova-builder error: ${msg}`);
2551
+ });
2552
+
2553
+ res.json({});
2554
+ })
2555
+ );
2556
+
2557
+ router.get(
2558
+ "/mobile-app/check-cordova-builder",
2559
+ isAdmin,
2560
+ error_catcher(async (req, res) => {
2561
+ const installed = await imageAvailable();
2562
+ res.json({ installed });
2563
+ })
2564
+ );
2565
+
2422
2566
  /**
2423
2567
  * Do Clear All
2424
2568
  * @function
@@ -547,7 +547,7 @@ const no_views_logged_in = async (req, res) => {
547
547
  * @returns {Promise<boolean>}
548
548
  */
549
549
  const get_config_response = async (role_id, res, req) => {
550
- const wrap = async (contents, homeCfg, title, description) => {
550
+ const wrap = async (contents, homeCfg, title, description, no_menu) => {
551
551
  if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
552
552
  else
553
553
  res.sendWrap(
@@ -555,6 +555,7 @@ const get_config_response = async (role_id, res, req) => {
555
555
  title: title || "",
556
556
  description: description || "",
557
557
  bodyClass: "page_" + db.sqlsanitize(homeCfg),
558
+ no_menu,
558
559
  },
559
560
  contents
560
561
  );
@@ -574,7 +575,8 @@ const get_config_response = async (role_id, res, req) => {
574
575
  await db_page.run(req.query, { res, req }),
575
576
  homeCfg,
576
577
  db_page.title,
577
- db_page.description
578
+ db_page.description,
579
+ db_page.attributes?.no_menu
578
580
  );
579
581
  else {
580
582
  const group = PageGroup.findOne({ name: homeCfg });
@@ -587,7 +589,8 @@ const get_config_response = async (role_id, res, req) => {
587
589
  await eligible.run(req.query, { res, req }),
588
590
  homeCfg,
589
591
  eligible.title,
590
- eligible.description
592
+ eligible.description,
593
+ eligible.attributes?.no_menu
591
594
  );
592
595
  } else wrap(req.__("%s has no eligible page", group.name), homeCfg);
593
596
  } else res.redirect(homeCfg);
package/routes/plugins.js CHANGED
@@ -1243,7 +1243,12 @@ router.post(
1243
1243
  res.redirect(`/plugins`);
1244
1244
  return;
1245
1245
  }
1246
- await load_plugins.loadAndSaveNewPlugin(plugin, forceReInstall);
1246
+ const msgs = await load_plugins.loadAndSaveNewPlugin(
1247
+ plugin,
1248
+ forceReInstall,
1249
+ undefined,
1250
+ req.__
1251
+ );
1247
1252
  const plugin_module = getState().plugins[name];
1248
1253
  await sleep(1000); // Allow other workers to load this plugin
1249
1254
  await getState().refresh_views();
@@ -1257,9 +1262,11 @@ router.post(
1257
1262
  plugin_db.name
1258
1263
  )
1259
1264
  );
1265
+ if (msgs?.length > 0) req.flash("warning", msgs.join("<br>"));
1260
1266
  res.redirect(`/plugins/configure/${plugin_db.name}`);
1261
1267
  } else {
1262
1268
  req.flash("success", req.__(`Module %s installed`, plugin.name));
1269
+ if (msgs?.length > 0) req.flash("warning", msgs.join("<br>"));
1263
1270
  res.redirect(`/plugins`);
1264
1271
  }
1265
1272
  })
package/routes/tables.js CHANGED
@@ -167,6 +167,10 @@ const tableForm = async (table, req) => {
167
167
  label: req.__("Version history"),
168
168
  sublabel: req.__("Track table data changes over time"),
169
169
  name: "versioned",
170
+ attributes: {
171
+ onChange:
172
+ "if(!this.checked && !confirm('Are you sure? This will delete all history')) {this.checked = true; return false}",
173
+ },
170
174
  type: "Bool",
171
175
  },
172
176
  ...(table.name === "users"