@saltcorn/server 0.9.5-beta.2 → 0.9.5-beta.21

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
@@ -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(
@@ -281,9 +284,23 @@ router.get(
281
284
  backupForm.values.auto_backup_destination = getState().getConfig(
282
285
  "auto_backup_destination"
283
286
  );
287
+ backupForm.values.auto_backup_tenants = getState().getConfig(
288
+ "auto_backup_tenants"
289
+ );
284
290
  backupForm.values.auto_backup_directory = getState().getConfig(
285
291
  "auto_backup_directory"
286
292
  );
293
+ backupForm.values.auto_backup_username = getState().getConfig(
294
+ "auto_backup_username"
295
+ );
296
+ backupForm.values.auto_backup_server =
297
+ getState().getConfig("auto_backup_server");
298
+ backupForm.values.auto_backup_password = getState().getConfig(
299
+ "auto_backup_password"
300
+ );
301
+ backupForm.values.auto_backup_port =
302
+ getState().getConfig("auto_backup_port");
303
+
287
304
  backupForm.values.auto_backup_expire_days = getState().getConfig(
288
305
  "auto_backup_expire_days"
289
306
  );
@@ -673,6 +690,13 @@ const backupFilePrefixForm = (req) =>
673
690
  sublabel: req.__("Backup file prefix"),
674
691
  default: "sc-backup-",
675
692
  },
693
+ {
694
+ type: "Bool",
695
+ label: req.__("History"),
696
+ name: "backup_history",
697
+ sublabel: req.__("Include table history in backup"),
698
+ default: true,
699
+ },
676
700
  ],
677
701
  });
678
702
 
@@ -681,8 +705,10 @@ const backupFilePrefixForm = (req) =>
681
705
  * @param {object} req
682
706
  * @returns {Form} form
683
707
  */
684
- const autoBackupForm = (req) =>
685
- new Form({
708
+ const autoBackupForm = (req) => {
709
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
710
+
711
+ return new Form({
686
712
  action: "/admin/set-auto-backup",
687
713
  onChange: `saveAndContinue(this);$('#btnBackupNow').prop('disabled', $('#inputauto_backup_frequency').val()==='Never');`,
688
714
  noSubmitButton: true,
@@ -708,7 +734,47 @@ const autoBackupForm = (req) =>
708
734
  name: "auto_backup_destination",
709
735
  required: true,
710
736
  showIf: { auto_backup_frequency: ["Daily", "Weekly"] },
711
- attributes: { options: ["Saltcorn files", "Local directory"] },
737
+ attributes: {
738
+ auto_backup_frequency: ["Daily", "Weekly"],
739
+ options: ["Saltcorn files", "Local directory", "SFTP server"],
740
+ },
741
+ },
742
+ {
743
+ type: "String",
744
+ label: req.__("Server host"),
745
+ name: "auto_backup_server",
746
+ showIf: {
747
+ auto_backup_frequency: ["Daily", "Weekly"],
748
+ auto_backup_destination: "SFTP server",
749
+ },
750
+ },
751
+ {
752
+ type: "String",
753
+ label: req.__("Username"),
754
+ name: "auto_backup_username",
755
+ showIf: {
756
+ auto_backup_frequency: ["Daily", "Weekly"],
757
+ auto_backup_destination: "SFTP server",
758
+ },
759
+ },
760
+ {
761
+ type: "String",
762
+ label: req.__("Password"),
763
+ fieldview: "password",
764
+ name: "auto_backup_password",
765
+ showIf: {
766
+ auto_backup_frequency: ["Daily", "Weekly"],
767
+ auto_backup_destination: "SFTP server",
768
+ },
769
+ },
770
+ {
771
+ type: "Integer",
772
+ label: req.__("Port"),
773
+ name: "auto_backup_port",
774
+ showIf: {
775
+ auto_backup_frequency: ["Daily", "Weekly"],
776
+ auto_backup_destination: "SFTP server",
777
+ },
712
778
  },
713
779
  {
714
780
  type: "String",
@@ -720,6 +786,19 @@ const autoBackupForm = (req) =>
720
786
  //auto_backup_destination: "Local directory",
721
787
  },
722
788
  },
789
+ {
790
+ type: "String",
791
+ label: req.__("Retain local directory"),
792
+ name: "auto_backup_retain_local_directory",
793
+ sublabel: req.__(
794
+ "Retain a local backup copy in this directory (optional)"
795
+ ),
796
+ showIf: {
797
+ auto_backup_frequency: ["Daily", "Weekly"],
798
+ auto_backup_destination: "SFTP server",
799
+ //auto_backup_destination: "Local directory",
800
+ },
801
+ },
723
802
  {
724
803
  type: "Integer",
725
804
  label: req.__("Expiration in days"),
@@ -732,6 +811,19 @@ const autoBackupForm = (req) =>
732
811
  auto_backup_destination: "Local directory",
733
812
  },
734
813
  },
814
+ ...(isRoot
815
+ ? [
816
+ {
817
+ type: "Bool",
818
+ label: req.__("All tenants"),
819
+ sublabel: req.__("Also backup all tenants"),
820
+ name: "auto_backup_tenants",
821
+ showIf: {
822
+ auto_backup_frequency: ["Daily", "Weekly"],
823
+ },
824
+ },
825
+ ]
826
+ : []),
735
827
  {
736
828
  type: "Bool",
737
829
  label: req.__("Include Event Logs"),
@@ -743,6 +835,7 @@ const autoBackupForm = (req) =>
743
835
  },
744
836
  ],
745
837
  });
838
+ };
746
839
 
747
840
  /**
748
841
  * Snapshot Form
@@ -1104,6 +1197,28 @@ router.post(
1104
1197
  })
1105
1198
  );
1106
1199
 
1200
+ const pullCordovaBuilder = (req, res) => {
1201
+ const child = spawn("docker", ["pull", "saltcorn/cordova-builder"], {
1202
+ stdio: ["ignore", "pipe", "pipe"],
1203
+ });
1204
+ return new Promise((resolve, reject) => {
1205
+ child.stdout.on("data", (data) => {
1206
+ res.write(data);
1207
+ });
1208
+ child.stderr?.on("data", (data) => {
1209
+ res.write(data);
1210
+ });
1211
+ child.on("exit", function (code, signal) {
1212
+ resolve(code);
1213
+ });
1214
+ child.on("error", (msg) => {
1215
+ const message = msg.message ? msg.message : msg.code;
1216
+ res.write(req.__("Error: ") + message + "\n");
1217
+ resolve(msg.code);
1218
+ });
1219
+ });
1220
+ };
1221
+
1107
1222
  /**
1108
1223
  * Do Upgrade
1109
1224
  * @name post/upgrade
@@ -1132,7 +1247,14 @@ router.post(
1132
1247
  child.stderr?.on("data", (data) => {
1133
1248
  res.write(data);
1134
1249
  });
1135
- child.on("exit", function (code, signal) {
1250
+ child.on("exit", async function (code, signal) {
1251
+ if (code === 0) {
1252
+ res.write(
1253
+ req.__("Pulling the cordova-builder docker image...") + "\n"
1254
+ );
1255
+ const pullCode = await pullCordovaBuilder(req, res);
1256
+ res.write(req.__("Pull done with code %s", pullCode) + "\n");
1257
+ }
1136
1258
  res.end(
1137
1259
  req.__(
1138
1260
  `Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
@@ -1481,8 +1603,9 @@ router.get(
1481
1603
  });
1482
1604
  })
1483
1605
  );
1484
- const buildDialogScript = () => {
1606
+ const buildDialogScript = (cordovaBuilderAvailable) => {
1485
1607
  return `<script>
1608
+ var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1486
1609
  function showEntrySelect(type) {
1487
1610
  for( const currentType of ["view", "page", "pagegroup"]) {
1488
1611
  const tab = $('#' + currentType + 'NavLinkID');
@@ -1505,20 +1628,20 @@ const buildDialogScript = () => {
1505
1628
 
1506
1629
  function handleMessages() {
1507
1630
  notifyAlert("Building the app, please wait.", true)
1508
- ${
1509
- getState().getConfig("apple_team_id") &&
1510
- getState().getConfig("apple_team_id") !== "null"
1511
- ? ""
1512
- : `
1513
- if ($("#iOSCheckboxId")[0].checked) {
1514
- notifyAlert(
1515
- "No 'Apple Team ID' is configured, I will try to build a project for the iOS simulator."
1516
- );
1517
- }`
1518
- }
1519
1631
  }
1520
1632
  </script>`;
1521
1633
  };
1634
+
1635
+ const imageAvailable = async () => {
1636
+ try {
1637
+ const image = new Docker().getImage("saltcorn/cordova-builder");
1638
+ await image.inspect();
1639
+ return true;
1640
+ } catch (e) {
1641
+ return false;
1642
+ }
1643
+ };
1644
+
1522
1645
  /**
1523
1646
  * Build mobile app
1524
1647
  */
@@ -1538,13 +1661,14 @@ router.get(
1538
1661
  );
1539
1662
  const builderSettings =
1540
1663
  getState().getConfig("mobile_builder_settings") || {};
1664
+ const dockerAvailable = await imageAvailable();
1541
1665
  send_admin_page({
1542
1666
  res,
1543
1667
  req,
1544
1668
  active_sub: "Mobile app",
1545
1669
  headers: [
1546
1670
  {
1547
- headerTag: buildDialogScript(),
1671
+ headerTag: buildDialogScript(dockerAvailable),
1548
1672
  },
1549
1673
  ],
1550
1674
  contents: {
@@ -2165,6 +2289,80 @@ router.get(
2165
2289
  )
2166
2290
  )
2167
2291
  )
2292
+ ),
2293
+ div(
2294
+ { class: "row pb-3 pt-3" },
2295
+ div(
2296
+ { class: "col-sm-8" },
2297
+ label(
2298
+ {
2299
+ for: "splashPageInputId",
2300
+ class: "form-label fw-bold",
2301
+ },
2302
+ req.__("Apple Team ID")
2303
+ ),
2304
+ input({
2305
+ type: "text",
2306
+ class: "form-control",
2307
+ name: "appleTeamId",
2308
+ id: "appleTeamIdInputId",
2309
+ value:
2310
+ builderSettings.appleTeamId ||
2311
+ getState().getConfig("apple_team_id") ||
2312
+ "",
2313
+ placeholder: req.__("Please enter your Apple Team ID"),
2314
+ })
2315
+ )
2316
+ ),
2317
+ div(
2318
+ { class: "row pb-3 pt-2" },
2319
+ div(
2320
+ label(
2321
+ { class: "form-label fw-bold" },
2322
+ req.__("Cordova builder") +
2323
+ a(
2324
+ {
2325
+ href: "javascript:ajax_modal('/admin/help/Cordova Builder?')",
2326
+ },
2327
+ i({ class: "fas fa-question-circle ps-1" })
2328
+ )
2329
+ )
2330
+ ),
2331
+ div(
2332
+ { class: "col-sm-4" },
2333
+ div(
2334
+ {
2335
+ id: "dockerBuilderStatusId",
2336
+ class: "",
2337
+ },
2338
+ dockerAvailable
2339
+ ? span(
2340
+ req.__("installed"),
2341
+ i({ class: "ps-2 fas fa-check text-success" })
2342
+ )
2343
+ : span(
2344
+ req.__("not available"),
2345
+ i({ class: "ps-2 fas fa-times text-danger" })
2346
+ )
2347
+ )
2348
+ ),
2349
+ div(
2350
+ { class: "col-sm-4" },
2351
+ button(
2352
+ {
2353
+ id: "pullCordovaBtnId",
2354
+ type: "button",
2355
+ onClick: `pull_cordova_builder(this);`,
2356
+ class: "btn btn-warning",
2357
+ },
2358
+ req.__("pull")
2359
+ ),
2360
+ span(
2361
+ { role: "button", onClick: "check_cordova_builder()" },
2362
+ span({ class: "ps-3" }, req.__("refresh")),
2363
+ i({ class: "ps-2 fas fa-undo" })
2364
+ )
2365
+ )
2168
2366
  )
2169
2367
  ),
2170
2368
  button(
@@ -2275,6 +2473,7 @@ router.post(
2275
2473
  allowOfflineMode,
2276
2474
  synchedTables,
2277
2475
  includedPlugins,
2476
+ appleTeamId,
2278
2477
  } = req.body;
2279
2478
  if (!includedPlugins) includedPlugins = [];
2280
2479
  if (!synchedTables) synchedTables = [];
@@ -2317,10 +2516,9 @@ router.post(
2317
2516
  if (androidPlatform) spawnParams.push("-p", "android");
2318
2517
  if (iOSPlatform) {
2319
2518
  spawnParams.push("-p", "ios");
2320
- const teamId = getState().getConfig("apple_team_id");
2321
- if (!teamId || teamId === "null") {
2519
+ if (!appleTeamId || appleTeamId === "null")
2322
2520
  spawnParams.push("--buildForEmulator");
2323
- }
2521
+ else spawnParams.push("--appleTeamId", appleTeamId);
2324
2522
  }
2325
2523
  if (appName) spawnParams.push("--appName", appName);
2326
2524
  if (appVersion) spawnParams.push("--appVersion", appVersion);
@@ -2365,6 +2563,7 @@ router.post(
2365
2563
  synchedTables: synchedTables,
2366
2564
  includedPlugins: includedPlugins,
2367
2565
  excludedPlugins,
2566
+ appleTeamId,
2368
2567
  });
2369
2568
  // end http call, return the out directory name
2370
2569
  // the gui polls for results
@@ -2419,6 +2618,48 @@ router.post(
2419
2618
  })
2420
2619
  );
2421
2620
 
2621
+ router.post(
2622
+ "/mobile-app/pull-cordova-builder",
2623
+ isAdmin,
2624
+ error_catcher(async (req, res) => {
2625
+ const state = getState();
2626
+ const child = spawn(
2627
+ "docker",
2628
+ ["image", "pull", "saltcorn/cordova-builder:latest"],
2629
+ {
2630
+ stdio: ["ignore", "pipe", "pipe"],
2631
+ cwd: ".",
2632
+ }
2633
+ );
2634
+ child.stdout.on("data", (data) => {
2635
+ state.log(5, data.toString());
2636
+ });
2637
+ child.stderr.on("data", (data) => {
2638
+ state.log(1, data.toString());
2639
+ });
2640
+ child.on("exit", (exitCode, signal) => {
2641
+ state.log(
2642
+ 2,
2643
+ `"pull cordova-builder exit with code: ${exitCode} and signal: ${signal}`
2644
+ );
2645
+ });
2646
+ child.on("error", (msg) => {
2647
+ state.log(1, `pull cordova-builder error: ${msg}`);
2648
+ });
2649
+
2650
+ res.json({});
2651
+ })
2652
+ );
2653
+
2654
+ router.get(
2655
+ "/mobile-app/check-cordova-builder",
2656
+ isAdmin,
2657
+ error_catcher(async (req, res) => {
2658
+ const installed = await imageAvailable();
2659
+ res.json({ installed });
2660
+ })
2661
+ );
2662
+
2422
2663
  /**
2423
2664
  * Do Clear All
2424
2665
  * @function
@@ -2628,6 +2869,7 @@ admin_config_route({
2628
2869
  });
2629
2870
  },
2630
2871
  response(form, req, res) {
2872
+ const code_pages = getState().getConfig("function_code_pages", {});
2631
2873
  send_admin_page({
2632
2874
  res,
2633
2875
  req,
@@ -2654,11 +2896,138 @@ admin_config_route({
2654
2896
  ),
2655
2897
  ],
2656
2898
  },
2899
+ {
2900
+ type: "card",
2901
+ title: req.__("Constants and function code"),
2902
+ contents: [
2903
+ div(
2904
+ Object.keys(code_pages)
2905
+ .map((k) =>
2906
+ a(
2907
+ {
2908
+ href: `/admin/edit-codepage/${encodeURIComponent(k)}`,
2909
+ class: "",
2910
+ },
2911
+ k
2912
+ )
2913
+ )
2914
+ .join(" | "),
2915
+ button(
2916
+ {
2917
+ class: "btn btn-secondary btn-sm d-block mt-2",
2918
+ onclick: `location.href='/admin/edit-codepage/'+prompt('Name of the new page')`,
2919
+ },
2920
+ i({ class: "fas fa-plus me-1" }),
2921
+ "Add page"
2922
+ )
2923
+ ),
2924
+ ],
2925
+ },
2657
2926
  ],
2658
2927
  },
2659
2928
  });
2660
2929
  },
2661
2930
  });
2931
+
2932
+ router.get(
2933
+ "/edit-codepage/:name",
2934
+ isAdmin,
2935
+ error_catcher(async (req, res) => {
2936
+ const { name } = req.params;
2937
+ const code_pages = getState().getConfig("function_code_pages", {});
2938
+ const existing = code_pages[name] || "";
2939
+ const form = new Form({
2940
+ action: `/admin/edit-codepage/${encodeURIComponent(name)}`,
2941
+ onChange: "saveAndContinue(this)",
2942
+ values: { code: existing },
2943
+ noSubmitButton: true,
2944
+ labelCols: 0,
2945
+ additionalButtons: [
2946
+ {
2947
+ label: req.__("Delete code page"),
2948
+ class: "btn btn-outline-danger btn-sm",
2949
+ onclick: `if(confirm('Are you sure you would like to delete this code page?'))ajax_post('/admin/delete-codepage/${encodeURIComponent(
2950
+ name
2951
+ )}')`,
2952
+ },
2953
+ ],
2954
+ fields: [
2955
+ {
2956
+ name: "code",
2957
+ form_name: "code",
2958
+ label: "Code",
2959
+ sublabel:
2960
+ "Only functions declared as <code>function name(...) {...}</code> or <code>async function name(...) {...}</code> will be available in formulae and code actions. Declare a constant <code>k</code> as <code>globalThis.k = ...</code> In scope: " +
2961
+ a(
2962
+ {
2963
+ href: "https://saltcorn.github.io/saltcorn/classes/_saltcorn_data.models.Table-1.html",
2964
+ target: "_blank",
2965
+ },
2966
+ "Table"
2967
+ ),
2968
+ input_type: "code",
2969
+ attributes: { mode: "text/javascript" },
2970
+ class: "validate-statements",
2971
+ validator(s) {
2972
+ try {
2973
+ let AsyncFunction = Object.getPrototypeOf(
2974
+ async function () {}
2975
+ ).constructor;
2976
+ AsyncFunction(s);
2977
+ return true;
2978
+ } catch (e) {
2979
+ return e.message;
2980
+ }
2981
+ },
2982
+ },
2983
+ ],
2984
+ });
2985
+
2986
+ send_admin_page({
2987
+ res,
2988
+ req,
2989
+ active_sub: "Development",
2990
+ sub2_page: req.__(`%s code page`, name),
2991
+ contents: {
2992
+ type: "card",
2993
+ title: req.__(`%s code page`, name),
2994
+ contents: [renderForm(form, req.csrfToken())],
2995
+ },
2996
+ });
2997
+ })
2998
+ );
2999
+
3000
+ router.post(
3001
+ "/edit-codepage/:name",
3002
+ isAdmin,
3003
+ error_catcher(async (req, res) => {
3004
+ const { name } = req.params;
3005
+ const code_pages = getState().getConfigCopy("function_code_pages", {});
3006
+
3007
+ const code = req.body.code;
3008
+ await getState().setConfig("function_code_pages", {
3009
+ ...code_pages,
3010
+ [name]: code,
3011
+ });
3012
+ await getState().refresh_codepages();
3013
+
3014
+ res.json({ success: true });
3015
+ })
3016
+ );
3017
+ router.post(
3018
+ "/delete-codepage/:name",
3019
+ isAdmin,
3020
+ error_catcher(async (req, res) => {
3021
+ const { name } = req.params;
3022
+ const code_pages = getState().getConfigCopy("function_code_pages", {});
3023
+ delete code_pages[name];
3024
+ await getState().setConfig("function_code_pages", code_pages);
3025
+ await getState().refresh_codepages();
3026
+
3027
+ res.json({ goto: `/admin/dev` });
3028
+ })
3029
+ );
3030
+
2662
3031
  /**
2663
3032
  * Notifications
2664
3033
  */
package/routes/api.js CHANGED
@@ -373,7 +373,10 @@ router.all(
373
373
  res.redirect(resp.goto);
374
374
  else if (req.headers?.scgotourl)
375
375
  res.redirect(req.headers?.scgotourl);
376
- else res.json({ success: true, data: resp });
376
+ else {
377
+ if (trigger.configuration?._raw_output) res.json({ resp });
378
+ else res.json({ success: true, data: resp });
379
+ }
377
380
  } catch (e) {
378
381
  Crash.create(e, req);
379
382
  res.status(400).json({ success: false, error: e.message });
@@ -226,7 +226,7 @@ const tagsDropdown = (tags, altHeader) =>
226
226
  { class: "dropdown" },
227
227
  div(
228
228
  {
229
- class: "link-style",
229
+ class: "link-style text-nowrap",
230
230
  "data-boundary": "viewport",
231
231
  type: "button",
232
232
  id: "tagsselector",
package/routes/fields.js CHANGED
@@ -955,8 +955,11 @@ router.post(
955
955
  }
956
956
  return;
957
957
  }
958
+ }
959
+ if (targetField.type === "File") {
960
+ fv = getState().fileviews[fieldview];
958
961
  } else {
959
- fv = targetField.type.fieldviews[fieldview];
962
+ fv = targetField.type?.fieldviews?.[fieldview];
960
963
  if (!fv)
961
964
  fv =
962
965
  targetField.type.fieldviews.show ||
@@ -994,13 +997,15 @@ router.post(
994
997
  if (oldRow) {
995
998
  const value = oldRow[kpath[kpath.length - 1]];
996
999
  //TODO run fieldview
997
- res.send(
998
- typeof value === "string"
999
- ? value
1000
- : value?.toString
1001
- ? value.toString()
1002
- : `${value}`
1003
- );
1000
+ if (value === null || typeof value === "undefined") res.send("");
1001
+ else
1002
+ res.send(
1003
+ typeof value === "string"
1004
+ ? value
1005
+ : value?.toString
1006
+ ? value.toString()
1007
+ : `${value}`
1008
+ );
1004
1009
  return;
1005
1010
  }
1006
1011
  }
@@ -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/menu.js CHANGED
@@ -19,7 +19,7 @@ const { save_menu_items } = require("@saltcorn/data/models/config");
19
19
  const db = require("@saltcorn/data/db");
20
20
 
21
21
  const { renderForm } = require("@saltcorn/markup");
22
- const { script, domReady, div, ul } = require("@saltcorn/markup/tags");
22
+ const { script, domReady, div, ul, i } = require("@saltcorn/markup/tags");
23
23
  const { send_infoarch_page } = require("../markup/admin.js");
24
24
  const Table = require("@saltcorn/data/models/table");
25
25
  const Trigger = require("@saltcorn/data/models/trigger");
@@ -368,7 +368,7 @@ const menuEditorScript = (menu_items) => `
368
368
  iconPicker: iconPickerOptions,
369
369
  getLabelText: (item) => item?.text || item?.type,
370
370
  labelEdit: 'Edit&nbsp;<i class="fas fa-edit clickable"></i>',
371
- maxLevel: 1 // (Optional) Default is -1 (no level limit)
371
+ maxLevel: 2 // (Optional) Default is -1 (no level limit)
372
372
  // Valid levels are from [0, 1, 2, 3,...N]
373
373
  });
374
374
  editor.setForm($('#menuForm'));
@@ -446,7 +446,16 @@ router.get(
446
446
  above: [
447
447
  {
448
448
  besides: [
449
- div(ul({ id: "myEditor", class: "sortableLists list-group" })),
449
+ div(
450
+ ul({ id: "myEditor", class: "sortableLists list-group" }),
451
+ div(
452
+ i(
453
+ req.__(
454
+ "Some themes support only one level of menu nesting."
455
+ )
456
+ )
457
+ )
458
+ ),
450
459
  div(
451
460
  renderForm(form, req.csrfToken()),
452
461
  script(domReady(menuEditorScript(menu_items)))