@saltcorn/server 0.9.4-beta.9 → 0.9.4

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 (183) hide show
  1. package/app.js +16 -1
  2. package/auth/admin.js +19 -3
  3. package/auth/routes.js +16 -4
  4. package/auth/testhelp.js +17 -1
  5. package/load_plugins.js +8 -2
  6. package/locales/en.json +24 -2
  7. package/markup/admin.js +22 -18
  8. package/package.json +10 -9
  9. package/public/dayjslocales/af.js +1 -0
  10. package/public/dayjslocales/am.js +1 -0
  11. package/public/dayjslocales/ar-dz.js +1 -0
  12. package/public/dayjslocales/ar-iq.js +1 -0
  13. package/public/dayjslocales/ar-kw.js +1 -0
  14. package/public/dayjslocales/ar-ly.js +1 -0
  15. package/public/dayjslocales/ar-ma.js +1 -0
  16. package/public/dayjslocales/ar-sa.js +1 -0
  17. package/public/dayjslocales/ar-tn.js +1 -0
  18. package/public/dayjslocales/ar.js +1 -0
  19. package/public/dayjslocales/az.js +1 -0
  20. package/public/dayjslocales/be.js +1 -0
  21. package/public/dayjslocales/bg.js +1 -0
  22. package/public/dayjslocales/bi.js +1 -0
  23. package/public/dayjslocales/bm.js +1 -0
  24. package/public/dayjslocales/bn-bd.js +1 -0
  25. package/public/dayjslocales/bn.js +1 -0
  26. package/public/dayjslocales/bo.js +1 -0
  27. package/public/dayjslocales/br.js +1 -0
  28. package/public/dayjslocales/bs.js +1 -0
  29. package/public/dayjslocales/ca.js +1 -0
  30. package/public/dayjslocales/cs.js +1 -0
  31. package/public/dayjslocales/cv.js +1 -0
  32. package/public/dayjslocales/cy.js +1 -0
  33. package/public/dayjslocales/da.js +1 -0
  34. package/public/dayjslocales/de-at.js +1 -0
  35. package/public/dayjslocales/de-ch.js +1 -0
  36. package/public/dayjslocales/de.js +1 -0
  37. package/public/dayjslocales/dv.js +1 -0
  38. package/public/dayjslocales/el.js +1 -0
  39. package/public/dayjslocales/en-au.js +1 -0
  40. package/public/dayjslocales/en-ca.js +1 -0
  41. package/public/dayjslocales/en-gb.js +1 -0
  42. package/public/dayjslocales/en-ie.js +1 -0
  43. package/public/dayjslocales/en-il.js +1 -0
  44. package/public/dayjslocales/en-in.js +1 -0
  45. package/public/dayjslocales/en-nz.js +1 -0
  46. package/public/dayjslocales/en-sg.js +1 -0
  47. package/public/dayjslocales/en-tt.js +1 -0
  48. package/public/dayjslocales/en.js +1 -0
  49. package/public/dayjslocales/eo.js +1 -0
  50. package/public/dayjslocales/es-do.js +1 -0
  51. package/public/dayjslocales/es-mx.js +1 -0
  52. package/public/dayjslocales/es-pr.js +1 -0
  53. package/public/dayjslocales/es-us.js +1 -0
  54. package/public/dayjslocales/es.js +1 -0
  55. package/public/dayjslocales/et.js +1 -0
  56. package/public/dayjslocales/eu.js +1 -0
  57. package/public/dayjslocales/fa.js +1 -0
  58. package/public/dayjslocales/fi.js +1 -0
  59. package/public/dayjslocales/fo.js +1 -0
  60. package/public/dayjslocales/fr-ca.js +1 -0
  61. package/public/dayjslocales/fr-ch.js +1 -0
  62. package/public/dayjslocales/fr.js +1 -0
  63. package/public/dayjslocales/fy.js +1 -0
  64. package/public/dayjslocales/ga.js +1 -0
  65. package/public/dayjslocales/gd.js +1 -0
  66. package/public/dayjslocales/gl.js +1 -0
  67. package/public/dayjslocales/gom-latn.js +1 -0
  68. package/public/dayjslocales/gu.js +1 -0
  69. package/public/dayjslocales/he.js +1 -0
  70. package/public/dayjslocales/hi.js +1 -0
  71. package/public/dayjslocales/hr.js +1 -0
  72. package/public/dayjslocales/ht.js +1 -0
  73. package/public/dayjslocales/hu.js +1 -0
  74. package/public/dayjslocales/hy-am.js +1 -0
  75. package/public/dayjslocales/id.js +1 -0
  76. package/public/dayjslocales/is.js +1 -0
  77. package/public/dayjslocales/it-ch.js +1 -0
  78. package/public/dayjslocales/it.js +1 -0
  79. package/public/dayjslocales/ja.js +1 -0
  80. package/public/dayjslocales/jv.js +1 -0
  81. package/public/dayjslocales/ka.js +1 -0
  82. package/public/dayjslocales/kk.js +1 -0
  83. package/public/dayjslocales/km.js +1 -0
  84. package/public/dayjslocales/kn.js +1 -0
  85. package/public/dayjslocales/ko.js +1 -0
  86. package/public/dayjslocales/ku.js +1 -0
  87. package/public/dayjslocales/ky.js +1 -0
  88. package/public/dayjslocales/lb.js +1 -0
  89. package/public/dayjslocales/lo.js +1 -0
  90. package/public/dayjslocales/lt.js +1 -0
  91. package/public/dayjslocales/lv.js +1 -0
  92. package/public/dayjslocales/me.js +1 -0
  93. package/public/dayjslocales/mi.js +1 -0
  94. package/public/dayjslocales/mk.js +1 -0
  95. package/public/dayjslocales/ml.js +1 -0
  96. package/public/dayjslocales/mn.js +1 -0
  97. package/public/dayjslocales/mr.js +1 -0
  98. package/public/dayjslocales/ms-my.js +1 -0
  99. package/public/dayjslocales/ms.js +1 -0
  100. package/public/dayjslocales/mt.js +1 -0
  101. package/public/dayjslocales/my.js +1 -0
  102. package/public/dayjslocales/nb.js +1 -0
  103. package/public/dayjslocales/ne.js +1 -0
  104. package/public/dayjslocales/nl-be.js +1 -0
  105. package/public/dayjslocales/nl.js +1 -0
  106. package/public/dayjslocales/nn.js +1 -0
  107. package/public/dayjslocales/oc-lnc.js +1 -0
  108. package/public/dayjslocales/pa-in.js +1 -0
  109. package/public/dayjslocales/pl.js +1 -0
  110. package/public/dayjslocales/pt-br.js +1 -0
  111. package/public/dayjslocales/pt.js +1 -0
  112. package/public/dayjslocales/rn.js +1 -0
  113. package/public/dayjslocales/ro.js +1 -0
  114. package/public/dayjslocales/ru.js +1 -0
  115. package/public/dayjslocales/rw.js +1 -0
  116. package/public/dayjslocales/sd.js +1 -0
  117. package/public/dayjslocales/se.js +1 -0
  118. package/public/dayjslocales/si.js +1 -0
  119. package/public/dayjslocales/sk.js +1 -0
  120. package/public/dayjslocales/sl.js +1 -0
  121. package/public/dayjslocales/sq.js +1 -0
  122. package/public/dayjslocales/sr-cyrl.js +1 -0
  123. package/public/dayjslocales/sr.js +1 -0
  124. package/public/dayjslocales/ss.js +1 -0
  125. package/public/dayjslocales/sv-fi.js +1 -0
  126. package/public/dayjslocales/sv.js +1 -0
  127. package/public/dayjslocales/sw.js +1 -0
  128. package/public/dayjslocales/ta.js +1 -0
  129. package/public/dayjslocales/te.js +1 -0
  130. package/public/dayjslocales/tet.js +1 -0
  131. package/public/dayjslocales/tg.js +1 -0
  132. package/public/dayjslocales/th.js +1 -0
  133. package/public/dayjslocales/tk.js +1 -0
  134. package/public/dayjslocales/tl-ph.js +1 -0
  135. package/public/dayjslocales/tlh.js +1 -0
  136. package/public/dayjslocales/tr.js +1 -0
  137. package/public/dayjslocales/tzl.js +1 -0
  138. package/public/dayjslocales/tzm-latn.js +1 -0
  139. package/public/dayjslocales/tzm.js +1 -0
  140. package/public/dayjslocales/ug-cn.js +1 -0
  141. package/public/dayjslocales/uk.js +1 -0
  142. package/public/dayjslocales/ur.js +1 -0
  143. package/public/dayjslocales/uz-latn.js +1 -0
  144. package/public/dayjslocales/uz.js +1 -0
  145. package/public/dayjslocales/vi.js +1 -0
  146. package/public/dayjslocales/x-pseudo.js +1 -0
  147. package/public/dayjslocales/yo.js +1 -0
  148. package/public/dayjslocales/zh-cn.js +1 -0
  149. package/public/dayjslocales/zh-hk.js +1 -0
  150. package/public/dayjslocales/zh-tw.js +1 -0
  151. package/public/dayjslocales/zh.js +1 -0
  152. package/public/gridedit.js +2 -2
  153. package/public/log_viewer_utils.js +156 -0
  154. package/public/saltcorn-builder.css +43 -2
  155. package/public/saltcorn-common.js +39 -29
  156. package/public/saltcorn.js +26 -8
  157. package/public/tabulator_bootstrap5.min.css +1 -0
  158. package/restart_watcher.js +1 -0
  159. package/routes/actions.js +158 -16
  160. package/routes/admin.js +83 -9
  161. package/routes/common_lists.js +40 -17
  162. package/routes/fields.js +18 -3
  163. package/routes/homepage.js +2 -1
  164. package/routes/page.js +30 -13
  165. package/routes/page_groupedit.js +104 -83
  166. package/routes/pageedit.js +4 -3
  167. package/routes/tables.js +34 -0
  168. package/routes/tag_entries.js +6 -1
  169. package/routes/tags.js +4 -0
  170. package/routes/utils.js +23 -2
  171. package/routes/view.js +5 -1
  172. package/routes/viewedit.js +9 -0
  173. package/serve.js +177 -10
  174. package/tests/admin.test.js +17 -11
  175. package/tests/api.test.js +27 -0
  176. package/tests/fields.test.js +132 -5
  177. package/tests/help.test.js +37 -0
  178. package/tests/page_group.test.js +1 -0
  179. package/tests/plugins.test.js +0 -12
  180. package/tests/table.test.js +1 -5
  181. package/tests/view.test.js +12 -0
  182. package/tests/viewedit.test.js +52 -8
  183. package/wrapper.js +9 -1
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
  /**
@@ -186,7 +186,7 @@ const pageBuilderData = async (req, context) => {
186
186
  for (const view of views) {
187
187
  fixed_state_fields[view.name] = [];
188
188
  const table = Table.findOne(view.table_id || view.exttable_name);
189
-
189
+ if (table) view.table_name = table.name;
190
190
  const fs = await view.get_state_fields();
191
191
  for (const frec of fs) {
192
192
  const f = new Field(frec);
@@ -219,9 +219,10 @@ const pageBuilderData = async (req, context) => {
219
219
  }
220
220
  }
221
221
  }
222
+
222
223
  //console.log(fixed_state_fields.ListTasks);
223
224
  return {
224
- views,
225
+ views: views.map((v) => v.select_option),
225
226
  images,
226
227
  pages,
227
228
  page_groups,
@@ -383,7 +384,7 @@ const wrap = (contents, noCard, req, page) => ({
383
384
  crumbs: [
384
385
  { text: req.__("Pages"), href: "/pageedit" },
385
386
  page
386
- ? { href: `/page/${page.name}`, text: page.name }
387
+ ? { href: `/page/${encodeURIComponent(page.name)}`, text: page.name }
387
388
  : { text: req.__("New") },
388
389
  ],
389
390
  },
package/routes/tables.js CHANGED
@@ -832,6 +832,7 @@ router.get(
832
832
  }
833
833
  viewCard = {
834
834
  type: "card",
835
+ id: "table-views",
835
836
  title: req.__("Views of this table"),
836
837
  contents:
837
838
  viewCardContents +
@@ -994,6 +995,13 @@ router.get(
994
995
  req,
995
996
  true
996
997
  ),
998
+ table.name !== "users" &&
999
+ post_dropdown_item(
1000
+ `/table/delete/${table.id}`,
1001
+ '<i class="fas fa-trash"></i>&nbsp;' + req.__("Delete table"),
1002
+ req,
1003
+ true
1004
+ ),
997
1005
  ])
998
1006
  )
999
1007
  );
@@ -1173,6 +1181,32 @@ router.post(
1173
1181
  res.redirect(`/table`);
1174
1182
  return;
1175
1183
  }
1184
+ const views = await View.find(
1185
+ t.id ? { table_id: t.id } : { exttable_name: t.name }
1186
+ );
1187
+ if (views.length) {
1188
+ req.flash(
1189
+ "error",
1190
+ `${text(t.name)} has views. Delete these first: <a href="/table/${
1191
+ t.name
1192
+ }#table-views">Views for ${text(t.name)}</a>`
1193
+ );
1194
+ res.redirect(`/table`);
1195
+ return;
1196
+ }
1197
+ if (t.id) {
1198
+ const triggers = await Trigger.find({ table_id: t.id });
1199
+ if (triggers.length) {
1200
+ req.flash(
1201
+ "error",
1202
+ `${text(
1203
+ t.name
1204
+ )} has triggers. Delete these first: <a href="/actions">Trigger list</a>`
1205
+ );
1206
+ res.redirect(`/table`);
1207
+ return;
1208
+ }
1209
+ }
1176
1210
  try {
1177
1211
  await t.delete();
1178
1212
  req.flash("success", req.__(`Table %s deleted`, t.name));
@@ -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/tags.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  const db = require("@saltcorn/data/db");
28
28
  const { getState } = require("@saltcorn/data/db/state");
29
29
  const { create_pack_from_tag } = require("@saltcorn/admin-models/models/pack");
30
+ const Table = require("@saltcorn/data/models/table");
30
31
 
31
32
  const router = new Router();
32
33
  module.exports = router;
@@ -161,6 +162,9 @@ router.get(
161
162
  await setTableRefs(views);
162
163
  const pages = await tag.getPages();
163
164
  const triggers = await tag.getTriggers();
165
+ triggers.forEach((tr) => {
166
+ if (tr.table_id) tr.table_name = Table.findOne(tr.table_id)?.name;
167
+ });
164
168
  const roles = await User.get_roles();
165
169
 
166
170
  const tablesDomId = "tablesListId";
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();
@@ -144,9 +147,10 @@ const set_custom_http_headers = (res, req, state) => {
144
147
  /**
145
148
  * Tries to recognize tenant from HTTP Request
146
149
  * @param {object} req
150
+ * @param {number|undefined} hostPartsOffset (optional) for socketIO, to get the tenant with localhost
147
151
  * @returns {string}
148
152
  */
149
- const get_tenant_from_req = (req) => {
153
+ const get_tenant_from_req = (req, hostPartsOffset) => {
150
154
  if (req.subdomains && req.subdomains.length > 0)
151
155
  return req.subdomains[req.subdomains.length - 1];
152
156
 
@@ -154,7 +158,8 @@ const get_tenant_from_req = (req) => {
154
158
  return db.connectObj.default_schema;
155
159
  if (!req.subdomains && req.headers.host) {
156
160
  const parts = req.headers.host.split(".");
157
- if (parts.length < 3) return db.connectObj.default_schema;
161
+ if (parts.length < (!hostPartsOffset ? 3 : 3 - hostPartsOffset))
162
+ return db.connectObj.default_schema;
158
163
  else return parts[0];
159
164
  }
160
165
  };
@@ -505,6 +510,21 @@ const getEligiblePage = async (pageGroup, req, res) => {
505
510
  }
506
511
  };
507
512
 
513
+ /**
514
+ * @param {PageGroup} pageGroup
515
+ * @param {any} req
516
+ * @returns the page, null or an error msg
517
+ */
518
+ const getRandomPage = (pageGroup, req) => {
519
+ if (pageGroup.members.length === 0)
520
+ return req.__("Pagegroup %s has no members", pageGroup.name);
521
+ const hash = crypto.createHash("sha1").update(req.sessionID).digest("hex");
522
+ const idx =
523
+ parseInt(hash.substring(hash.length - 4), 16) % pageGroup.members.length;
524
+ const sessionMember = pageGroup.members[idx];
525
+ return Page.findOne({ id: sessionMember.page_id });
526
+ };
527
+
508
528
  module.exports = {
509
529
  sqlsanitize,
510
530
  csrfField,
@@ -524,4 +544,5 @@ module.exports = {
524
544
  sendHtmlFile,
525
545
  setRole,
526
546
  getEligiblePage,
547
+ getRandomPage,
527
548
  };
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"),