@saltcorn/server 0.9.6-beta.9 → 0.9.6

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.
Files changed (47) hide show
  1. package/app.js +9 -2
  2. package/auth/admin.js +51 -52
  3. package/auth/roleadmin.js +6 -2
  4. package/auth/routes.js +28 -10
  5. package/auth/testhelp.js +86 -0
  6. package/help/Field label.tmd +11 -0
  7. package/help/Field types.tmd +39 -0
  8. package/help/Inclusion Formula.tmd +38 -0
  9. package/help/Ownership field.tmd +76 -0
  10. package/help/Ownership formula.tmd +75 -0
  11. package/help/Table roles.tmd +20 -0
  12. package/help/User groups.tmd +35 -0
  13. package/help/User roles.tmd +30 -0
  14. package/load_plugins.js +28 -4
  15. package/locales/en.json +28 -1
  16. package/locales/it.json +3 -2
  17. package/markup/forms.js +5 -1
  18. package/package.json +9 -9
  19. package/public/log_viewer_utils.js +32 -0
  20. package/public/mermaid.min.js +705 -306
  21. package/public/saltcorn-builder.css +23 -0
  22. package/public/saltcorn-common.js +195 -71
  23. package/public/saltcorn.css +72 -0
  24. package/public/saltcorn.js +78 -0
  25. package/restart_watcher.js +1 -0
  26. package/routes/actions.js +27 -0
  27. package/routes/admin.js +180 -66
  28. package/routes/api.js +6 -0
  29. package/routes/common_lists.js +42 -32
  30. package/routes/fields.js +9 -1
  31. package/routes/homepage.js +2 -0
  32. package/routes/menu.js +69 -4
  33. package/routes/notifications.js +90 -10
  34. package/routes/pageedit.js +18 -13
  35. package/routes/plugins.js +5 -1
  36. package/routes/search.js +10 -4
  37. package/routes/tables.js +47 -27
  38. package/routes/tenant.js +4 -15
  39. package/routes/utils.js +20 -6
  40. package/routes/viewedit.js +11 -7
  41. package/serve.js +27 -5
  42. package/tests/edit.test.js +426 -0
  43. package/tests/fields.test.js +21 -0
  44. package/tests/filter.test.js +68 -0
  45. package/tests/page.test.js +2 -2
  46. package/tests/sync.test.js +59 -0
  47. package/wrapper.js +4 -1
package/routes/admin.js CHANGED
@@ -108,6 +108,7 @@ const stream = require("stream");
108
108
  const Crash = require("@saltcorn/data/models/crash");
109
109
  const { get_help_markup } = require("../help/index.js");
110
110
  const Docker = require("dockerode");
111
+ const npmFetch = require("npm-registry-fetch");
111
112
 
112
113
  const router = new Router();
113
114
  module.exports = router;
@@ -1004,6 +1005,7 @@ router.get(
1004
1005
  "custom_ssl_certificate",
1005
1006
  false
1006
1007
  );
1008
+ const rndid = `bs${Math.round(Math.random() * 100000)}`;
1007
1009
  let expiry = "";
1008
1010
  if (custom_ssl_certificate && X509Certificate) {
1009
1011
  const { validTo } = new X509Certificate(custom_ssl_certificate);
@@ -1062,7 +1064,8 @@ router.get(
1062
1064
  " ",
1063
1065
  req.__("Clear all"),
1064
1066
  " »"
1065
- )
1067
+ ),
1068
+ hr()
1066
1069
  ),
1067
1070
  },
1068
1071
  {
@@ -1075,32 +1078,45 @@ router.get(
1075
1078
  tr(
1076
1079
  th(req.__("Saltcorn version")),
1077
1080
  td(
1078
- packagejson.version +
1079
- (isRoot && can_update
1080
- ? post_btn(
1081
- "/admin/upgrade",
1082
- req.__("Upgrade"),
1083
- req.csrfToken(),
1084
- {
1085
- btnClass: "btn-primary btn-sm",
1086
- formClass: "d-inline",
1087
- }
1088
- )
1089
- : isRoot && is_latest
1090
- ? span(
1091
- { class: "badge bg-primary ms-2" },
1092
- req.__("Latest")
1093
- ) +
1094
- post_btn(
1095
- "/admin/check-for-upgrade",
1096
- req.__("Check for updates"),
1097
- req.csrfToken(),
1098
- {
1099
- btnClass: "btn-primary btn-sm px-1 py-0",
1100
- formClass: "d-inline",
1101
- }
1102
- )
1103
- : "")
1081
+ packagejson.version,
1082
+ isRoot && can_update
1083
+ ? post_btn(
1084
+ "/admin/upgrade",
1085
+ req.__("Upgrade"),
1086
+ req.csrfToken(),
1087
+ {
1088
+ btnClass: "btn-primary btn-sm",
1089
+ formClass: "d-inline",
1090
+ }
1091
+ )
1092
+ : isRoot && is_latest
1093
+ ? span(
1094
+ { class: "badge bg-primary ms-2" },
1095
+ req.__("Latest")
1096
+ ) +
1097
+ post_btn(
1098
+ "/admin/check-for-upgrade",
1099
+ req.__("Check updates"),
1100
+ req.csrfToken(),
1101
+ {
1102
+ btnClass: "btn-primary btn-sm px-1 py-0",
1103
+ formClass: "d-inline",
1104
+ }
1105
+ )
1106
+ : "",
1107
+ !git_commit &&
1108
+ a(
1109
+ {
1110
+ id: rndid,
1111
+ class: "btn btn-sm btn-secondary ms-1 px-1 py-0",
1112
+ onClick: "press_store_button(this, true)",
1113
+ href:
1114
+ `javascript:ajax_modal('/admin/install_dialog', ` +
1115
+ `{ onOpen: () => { restore_old_button('${rndid}'); }, ` +
1116
+ ` onError: (res) => { selectVersionError(res, '${rndid}') } });`,
1117
+ },
1118
+ req.__("Choose version")
1119
+ )
1104
1120
  )
1105
1121
  ),
1106
1122
  git_commit &&
@@ -1222,6 +1238,137 @@ const pullCordovaBuilder = (req, res) => {
1222
1238
  });
1223
1239
  };
1224
1240
 
1241
+ /*
1242
+ * fetch available saltcorn versions and show a dialog to select one
1243
+ */
1244
+ router.get(
1245
+ "/install_dialog",
1246
+ isAdmin,
1247
+ error_catcher(async (req, res) => {
1248
+ try {
1249
+ const pkgInfo = await npmFetch.json(
1250
+ "https://registry.npmjs.org/@saltcorn/cli"
1251
+ );
1252
+ if (!pkgInfo?.versions)
1253
+ throw new Error(req.__("Unable to fetch versions"));
1254
+ const versions = Object.keys(pkgInfo.versions);
1255
+ if (versions.length === 0) throw new Error(req.__("No versions found"));
1256
+ res.set("Page-Title", req.__("%s versions", "Saltcorn"));
1257
+ let selected = packagejson.version;
1258
+ res.send(
1259
+ form(
1260
+ {
1261
+ action: `/admin/install`,
1262
+ method: "post",
1263
+ },
1264
+ input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
1265
+ div(
1266
+ { class: "form-group" },
1267
+ label(
1268
+ {
1269
+ for: "version_select",
1270
+ class: "form-label fw-bold",
1271
+ },
1272
+ req.__("Version")
1273
+ ),
1274
+ select(
1275
+ {
1276
+ id: "version_select",
1277
+ class: "form-control form-select",
1278
+ name: "version",
1279
+ },
1280
+ versions.map((version) =>
1281
+ option({
1282
+ id: `${version}_opt`,
1283
+ value: version,
1284
+ label: version,
1285
+ selected: version === selected,
1286
+ })
1287
+ )
1288
+ )
1289
+ ),
1290
+ div(
1291
+ { class: "d-flex justify-content-end" },
1292
+ button(
1293
+ {
1294
+ type: "button",
1295
+ class: "btn btn-secondary me-2",
1296
+ "data-bs-dismiss": "modal",
1297
+ },
1298
+ req.__("Close")
1299
+ ),
1300
+ button(
1301
+ {
1302
+ type: "submit",
1303
+ class: "btn btn-primary",
1304
+ onClick: "press_store_button(this)",
1305
+ },
1306
+ req.__("Install")
1307
+ )
1308
+ )
1309
+ )
1310
+ );
1311
+ } catch (error) {
1312
+ getState().log(
1313
+ 2,
1314
+ `GET /install_dialog: ${error.message || "unknown error"}`
1315
+ );
1316
+ return res.status(500).json({ error: error.message || "unknown error" });
1317
+ }
1318
+ })
1319
+ );
1320
+
1321
+ const doInstall = async (req, res, version, runPull) => {
1322
+ if (db.getTenantSchema() !== db.connectObj.default_schema) {
1323
+ req.flash("error", req.__("Not possible for tenant"));
1324
+ res.redirect("/admin");
1325
+ } else {
1326
+ res.write(
1327
+ version === "latest"
1328
+ ? req.__("Starting upgrade, please wait...\n")
1329
+ : req.__("Installing %s, please wait...\n", version)
1330
+ );
1331
+ const child = spawn(
1332
+ "npm",
1333
+ ["install", "-g", `@saltcorn/cli@${version}`, "--unsafe"],
1334
+ {
1335
+ stdio: ["ignore", "pipe", "pipe"],
1336
+ }
1337
+ );
1338
+ child.stdout.on("data", (data) => {
1339
+ res.write(data);
1340
+ });
1341
+ child.stderr?.on("data", (data) => {
1342
+ res.write(data);
1343
+ });
1344
+ child.on("exit", async function (code, signal) {
1345
+ if (code === 0 && runPull) {
1346
+ res.write(req.__("Pulling the cordova-builder docker image...") + "\n");
1347
+ const pullCode = await pullCordovaBuilder(req, res);
1348
+ res.write(req.__("Pull done with code %s", pullCode) + "\n");
1349
+ }
1350
+ res.end(
1351
+ version === "latest"
1352
+ ? req.__(
1353
+ `Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
1354
+ )
1355
+ : req.__(
1356
+ `Install done with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
1357
+ )
1358
+ );
1359
+ setTimeout(() => {
1360
+ getState().processSend("RestartServer");
1361
+ process.exit(0);
1362
+ }, 100);
1363
+ });
1364
+ }
1365
+ };
1366
+
1367
+ router.post("/install", isAdmin, async (req, res) => {
1368
+ const { version } = req.body;
1369
+ await doInstall(req, res, version, false);
1370
+ });
1371
+
1225
1372
  /**
1226
1373
  * Do Upgrade
1227
1374
  * @name post/upgrade
@@ -1232,43 +1379,7 @@ router.post(
1232
1379
  "/upgrade",
1233
1380
  isAdmin,
1234
1381
  error_catcher(async (req, res) => {
1235
- if (db.getTenantSchema() !== db.connectObj.default_schema) {
1236
- req.flash("error", req.__("Not possible for tenant"));
1237
- res.redirect("/admin");
1238
- } else {
1239
- res.write(req.__("Starting upgrade, please wait...\n"));
1240
- const child = spawn(
1241
- "npm",
1242
- ["install", "-g", "@saltcorn/cli@latest", "--unsafe"],
1243
- {
1244
- stdio: ["ignore", "pipe", "pipe"],
1245
- }
1246
- );
1247
- child.stdout.on("data", (data) => {
1248
- res.write(data);
1249
- });
1250
- child.stderr?.on("data", (data) => {
1251
- res.write(data);
1252
- });
1253
- child.on("exit", async function (code, signal) {
1254
- if (code === 0) {
1255
- res.write(
1256
- req.__("Pulling the cordova-builder docker image...") + "\n"
1257
- );
1258
- const pullCode = await pullCordovaBuilder(req, res);
1259
- res.write(req.__("Pull done with code %s", pullCode) + "\n");
1260
- }
1261
- res.end(
1262
- req.__(
1263
- `Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
1264
- )
1265
- );
1266
- setTimeout(() => {
1267
- getState().processSend("RestartServer");
1268
- process.exit(0);
1269
- }, 100);
1270
- });
1271
- }
1382
+ await doInstall(req, res, "latest", true);
1272
1383
  })
1273
1384
  );
1274
1385
  /**
@@ -1740,8 +1851,11 @@ router.get(
1740
1851
  div({ class: "col-sm-4 fw-bold" }, req.__("Platform")),
1741
1852
  div(
1742
1853
  {
1743
- class:
1744
- "col-sm-1 fw-bold d-flex justify-content-center d-none",
1854
+ class: `col-sm-1 fw-bold d-flex justify-content-center ${
1855
+ builderSettings.androidPlatform !== "on"
1856
+ ? "d-none"
1857
+ : ""
1858
+ }`,
1745
1859
  id: "dockerLabelId",
1746
1860
  },
1747
1861
  req.__("docker")
package/routes/api.js CHANGED
@@ -260,6 +260,12 @@ router.get(
260
260
  );
261
261
  if (!table) {
262
262
  getState().log(3, `API get ${tableName} table not found`);
263
+ getState().log(
264
+ 6,
265
+ `API get failure additonal info: URL=${req.originalUrl}${
266
+ getState().getConfig("log_ip_address", false) ? ` IP=${req.ip}` : ""
267
+ }`
268
+ );
263
269
  res.status(404).json({ error: req.__("Not found") });
264
270
  return;
265
271
  }
@@ -304,6 +304,18 @@ const viewsList = async (
304
304
  ? `set_state_field('_sortby', 'name', this)`
305
305
  : undefined,
306
306
  },
307
+ {
308
+ label: "",
309
+ key: (r) =>
310
+ r.id && r.viewtemplateObj?.configuration_workflow
311
+ ? link(
312
+ `/viewedit/config/${encodeURIComponent(
313
+ r.name
314
+ )}${on_done_redirect_str}`,
315
+ req.__("Configure")
316
+ )
317
+ : "",
318
+ },
307
319
  ...(tagId
308
320
  ? []
309
321
  : [
@@ -340,18 +352,6 @@ const viewsList = async (
340
352
  ? editViewRoleForm(row, roles, req, on_done_redirect_str)
341
353
  : "admin",
342
354
  },
343
- {
344
- label: "",
345
- key: (r) =>
346
- r.id && r.viewtemplateObj?.configuration_workflow
347
- ? link(
348
- `/viewedit/config/${encodeURIComponent(
349
- r.name
350
- )}${on_done_redirect_str}`,
351
- req.__("Configure")
352
- )
353
- : "",
354
- },
355
355
  !tagId
356
356
  ? {
357
357
  label: "",
@@ -428,13 +428,6 @@ const page_dropdown = (page, req) =>
428
428
  },
429
429
  '<i class="fas fa-running"></i>&nbsp;' + req.__("Run")
430
430
  ),
431
- a(
432
- {
433
- class: "dropdown-item",
434
- href: `/pageedit/edit-properties/${encodeURIComponent(page.name)}`,
435
- },
436
- '<i class="fas fa-edit"></i>&nbsp;' + req.__("Edit properties")
437
- ),
438
431
  post_dropdown_item(
439
432
  `/pageedit/add-to-menu/${page.id}`,
440
433
  '<i class="fas fa-bars"></i>&nbsp;' + req.__("Add to menu"),
@@ -507,6 +500,22 @@ const getPageList = async (
507
500
  label: req.__("Name"),
508
501
  key: (r) => link(`/page/${encodeURIComponent(r.name)}`, r.name),
509
502
  },
503
+ {
504
+ label: "",
505
+ key: (r) =>
506
+ link(
507
+ `/pageedit/edit/${encodeURIComponent(r.name)}`,
508
+ req.__("Configure")
509
+ ),
510
+ },
511
+ {
512
+ label: "",
513
+ key: (r) =>
514
+ link(
515
+ `/pageedit/edit-properties/${encodeURIComponent(r.name)}`,
516
+ req.__("Edit")
517
+ ),
518
+ },
510
519
  ...(tagId
511
520
  ? []
512
521
  : [
@@ -522,11 +531,7 @@ const getPageList = async (
522
531
  label: req.__("Role to access"),
523
532
  key: (row) => editPageRoleForm(row, roles, req),
524
533
  },
525
- {
526
- label: req.__("Edit"),
527
- key: (r) =>
528
- link(`/pageedit/edit/${encodeURIComponent(r.name)}`, req.__("Edit")),
529
- },
534
+
530
535
  !tagId
531
536
  ? {
532
537
  label: "",
@@ -600,6 +605,11 @@ const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
600
605
  },
601
606
  '<i class="fas fa-undo-alt"></i>&nbsp;' + req.__("Restore")
602
607
  ),
608
+ post_dropdown_item(
609
+ `/actions/clone/${trigger.id}`,
610
+ '<i class="far fa-copy"></i>&nbsp;' + req.__("Duplicate"),
611
+ req
612
+ ),
603
613
  div({ class: "dropdown-divider" }),
604
614
 
605
615
  post_dropdown_item(
@@ -634,6 +644,14 @@ const getTriggerList = async (
634
644
  return mkTable(
635
645
  [
636
646
  { label: req.__("Name"), key: "name" },
647
+ {
648
+ label: req.__("Test run"),
649
+ key: (r) => link(`/actions/testrun/${r.id}`, req.__("Test run")),
650
+ },
651
+ {
652
+ label: req.__("Configure"),
653
+ key: (r) => link(`/actions/configure/${r.id}`, req.__("Configure")),
654
+ },
637
655
  ...(tagId
638
656
  ? []
639
657
  : [
@@ -667,14 +685,6 @@ const getTriggerList = async (
667
685
  ? a({ href: `/table/${r.table_name}` }, r.table_name)
668
686
  : r.channel,
669
687
  },
670
- {
671
- label: req.__("Test run"),
672
- key: (r) => link(`/actions/testrun/${r.id}`, req.__("Test run")),
673
- },
674
- {
675
- label: req.__("Configure"),
676
- key: (r) => link(`/actions/configure/${r.id}`, req.__("Configure")),
677
- },
678
688
  !tagId
679
689
  ? {
680
690
  label: "",
package/routes/fields.js CHANGED
@@ -84,6 +84,10 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
84
84
  sublabel: req.__("Name of the field"),
85
85
  type: "String",
86
86
  attributes: { autofocus: true },
87
+ help: {
88
+ topic: "Field label",
89
+ context: {},
90
+ },
87
91
  validator(s) {
88
92
  if (!s || s === "") return req.__("Missing label");
89
93
  if (!id && existing_names.includes(Field.labelToName(s)))
@@ -104,6 +108,10 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
104
108
  "The type determines the kind of data that can be stored in the field"
105
109
  ),
106
110
  input_type: "select",
111
+ help: {
112
+ topic: "Field types",
113
+ context: {},
114
+ },
107
115
  options: isPrimary
108
116
  ? primaryTypes
109
117
  : getState().type_names.concat(fkey_opts || []),
@@ -1149,7 +1157,7 @@ router.post(
1149
1157
  const jf = table.getField(ref);
1150
1158
  const jtable = Table.findOne(jf.reftable_name);
1151
1159
  const jrow = await jtable.getRow(
1152
- { [jtable.pk_name]: row[ref] },
1160
+ { [jtable.pk_name]: row[ref]?.[jtable.pk_name] || row[ref] },
1153
1161
  { forUser: req.user, forPublic: !req.user }
1154
1162
  );
1155
1163
  row[ref] = jrow;
@@ -459,8 +459,10 @@ const welcome_page = async (req) => {
459
459
  viewCard(views, req),
460
460
  pageCard(pages, req),
461
461
  ],
462
+ class: "welcome-page-row1",
462
463
  },
463
464
  {
465
+ class: "welcome-page-row2",
464
466
  besides: [
465
467
  {
466
468
  type: "card",
package/routes/menu.js CHANGED
@@ -15,6 +15,7 @@ const { getState } = require("@saltcorn/data/db/state");
15
15
  const User = require("@saltcorn/data/models/user");
16
16
  const View = require("@saltcorn/data/models/view");
17
17
  const Page = require("@saltcorn/data/models/page");
18
+ const PageGroup = require("@saltcorn/data/models/page_group");
18
19
  const { save_menu_items } = require("@saltcorn/data/models/config");
19
20
  const db = require("@saltcorn/data/db");
20
21
 
@@ -43,6 +44,10 @@ module.exports = router;
43
44
  const menuForm = async (req) => {
44
45
  const views = await View.find({}, { orderBy: "name", nocase: true });
45
46
  const pages = await Page.find({}, { orderBy: "name", nocase: true });
47
+ const pageGroups = await PageGroup.find(
48
+ {},
49
+ { orderBy: "name", nocase: true }
50
+ );
46
51
  const roles = await User.get_roles();
47
52
  const tables = await Table.find_with_external({});
48
53
  const dynTableOptions = tables.map((t) => t.name);
@@ -101,6 +106,7 @@ const menuForm = async (req) => {
101
106
  options: [
102
107
  "View",
103
108
  "Page",
109
+ "Page Group",
104
110
  "Link",
105
111
  "Header",
106
112
  "Dynamic",
@@ -141,6 +147,14 @@ const menuForm = async (req) => {
141
147
  attributes: { options: views.map((r) => r.select_option) },
142
148
  showIf: { type: "View" },
143
149
  },
150
+ {
151
+ name: "page_group",
152
+ label: req.__("Page group"),
153
+ input_type: "select",
154
+ class: "item-menu",
155
+ options: pageGroups.map((r) => r.name),
156
+ showIf: { type: "Page Group" },
157
+ },
144
158
  {
145
159
  name: "action_name",
146
160
  label: req.__("Action"),
@@ -194,6 +208,14 @@ const menuForm = async (req) => {
194
208
  required: true,
195
209
  showIf: { type: "Dynamic" },
196
210
  },
211
+ {
212
+ name: "dyn_tooltip_fml",
213
+ label: req.__("Tooltip formula"),
214
+ class: "item-menu",
215
+ type: "String",
216
+ required: false,
217
+ showIf: { type: "Dynamic" },
218
+ },
197
219
  {
198
220
  name: "dyn_url_fml",
199
221
  label: req.__("URL formula"),
@@ -223,6 +245,7 @@ const menuForm = async (req) => {
223
245
  type: [
224
246
  "View",
225
247
  "Page",
248
+ "Page Group",
226
249
  "Link",
227
250
  "Header",
228
251
  "Dynamic",
@@ -238,13 +261,34 @@ const menuForm = async (req) => {
238
261
  attributes: {
239
262
  html: `<button type="button" id="myEditor_icon" class="btn btn-outline-secondary"></button>`,
240
263
  },
241
- showIf: { type: ["View", "Page", "Link", "Header", "Action"] },
264
+ showIf: {
265
+ type: ["View", "Page", "Page Group", "Link", "Header", "Action"],
266
+ },
242
267
  },
243
268
  {
244
269
  name: "icon",
245
270
  class: "item-menu",
246
271
  input_type: "hidden",
247
272
  },
273
+ {
274
+ name: "tooltip",
275
+ label: req.__("Tooltip"),
276
+ class: "item-menu",
277
+ input_type: "text",
278
+ required: false,
279
+ showIf: {
280
+ type: [
281
+ "View",
282
+ "Page",
283
+ "Page Group",
284
+ "Link",
285
+ "Header",
286
+ "Dynamic",
287
+ "Search",
288
+ "Action",
289
+ ],
290
+ },
291
+ },
248
292
  {
249
293
  name: "min_role",
250
294
  label: req.__("Minimum role"),
@@ -258,6 +302,18 @@ const menuForm = async (req) => {
258
302
  type: "Bool",
259
303
  class: "item-menu",
260
304
  required: false,
305
+ default: false,
306
+ },
307
+ {
308
+ name: "mobile_item_html",
309
+ label: req.__("Mobile HTML"),
310
+ sublabel: req.__(
311
+ "HTML for the item in the bottom navigation bar. Currently, only supported by the metronic theme."
312
+ ),
313
+ type: "String",
314
+ class: "item-menu",
315
+ input_type: "textarea",
316
+ showIf: { disable_on_mobile: false, location: "Mobile Bottom" },
261
317
  },
262
318
  {
263
319
  name: "target_blank",
@@ -265,7 +321,7 @@ const menuForm = async (req) => {
265
321
  type: "Bool",
266
322
  required: false,
267
323
  class: "item-menu",
268
- showIf: { type: ["View", "Page", "Link"] },
324
+ showIf: { type: ["View", "Page", "Page Group", "Link"] },
269
325
  },
270
326
  {
271
327
  name: "in_modal",
@@ -273,7 +329,7 @@ const menuForm = async (req) => {
273
329
  type: "Bool",
274
330
  required: false,
275
331
  class: "item-menu",
276
- showIf: { type: ["View", "Page", "Link"] },
332
+ showIf: { type: ["View", "Page", "Page Group", "Link"] },
277
333
  },
278
334
  {
279
335
  name: "style",
@@ -283,7 +339,15 @@ const menuForm = async (req) => {
283
339
  type: "String",
284
340
  required: true,
285
341
  showIf: {
286
- type: ["View", "Page", "Link", "Header", "Dynamic", "Action"],
342
+ type: [
343
+ "View",
344
+ "Page",
345
+ "Page Group",
346
+ "Link",
347
+ "Header",
348
+ "Dynamic",
349
+ "Action",
350
+ ],
287
351
  },
288
352
  attributes: {
289
353
  options: [
@@ -310,6 +374,7 @@ const menuForm = async (req) => {
310
374
  type: [
311
375
  "View",
312
376
  "Page",
377
+ "Page Group",
313
378
  "Link",
314
379
  "Header",
315
380
  "Dynamic",