@saltcorn/server 0.9.4-beta.10 → 0.9.4-beta.12

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/locales/en.json CHANGED
@@ -1360,5 +1360,11 @@
1360
1360
  "Add entries to tag %s": "Add entries to tag %s",
1361
1361
  "Tag not found": "Tag not found",
1362
1362
  "Unable to save: No page or no layout": "Unable to save: No page or no layout",
1363
- "Unable to save: No view": "Unable to save: No view"
1364
- }
1363
+ "Unable to save: No view": "Unable to save: No view",
1364
+ "%s has no eligible page": "%s has no eligible page",
1365
+ "Random allocation": "Random allocation",
1366
+ "Serve a random page, ignoring the eligible formula. Within a session, reloads will always deliver the same page. This is a basic requirement for A/B testing.": "Serve a random page, ignoring the eligible formula. Within a session, reloads will always deliver the same page. This is a basic requirement for A/B testing.",
1367
+ "Pagegroup %s not found": "Pagegroup %s not found",
1368
+ "Create trigger": "Create trigger",
1369
+ "Omit the menu from this view": "Omit the menu from this view"
1370
+ }
package/markup/admin.js CHANGED
@@ -95,23 +95,27 @@ const add_edit_bar = ({
95
95
  const singleton = view?.viewtemplateObj?.singleton;
96
96
 
97
97
  const bar = div(
98
- { class: "alert alert-light d-print-none admin-edit-bar" },
99
- title,
100
- what && span({ class: "ms-1 me-2 badge bg-primary" }, what),
101
- !singleton &&
102
- a(
103
- { class: "ms-2", href: url },
104
- "Edit ",
105
- i({ class: "fas fa-edit" })
106
- ),
107
- cfgUrl && !singleton
108
- ? a(
109
- { class: "ms-1 me-3", href: cfgUrl },
110
- "Configure ",
111
- i({ class: "fas fa-cog" })
112
- )
113
- : "",
114
- !singleton && viewSpec
98
+ { class: "card p-1 mt-1 mb-3 d-print-none admin-edit-bar" },
99
+ div(
100
+ { class: "card-body p-1" },
101
+ i({ class: "fas fa-user-cog me-1" }),
102
+ what && span({ class: "ms-1 me-2 badge bg-secondary" }, what),
103
+ title,
104
+ !singleton &&
105
+ a(
106
+ { class: "ms-2", href: url },
107
+ "Edit ",
108
+ i({ class: "fas fa-edit" })
109
+ ),
110
+ cfgUrl && !singleton
111
+ ? a(
112
+ { class: "ms-1 me-3", href: cfgUrl },
113
+ "Configure ",
114
+ i({ class: "fas fa-cog" })
115
+ )
116
+ : "",
117
+ !singleton && viewSpec
118
+ )
115
119
  );
116
120
 
117
121
  if (contents.above) {
@@ -232,7 +236,7 @@ const send_infoarch_page = (args) => {
232
236
  { text: "Multitenancy", href: "/tenant/settings" },
233
237
  ]
234
238
  : []),
235
- { text: "Pagegroups", href: "/page_group/settings"},
239
+ { text: "Pagegroups", href: "/page_group/settings" },
236
240
  { text: "Tags", href: "/tag" },
237
241
  { text: "Diagram", href: "/diagram" },
238
242
  ],
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.4-beta.10",
3
+ "version": "0.9.4-beta.12",
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.4-beta.10",
11
- "@saltcorn/builder": "0.9.4-beta.10",
12
- "@saltcorn/data": "0.9.4-beta.10",
13
- "@saltcorn/admin-models": "0.9.4-beta.10",
14
- "@saltcorn/filemanager": "0.9.4-beta.10",
15
- "@saltcorn/markup": "0.9.4-beta.10",
16
- "@saltcorn/sbadmin2": "0.9.4-beta.10",
10
+ "@saltcorn/base-plugin": "0.9.4-beta.12",
11
+ "@saltcorn/builder": "0.9.4-beta.12",
12
+ "@saltcorn/data": "0.9.4-beta.12",
13
+ "@saltcorn/admin-models": "0.9.4-beta.12",
14
+ "@saltcorn/filemanager": "0.9.4-beta.12",
15
+ "@saltcorn/markup": "0.9.4-beta.12",
16
+ "@saltcorn/sbadmin2": "0.9.4-beta.12",
17
17
  "@socket.io/cluster-adapter": "^0.2.1",
18
18
  "@socket.io/sticky": "^1.0.1",
19
19
  "adm-zip": "0.5.10",
@@ -55,6 +55,7 @@
55
55
  "pluralize": "^8.0.0",
56
56
  "qrcode": "1.5.1",
57
57
  "resize-with-sharp-or-jimp": "0.1.7",
58
+ "semver": "^7.6.0",
58
59
  "socket.io": "4.6.0",
59
60
  "systeminformation": "^5.21.7",
60
61
  "thirty-two": "1.0.2",
@@ -13,6 +13,10 @@ div.settings-panel {
13
13
  min-height: 150px;
14
14
  }
15
15
 
16
+ div.settings-panel td {
17
+ vertical-align: top;
18
+ }
19
+
16
20
  span.is-builder-link {
17
21
  color: blue;
18
22
  text-decoration: underline;
@@ -190,10 +194,16 @@ div.settings-panel div.rfipbtn {
190
194
  }
191
195
 
192
196
  div.componets-and-library-accordion {
193
- min-height: 45vh;
194
197
  margin-top: -0.25rem;
195
198
  }
196
199
 
200
+ .builder-left-enlarged .componets-and-library-accordion {
201
+ min-height: 20vh;
202
+ }
203
+
204
+ .builder-left-shrunk .componets-and-library-accordion {
205
+ min-height: 35vh;
206
+ }
197
207
  .builder-layers {
198
208
  max-height: calc(45vh - 50px);
199
209
  overflow-y: scroll;
@@ -299,6 +309,13 @@ Copyright (c) 2017 Taha Paksu
299
309
  margin: 0px;
300
310
  padding: 1px;
301
311
  text-align: center;
312
+ cursor: pointer;
313
+ }
314
+ .boxmodel-container .boxmodel-input-container {
315
+ cursor: pointer;
316
+ }
317
+ .boxmodel-container .boxmodel-header {
318
+ cursor: pointer;
302
319
  }
303
320
  .boxmodel-container .dim-display {
304
321
  background: transparent;
@@ -154,7 +154,7 @@ $(function () {
154
154
  });
155
155
  });
156
156
 
157
- function reload_embedded_view(viewname) {
157
+ function reload_embedded_view(viewname, new_query_string) {
158
158
  if (window._sc_loglevel > 4)
159
159
  console.log(
160
160
  "reload_embedded_view",
@@ -164,15 +164,16 @@ function reload_embedded_view(viewname) {
164
164
  );
165
165
  $(`[data-sc-embed-viewname="${viewname}"]`).each(function () {
166
166
  const $e = $(this);
167
- const url =
168
- $e.attr("data-sc-local-state") || $e.attr("data-sc-view-source");
167
+ let url = $e.attr("data-sc-local-state") || $e.attr("data-sc-view-source");
169
168
  if (!url) return;
170
- const headers = {
171
- pjaxpageload: "true",
172
- localizedstate: "true", //no admin bar
173
- };
169
+ if (new_query_string) {
170
+ url = url.split("?")[0] + "?" + new_query_string;
171
+ }
174
172
  $.ajax(url, {
175
- headers,
173
+ headers: {
174
+ pjaxpageload: "true",
175
+ localizedstate: "true", //no admin bar
176
+ },
176
177
  success: function (res, textStatus, request) {
177
178
  $e.html(res);
178
179
  initialize_page();
package/routes/actions.js CHANGED
@@ -105,7 +105,13 @@ router.get(
105
105
  title: req.__("Triggers"),
106
106
  contents: div(
107
107
  await getTriggerList(triggers, req, { filterOnTag }),
108
- link("/actions/new", req.__("Add trigger"))
108
+ a(
109
+ {
110
+ href: "/actions/new",
111
+ class: "btn btn-primary",
112
+ },
113
+ req.__("Create trigger")
114
+ )
109
115
  ),
110
116
  },
111
117
  {
package/routes/admin.js CHANGED
@@ -2427,6 +2427,7 @@ router.post(
2427
2427
  await PageGroup.delete({});
2428
2428
  }
2429
2429
  if (form.values.pages) {
2430
+ await db.deleteWhere("_sc_tag_entries", { not: { page_id: null } });
2430
2431
  await db.deleteWhere("_sc_pages");
2431
2432
  }
2432
2433
  if (form.values.views) {
@@ -505,7 +505,7 @@ const getPageList = async (
505
505
  [
506
506
  {
507
507
  label: req.__("Name"),
508
- key: (r) => link(`/page/${r.name}`, r.name),
508
+ key: (r) => link(`/page/${encodeURIComponent(r.name)}`, r.name),
509
509
  },
510
510
  ...(tagId
511
511
  ? []
@@ -24,6 +24,7 @@ const packagejson = require("../package.json");
24
24
  const Trigger = require("@saltcorn/data/models/trigger");
25
25
  const { fileUploadForm } = require("../markup/forms");
26
26
  const { get_base_url, sendHtmlFile, getEligiblePage } = require("./utils.js");
27
+ const semver = require("semver");
27
28
 
28
29
  /**
29
30
  * Tables List
@@ -519,8 +520,8 @@ const no_views_logged_in = async (req, res) => {
519
520
  const latest =
520
521
  isRoot && (await get_latest_npm_version("@saltcorn/cli", 500));
521
522
  const can_update =
522
- packagejson.version !== latest &&
523
523
  latest &&
524
+ semver.gt(latest, packagejson.version) &&
524
525
  !process.env.SALTCORN_DISABLE_UPGRADE;
525
526
  if (latest && can_update && isRoot)
526
527
  req.flash(
package/routes/page.js CHANGED
@@ -16,6 +16,7 @@ const {
16
16
  isAdmin,
17
17
  sendHtmlFile,
18
18
  getEligiblePage,
19
+ getRandomPage,
19
20
  } = require("../routes/utils.js");
20
21
  const { isTest } = require("@saltcorn/data/utils");
21
22
  const { add_edit_bar } = require("../markup/admin.js");
@@ -88,20 +89,36 @@ const runPage = async (page, req, res, tic) => {
88
89
  const runPageGroup = async (pageGroup, req, res, tic) => {
89
90
  const role = req.user && req.user.id ? req.user.role_id : 100;
90
91
  if (role <= pageGroup.min_role) {
91
- const eligible = await getEligiblePage(pageGroup, req, res);
92
- if (typeof eligible === "string") {
93
- getState().log(2, eligible);
94
- res.status(400).sendWrap(req.__("Internal Error"), eligible);
95
- } else if (eligible) {
96
- if (!eligible.isReload) await runPage(eligible, req, res, tic);
92
+ if (pageGroup.random_allocation) {
93
+ const page = getRandomPage(pageGroup, req);
94
+ if (typeof page === "string") {
95
+ getState().log(2, page);
96
+ res.status(400).sendWrap(req.__("Internal Error"), page);
97
+ } else if (!page) {
98
+ getState().log(2, `Unable to find a random page in ${pageGroup.name}`);
99
+ res
100
+ .status(404)
101
+ .sendWrap(
102
+ req.__("Internal Error"),
103
+ req.__("Unable to find a random page in %s", pageGroup.name)
104
+ );
105
+ } else await runPage(page, req, res, tic);
97
106
  } else {
98
- getState().log(2, `Pagegroup ${pageGroup.name} has no eligible page`);
99
- res
100
- .status(404)
101
- .sendWrap(
102
- req.__("Internal Error"),
103
- req.__("%s has no eligible page", pageGroup.name)
104
- );
107
+ const eligible = await getEligiblePage(pageGroup, req, res);
108
+ if (typeof eligible === "string") {
109
+ getState().log(2, eligible);
110
+ res.status(400).sendWrap(req.__("Internal Error"), eligible);
111
+ } else if (eligible) {
112
+ if (!eligible.isReload) await runPage(eligible, req, res, tic);
113
+ } else {
114
+ getState().log(2, `Pagegroup ${pageGroup.name} has no eligible page`);
115
+ res
116
+ .status(404)
117
+ .sendWrap(
118
+ req.__("Internal Error"),
119
+ req.__("%s has no eligible page", pageGroup.name)
120
+ );
121
+ }
105
122
  }
106
123
  } else {
107
124
  getState().log(2, `Pagegroup ${pageGroup.name} not authorized`);
@@ -29,9 +29,17 @@ const groupPropsForm = async (req, isNew) => {
29
29
  ...(isNew
30
30
  ? {}
31
31
  : {
32
- onChange: `saveAndContinue(this, (res) => {
33
- history.replaceState(null, '', res.responseJSON.row.name);
34
- });`,
32
+ onChange: `
33
+ saveAndContinue(this, (res) => {
34
+ history.replaceState(null, '', res.responseJSON.row.name);
35
+ const arrowsVisible = $('#upDownArrowsId').length > 0;
36
+ if (
37
+ arrowsVisible && res.responseJSON.row.random_allocation ||
38
+ !arrowsVisible && !res.responseJSON.row.random_allocation
39
+ ) {
40
+ window.location.reload();
41
+ }
42
+ });`,
35
43
  }),
36
44
  noSubmitButton: !isNew,
37
45
  fields: [
@@ -68,65 +76,78 @@ const groupPropsForm = async (req, isNew) => {
68
76
  input_type: "select",
69
77
  options: roles.map((r) => ({ value: r.id, label: r.role })),
70
78
  },
79
+ {
80
+ name: "random_allocation",
81
+ label: req.__("Random allocation"),
82
+ type: "Bool",
83
+ sublabel: req.__(
84
+ "Serve a random page, ignoring the eligible formula. " +
85
+ "Within a session, reloads will always deliver the same page. " +
86
+ "This is a basic requirement for A/B testing."
87
+ ),
88
+ },
71
89
  ],
72
90
  });
73
91
  };
74
92
 
75
- const memberForm = async (action, req, groupName, pageValidator) => {
93
+ const memberForm = async (action, req, group, pageValidator) => {
76
94
  const pageOptions = (await Page.find()).map((p) => p.name);
77
- return new Form({
78
- action,
79
- fields: [
80
- {
81
- name: "page_name",
82
- label: req.__("Page"),
83
- sublabel: req.__("Page to be served"),
84
- type: "String",
85
- required: true,
86
- validator: pageValidator,
87
- attributes: {
88
- options: pageOptions,
89
- },
90
- },
91
- {
92
- name: "description",
93
- label: req.__("Description"),
94
- type: "String",
95
- sublabel: req.__("A description of the group member"),
95
+ const fields = [
96
+ {
97
+ name: "page_name",
98
+ label: req.__("Page"),
99
+ sublabel: req.__("Page to be served"),
100
+ type: "String",
101
+ required: true,
102
+ validator: pageValidator,
103
+ attributes: {
104
+ options: pageOptions,
96
105
  },
97
- {
98
- name: "eligible_formula",
99
- label: req.__("Eligible Formula"),
100
- sublabel:
101
- req.__("Formula to determine if this page should be served.") +
102
- br() +
103
- span(
104
- "Variables in scope: ",
105
- [
106
- "width",
107
- "height",
108
- "innerWidth",
109
- "innerHeight",
110
- "user",
111
- "locale",
112
- "device",
113
- ]
114
- .map((f) => code(f))
115
- .join(", ")
116
- ),
117
- help: {
118
- topic: "Eligible Formula",
119
- },
120
- type: "String",
121
- required: true,
122
- class: "validate-expression",
106
+ },
107
+ {
108
+ name: "description",
109
+ label: req.__("Description"),
110
+ type: "String",
111
+ sublabel: req.__("A description of the group member"),
112
+ },
113
+ ];
114
+ if (!group.random_allocation) {
115
+ fields.push({
116
+ name: "eligible_formula",
117
+ label: req.__("Eligible Formula"),
118
+ sublabel:
119
+ req.__("Formula to determine if this page should be served.") +
120
+ br() +
121
+ span(
122
+ "Variables in scope: ",
123
+ [
124
+ "width",
125
+ "height",
126
+ "innerWidth",
127
+ "innerHeight",
128
+ "user",
129
+ "locale",
130
+ "device",
131
+ ]
132
+ .map((f) => code(f))
133
+ .join(", ")
134
+ ),
135
+ help: {
136
+ topic: "Eligible Formula",
123
137
  },
124
- ],
138
+ type: "String",
139
+ required: true,
140
+ class: "validate-expression",
141
+ });
142
+ }
143
+ return new Form({
144
+ action,
145
+ fields,
125
146
  additionalButtons: [
126
147
  {
127
148
  label: req.__("Cancel"),
128
149
  class: "btn btn-primary",
129
- onclick: `cancelMemberEdit('${groupName}');`,
150
+ onclick: `cancelMemberEdit('${group.name}');`,
130
151
  },
131
152
  ],
132
153
  });
@@ -142,7 +163,7 @@ const editMemberForm = async (member, req) => {
142
163
  return await memberForm(
143
164
  `/page_groupedit/edit-member/${member.id}`,
144
165
  req,
145
- group.name,
166
+ group,
146
167
  validator
147
168
  );
148
169
  };
@@ -156,7 +177,7 @@ const addMemberForm = async (group, req) => {
156
177
  return await memberForm(
157
178
  `/page_groupedit/add-member/${group.name}`,
158
179
  req,
159
- group.name,
180
+ group,
160
181
  validator
161
182
  );
162
183
  };
@@ -224,7 +245,7 @@ const pageGroupMembers = async (pageGroup, req) => {
224
245
  if (members.length <= 1) return "";
225
246
  else
226
247
  return div(
227
- { class: "container" },
248
+ { class: "container", id: "upDownArrowsId" },
228
249
  div(
229
250
  { class: "row" },
230
251
  div(
@@ -266,38 +287,38 @@ const pageGroupMembers = async (pageGroup, req) => {
266
287
  )
267
288
  );
268
289
  };
269
-
270
- return mkTable(
271
- [
272
- {
273
- label: req.__("Page"),
274
- key: (r) =>
275
- link(`/page/${pageIdToName[r.page_id]}`, pageIdToName[r.page_id]),
276
- },
277
- {
278
- label: "",
279
- key: (r) => upDownBtns(r, req),
280
- },
281
- {
282
- label: req.__("Edit"),
283
- key: (member) =>
284
- link(`/page_groupedit/edit-member/${member.id}`, req.__("Edit")),
285
- },
286
- {
287
- label: req.__("Delete"),
288
- key: (member) =>
289
- post_delete_btn(
290
- `/page_groupedit/remove-member/${member.id}`,
291
- req,
292
- req.__("Member %s", member.sequence)
293
- ),
294
- },
295
- ],
296
- members,
290
+ const tblArr = [
291
+ {
292
+ label: req.__("Page"),
293
+ key: (r) =>
294
+ link(`/page/${pageIdToName[r.page_id]}`, pageIdToName[r.page_id]),
295
+ },
296
+ ];
297
+ if (!pageGroup.random_allocation) {
298
+ tblArr.push({
299
+ label: "",
300
+ key: (r) => upDownBtns(r, req),
301
+ });
302
+ }
303
+ tblArr.push(
297
304
  {
298
- hover: true,
305
+ label: req.__("Edit"),
306
+ key: (member) =>
307
+ link(`/page_groupedit/edit-member/${member.id}`, req.__("Edit")),
308
+ },
309
+ {
310
+ label: req.__("Delete"),
311
+ key: (member) =>
312
+ post_delete_btn(
313
+ `/page_groupedit/remove-member/${member.id}`,
314
+ req,
315
+ req.__("Member %s", member.sequence)
316
+ ),
299
317
  }
300
318
  );
319
+ return mkTable(tblArr, members, {
320
+ hover: true,
321
+ });
301
322
  };
302
323
 
303
324
  /**
@@ -383,7 +383,7 @@ const wrap = (contents, noCard, req, page) => ({
383
383
  crumbs: [
384
384
  { text: req.__("Pages"), href: "/pageedit" },
385
385
  page
386
- ? { href: `/page/${page.name}`, text: page.name }
386
+ ? { href: `/page/${encodeURIComponent(page.name)}`, text: page.name }
387
387
  : { text: req.__("New") },
388
388
  ],
389
389
  },
@@ -30,7 +30,12 @@ const buildFields = (entryType, formOptions, req) => {
30
30
  div(
31
31
  { class: "col-sm-10" },
32
32
  select(
33
- { name: "ids", class: "form-control form-select", multiple: true },
33
+ {
34
+ name: "ids",
35
+ class: "form-control form-select",
36
+ multiple: true,
37
+ size: 20,
38
+ },
34
39
  list.map((entry) => {
35
40
  return option({ value: entry.id, label: entry.name });
36
41
  })
package/routes/utils.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  features,
14
14
  } = require("@saltcorn/data/db/state");
15
15
  const { get_base_url } = require("@saltcorn/data/models/config");
16
+ const { hash } = require("@saltcorn/data/utils");
16
17
  const { input, script, domReady } = require("@saltcorn/markup/tags");
17
18
  const session = require("express-session");
18
19
  const cookieSession = require("cookie-session");
@@ -21,6 +22,7 @@ const { validateHeaderName, validateHeaderValue } = require("http");
21
22
  const Crash = require("@saltcorn/data/models/crash");
22
23
  const File = require("@saltcorn/data/models/file");
23
24
  const User = require("@saltcorn/data/models/user");
25
+ const Page = require("@saltcorn/data/models/page");
24
26
  const si = require("systeminformation");
25
27
  const {
26
28
  config_fields_form,
@@ -30,6 +32,7 @@ const {
30
32
  } = require("../markup/admin.js");
31
33
  const path = require("path");
32
34
  const { UAParser } = require("ua-parser-js");
35
+ const crypto = require("crypto");
33
36
 
34
37
  const get_sys_info = async () => {
35
38
  const disks = await si.fsSize();
@@ -505,6 +508,21 @@ const getEligiblePage = async (pageGroup, req, res) => {
505
508
  }
506
509
  };
507
510
 
511
+ /**
512
+ * @param {PageGroup} pageGroup
513
+ * @param {any} req
514
+ * @returns the page, null or an error msg
515
+ */
516
+ const getRandomPage = (pageGroup, req) => {
517
+ if (pageGroup.members.length === 0)
518
+ return req.__("Pagegroup %s has no members", pageGroup.name);
519
+ const hash = crypto.createHash("sha1").update(req.sessionID).digest("hex");
520
+ const idx =
521
+ parseInt(hash.substring(hash.length - 4), 16) % pageGroup.members.length;
522
+ const sessionMember = pageGroup.members[idx];
523
+ return Page.findOne({ id: sessionMember.page_id });
524
+ };
525
+
508
526
  module.exports = {
509
527
  sqlsanitize,
510
528
  csrfField,
@@ -524,4 +542,5 @@ module.exports = {
524
542
  sendHtmlFile,
525
543
  setRole,
526
544
  getEligiblePage,
545
+ getRandomPage,
527
546
  };
package/routes/view.js CHANGED
@@ -79,6 +79,7 @@ router.get(
79
79
  if ((title || "").includes("{{")) {
80
80
  title = await view.interpolate_title_string(title, query);
81
81
  }
82
+ title = { title };
82
83
  if (isModal && view.attributes?.popup_width)
83
84
  res.set(
84
85
  "SaltcornModalWidth",
@@ -102,7 +103,10 @@ router.get(
102
103
  if ((description || "").includes("{{")) {
103
104
  description = await view.interpolate_title_string(description, query);
104
105
  }
105
- title = { title, description };
106
+ title.description = description;
107
+ }
108
+ if (view.attributes?.no_menu) {
109
+ title.no_menu = true;
106
110
  }
107
111
  const tock = new Date();
108
112
  const ms = tock.getTime() - tic.getTime();
@@ -217,6 +217,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
217
217
  ),
218
218
  }),
219
219
  new Field({
220
+ // legacy
220
221
  name: "default_render_page",
221
222
  label: req.__("Show on page"),
222
223
  sublabel: req.__(
@@ -243,6 +244,14 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
243
244
  },
244
245
  showIf: { viewtemplate: hasTable },
245
246
  }),
247
+ new Field({
248
+ name: "no_menu",
249
+ label: req.__("No menu"),
250
+ sublabel: req.__("Omit the menu from this view"),
251
+ tab: "View settings",
252
+ parent_field: "attributes",
253
+ type: "Bool",
254
+ }),
246
255
  new Field({
247
256
  name: "popup_title",
248
257
  label: req.__("Title"),
@@ -73,6 +73,7 @@ describe("edit Page groups", () => {
73
73
  name: nameAfterUpdate,
74
74
  description: null,
75
75
  min_role: 100,
76
+ random_allocation: false,
76
77
  });
77
78
  });
78
79