@saltcorn/server 0.9.4-beta.2 → 0.9.4-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.
Files changed (182) hide show
  1. package/app.js +16 -1
  2. package/auth/admin.js +19 -3
  3. package/auth/routes.js +8 -2
  4. package/help/JavaScript action code.tmd +1 -0
  5. package/load_plugins.js +8 -2
  6. package/locales/en.json +34 -1
  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 +62 -3
  155. package/public/saltcorn-common.js +8 -0
  156. package/public/saltcorn.js +30 -9
  157. package/public/tabulator_bootstrap5.min.css +1 -0
  158. package/restart_watcher.js +1 -0
  159. package/routes/actions.js +175 -18
  160. package/routes/admin.js +77 -5
  161. package/routes/common_lists.js +344 -152
  162. package/routes/fields.js +29 -5
  163. package/routes/files.js +3 -1
  164. package/routes/homepage.js +2 -1
  165. package/routes/list.js +5 -0
  166. package/routes/page.js +30 -13
  167. package/routes/page_groupedit.js +104 -83
  168. package/routes/pageedit.js +23 -7
  169. package/routes/tables.js +56 -6
  170. package/routes/tag_entries.js +18 -5
  171. package/routes/tags.js +65 -12
  172. package/routes/utils.js +23 -2
  173. package/routes/view.js +21 -2
  174. package/routes/viewedit.js +70 -4
  175. package/serve.js +177 -10
  176. package/tests/admin.test.js +17 -11
  177. package/tests/page_group.test.js +1 -0
  178. package/tests/table.test.js +1 -5
  179. package/tests/view.test.js +115 -15
  180. package/tests/viewedit.test.js +52 -29
  181. package/wrapper.js +11 -3
  182. package/public/relation_helpers.js +0 -351
@@ -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/list.js CHANGED
@@ -161,6 +161,11 @@ const typeToGridType = (t, field) => {
161
161
  jsgField.formatterParams = {
162
162
  inputFormat: "iso",
163
163
  };
164
+
165
+ if (field.attributes?.day_only) {
166
+ jsgField.editorParams = { dayOnly: true };
167
+ jsgField.formatter = "__isoDateFormatter";
168
+ }
164
169
  } else if (t.name === "Color") {
165
170
  jsgField.editor = "__colorEditor";
166
171
  jsgField.formatter = "__colorFormatter";
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
  /**
@@ -21,6 +21,8 @@ const { getViews, traverseSync } = require("@saltcorn/data/models/layout");
21
21
  const { add_to_menu } = require("@saltcorn/admin-models/models/pack");
22
22
  const db = require("@saltcorn/data/db");
23
23
  const { getPageList, getPageGroupList } = require("./common_lists");
24
+ const TagEntry = require("@saltcorn/data/models/tag_entry");
25
+ const Tag = require("@saltcorn/data/models/tag");
24
26
 
25
27
  const {
26
28
  isAdmin,
@@ -184,7 +186,7 @@ const pageBuilderData = async (req, context) => {
184
186
  for (const view of views) {
185
187
  fixed_state_fields[view.name] = [];
186
188
  const table = Table.findOne(view.table_id || view.exttable_name);
187
-
189
+ if (table) view.table_name = table.name;
188
190
  const fs = await view.get_state_fields();
189
191
  for (const frec of fs) {
190
192
  const f = new Field(frec);
@@ -217,9 +219,10 @@ const pageBuilderData = async (req, context) => {
217
219
  }
218
220
  }
219
221
  }
222
+
220
223
  //console.log(fixed_state_fields.ListTasks);
221
224
  return {
222
- views,
225
+ views: views.map((v) => v.select_option),
223
226
  images,
224
227
  pages,
225
228
  page_groups,
@@ -293,7 +296,18 @@ router.get(
293
296
  "/",
294
297
  isAdmin,
295
298
  error_catcher(async (req, res) => {
296
- const pages = await Page.find({}, { orderBy: "name", nocase: true });
299
+ const pageq = {};
300
+ let filterOnTag;
301
+
302
+ if (req.query._tag) {
303
+ const tagEntries = await TagEntry.find({
304
+ tag_id: +req.query._tag,
305
+ not: { page_id: null },
306
+ });
307
+ pageq.id = { in: tagEntries.map((te) => te.page_id).filter(Boolean) };
308
+ filterOnTag = await Tag.findOne({ id: +req.query._tag });
309
+ }
310
+ const pages = await Page.find(pageq, { orderBy: "name", nocase: true });
297
311
  const pageGroups = await PageGroup.find(
298
312
  {},
299
313
  { orderBy: "name", nocase: true }
@@ -311,7 +325,7 @@ router.get(
311
325
  title: req.__("Your pages"),
312
326
  class: "mt-0",
313
327
  contents: div(
314
- getPageList(pages, roles, req),
328
+ await getPageList(pages, roles, req, { filterOnTag }),
315
329
  a(
316
330
  {
317
331
  href: `/pageedit/new`,
@@ -370,7 +384,7 @@ const wrap = (contents, noCard, req, page) => ({
370
384
  crumbs: [
371
385
  { text: req.__("Pages"), href: "/pageedit" },
372
386
  page
373
- ? { href: `/page/${page.name}`, text: page.name }
387
+ ? { href: `/page/${encodeURIComponent(page.name)}`, text: page.name }
374
388
  : { text: req.__("New") },
375
389
  ],
376
390
  },
@@ -677,9 +691,11 @@ router.post(
677
691
 
678
692
  if (id && req.body.layout) {
679
693
  await Page.update(+id, { layout: req.body.layout });
680
- res.json({ success: "ok" });
694
+ res.json({
695
+ success: "ok",
696
+ });
681
697
  } else {
682
- res.json({ error: "no page or no layout." });
698
+ res.json({ error: req.__("Unable to save: No page or no layout") });
683
699
  }
684
700
  })
685
701
  );
package/routes/tables.js CHANGED
@@ -13,6 +13,7 @@ const View = require("@saltcorn/data/models/view");
13
13
  const User = require("@saltcorn/data/models/user");
14
14
  const Model = require("@saltcorn/data/models/model");
15
15
  const Trigger = require("@saltcorn/data/models/trigger");
16
+ const TagEntry = require("@saltcorn/data/models/tag_entry");
16
17
  const {
17
18
  mkTable,
18
19
  renderForm,
@@ -59,10 +60,12 @@ const { tablesList, viewsList } = require("./common_lists");
59
60
  const {
60
61
  InvalidConfiguration,
61
62
  removeAllWhiteSpace,
63
+ comparingCaseInsensitive,
62
64
  } = require("@saltcorn/data/utils");
63
65
  const { EOL } = require("os");
64
66
 
65
67
  const path = require("path");
68
+ const Tag = require("@saltcorn/data/models/tag");
66
69
  /**
67
70
  * @type {object}
68
71
  * @const
@@ -714,6 +717,7 @@ router.get(
714
717
  ...new Set(child_relations.map(({ table }) => table.name)),
715
718
  ];
716
719
  const triggers = table.id ? Trigger.find({ table_id: table.id }) : [];
720
+ triggers.sort(comparingCaseInsensitive("name"));
717
721
  let fieldCard;
718
722
  if (fields.length === 0) {
719
723
  fieldCard = [
@@ -785,7 +789,16 @@ router.get(
785
789
  triggers.length
786
790
  ? req.__("Table triggers: ") +
787
791
  triggers
788
- .map((t) => link(`/actions/configure/${t.id}`, t.name))
792
+ .map((t) =>
793
+ link(
794
+ `/actions/configure/${
795
+ t.id
796
+ }?on_done_redirect=${encodeURIComponent(
797
+ `table/${table.name}`
798
+ )}`,
799
+ t.name
800
+ )
801
+ )
789
802
  .join(", ") +
790
803
  "<br>"
791
804
  : "",
@@ -819,6 +832,7 @@ router.get(
819
832
  }
820
833
  viewCard = {
821
834
  type: "card",
835
+ id: "table-views",
822
836
  title: req.__("Views of this table"),
823
837
  contents:
824
838
  viewCardContents +
@@ -1160,6 +1174,32 @@ router.post(
1160
1174
  res.redirect(`/table`);
1161
1175
  return;
1162
1176
  }
1177
+ const views = await View.find(
1178
+ t.id ? { table_id: t.id } : { exttable_name: t.name }
1179
+ );
1180
+ if (views.length) {
1181
+ req.flash(
1182
+ "error",
1183
+ `${text(t.name)} has views. Delete these first: <a href="/table/${
1184
+ t.name
1185
+ }#table-views">Views for ${text(t.name)}</a>`
1186
+ );
1187
+ res.redirect(`/table`);
1188
+ return;
1189
+ }
1190
+ if (t.id) {
1191
+ const triggers = await Trigger.find({ table_id: t.id });
1192
+ if (triggers.length) {
1193
+ req.flash(
1194
+ "error",
1195
+ `${text(
1196
+ t.name
1197
+ )} has triggers. Delete these first: <a href="/actions">Trigger list</a>`
1198
+ );
1199
+ res.redirect(`/table`);
1200
+ return;
1201
+ }
1202
+ }
1163
1203
  try {
1164
1204
  await t.delete();
1165
1205
  req.flash("success", req.__(`Table %s deleted`, t.name));
@@ -1211,13 +1251,23 @@ router.get(
1211
1251
  "/",
1212
1252
  isAdmin,
1213
1253
  error_catcher(async (req, res) => {
1214
- const rows = await Table.find_with_external(
1215
- {},
1216
- { orderBy: "name", nocase: true }
1217
- );
1254
+ const tblq = {};
1255
+ let filterOnTag;
1256
+ if (req.query._tag) {
1257
+ const tagEntries = await TagEntry.find({
1258
+ tag_id: +req.query._tag,
1259
+ not: { table_id: null },
1260
+ });
1261
+ tblq.id = { in: tagEntries.map((te) => te.table_id).filter(Boolean) };
1262
+ filterOnTag = await Tag.findOne({ id: +req.query._tag });
1263
+ }
1264
+ const rows = await Table.find_with_external(tblq, {
1265
+ orderBy: "name",
1266
+ nocase: true,
1267
+ });
1218
1268
  const roles = await User.get_roles();
1219
1269
  const getRole = (rid) => roles.find((r) => r.id === rid).role;
1220
- const mainCard = await tablesList(rows, req);
1270
+ const mainCard = await tablesList(rows, req, { filterOnTag });
1221
1271
  const createCard = div(
1222
1272
  a(
1223
1273
  { href: `/table/new`, class: "btn btn-primary mt-1 me-3" },
@@ -5,6 +5,7 @@ const {
5
5
  select,
6
6
  option,
7
7
  label,
8
+ text,
8
9
  } = require("@saltcorn/markup/tags");
9
10
 
10
11
  const Tag = require("@saltcorn/data/models/tag");
@@ -29,7 +30,12 @@ const buildFields = (entryType, formOptions, req) => {
29
30
  div(
30
31
  { class: "col-sm-10" },
31
32
  select(
32
- { 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
+ },
33
39
  list.map((entry) => {
34
40
  return option({ value: entry.id, label: entry.name });
35
41
  })
@@ -94,15 +100,21 @@ router.get(
94
100
  isAdmin,
95
101
  error_catcher(async (req, res) => {
96
102
  const { entry_type, tag_id } = req.params;
97
- res.sendWrap(req.__("Add %s to tag"), {
103
+ const tag = await Tag.findOne({ id: tag_id });
104
+
105
+ res.sendWrap(req.__("Add %s to tag %s", entry_type, tag.name), {
98
106
  above: [
99
107
  {
100
108
  type: "breadcrumbs",
101
- crumbs: [{ text: req.__(`Tag entry`) }],
109
+ crumbs: [
110
+ { text: req.__(`Tags`), href: "/tag" },
111
+ { text: tag.name, href: `/tag/${tag.id}` },
112
+ { text: req.__(`Add %s`, text(entry_type)) },
113
+ ],
102
114
  },
103
115
  {
104
116
  type: "card",
105
- title: req.__(`Add entries to tag`),
117
+ title: req.__(`Add entries to tag %s`, tag.name),
106
118
  contents: buildForm(
107
119
  entry_type,
108
120
  tag_id,
@@ -148,9 +160,10 @@ router.post(
148
160
  req.flash("error", req.__("Please select at least one item"));
149
161
  return res.redirect(`/tag-entries/add/${entry_type}/${tag_id}`);
150
162
  }
163
+ const ids_array = Array.isArray(ids) ? ids : [ids];
151
164
  const fieldName = idField(entry_type);
152
165
  const tag = await Tag.findOne({ id: tag_id });
153
- for (const id of ids) {
166
+ for (const id of ids_array) {
154
167
  await tag.addEntry({ [fieldName]: id });
155
168
  }
156
169
  res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);