@saltcorn/server 0.9.4-beta.8 → 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 (184) 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 +29 -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 +43 -2
  155. package/public/saltcorn-common.js +39 -29
  156. package/public/saltcorn.js +29 -8
  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 +83 -9
  161. package/routes/common_lists.js +344 -152
  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 +23 -7
  167. package/routes/tables.js +51 -5
  168. package/routes/tag_entries.js +18 -5
  169. package/routes/tags.js +65 -12
  170. package/routes/utils.js +23 -2
  171. package/routes/view.js +12 -1
  172. package/routes/viewedit.js +46 -3
  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 +127 -15
  182. package/tests/viewedit.test.js +52 -8
  183. package/wrapper.js +9 -2
  184. package/public/relation_helpers.js +0 -351
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,
@@ -64,6 +65,7 @@ const {
64
65
  const { EOL } = require("os");
65
66
 
66
67
  const path = require("path");
68
+ const Tag = require("@saltcorn/data/models/tag");
67
69
  /**
68
70
  * @type {object}
69
71
  * @const
@@ -830,6 +832,7 @@ router.get(
830
832
  }
831
833
  viewCard = {
832
834
  type: "card",
835
+ id: "table-views",
833
836
  title: req.__("Views of this table"),
834
837
  contents:
835
838
  viewCardContents +
@@ -992,6 +995,13 @@ router.get(
992
995
  req,
993
996
  true
994
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
+ ),
995
1005
  ])
996
1006
  )
997
1007
  );
@@ -1171,6 +1181,32 @@ router.post(
1171
1181
  res.redirect(`/table`);
1172
1182
  return;
1173
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
+ }
1174
1210
  try {
1175
1211
  await t.delete();
1176
1212
  req.flash("success", req.__(`Table %s deleted`, t.name));
@@ -1222,13 +1258,23 @@ router.get(
1222
1258
  "/",
1223
1259
  isAdmin,
1224
1260
  error_catcher(async (req, res) => {
1225
- const rows = await Table.find_with_external(
1226
- {},
1227
- { orderBy: "name", nocase: true }
1228
- );
1261
+ const tblq = {};
1262
+ let filterOnTag;
1263
+ if (req.query._tag) {
1264
+ const tagEntries = await TagEntry.find({
1265
+ tag_id: +req.query._tag,
1266
+ not: { table_id: null },
1267
+ });
1268
+ tblq.id = { in: tagEntries.map((te) => te.table_id).filter(Boolean) };
1269
+ filterOnTag = await Tag.findOne({ id: +req.query._tag });
1270
+ }
1271
+ const rows = await Table.find_with_external(tblq, {
1272
+ orderBy: "name",
1273
+ nocase: true,
1274
+ });
1229
1275
  const roles = await User.get_roles();
1230
1276
  const getRole = (rid) => roles.find((r) => r.id === rid).role;
1231
- const mainCard = await tablesList(rows, req);
1277
+ const mainCard = await tablesList(rows, req, { filterOnTag });
1232
1278
  const createCard = div(
1233
1279
  a(
1234
1280
  { 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}`);