@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
@@ -1,5 +1,7 @@
1
1
  const User = require("@saltcorn/data/models/user");
2
2
  const Table = require("@saltcorn/data/models/table");
3
+ const Tag = require("@saltcorn/data/models/tag");
4
+ const TagEntry = require("@saltcorn/data/models/tag_entry");
3
5
  const { editRoleForm } = require("../markup/forms.js");
4
6
  const {
5
7
  mkTable,
@@ -7,16 +9,16 @@ const {
7
9
  post_delete_btn,
8
10
  settingsDropdown,
9
11
  post_dropdown_item,
12
+ badge,
10
13
  } = require("@saltcorn/markup");
11
14
  const { get_base_url } = require("./utils.js");
12
- const { h4, p, div, a, i, text } = require("@saltcorn/markup/tags");
15
+ const { h4, p, div, a, i, text, span, nbsp } = require("@saltcorn/markup/tags");
13
16
 
14
17
  /**
15
18
  * @param {string} col
16
19
  * @param {string} lbl
17
20
  * @returns {string}
18
21
  */
19
- const badge = (col, lbl) => `<span class="badge bg-${col}">${lbl}</span>&nbsp;`;
20
22
 
21
23
  /**
22
24
  * Table badges to show in System Table list views
@@ -42,57 +44,90 @@ const valIfSet = (check, value) => (check ? value : "");
42
44
  const listClass = (tagId, showList) =>
43
45
  valIfSet(tagId, `collapse ${valIfSet(showList, "show")}`);
44
46
 
45
- const tablesList = async (tables, req, { tagId, domId, showList } = {}) => {
47
+ const tablesList = async (
48
+ tables,
49
+ req,
50
+ { tagId, domId, showList, filterOnTag } = {}
51
+ ) => {
46
52
  const roles = await User.get_roles();
47
53
  const getRole = (rid) => roles.find((r) => r.id === rid)?.role || "?";
48
- return tables.length > 0
49
- ? mkTable(
50
- [
51
- {
52
- label: req.__("Name"),
53
- key: (r) => link(`/table/${r.id || r.name}`, text(r.name)),
54
- },
55
- {
56
- label: "",
57
- key: (r) => tableBadges(r, req),
58
- },
59
- {
60
- label: req.__("Access Read/Write"),
61
- key: (t) =>
62
- t.external
63
- ? `${getRole(t.min_role_read)} (read only)`
64
- : `${getRole(t.min_role_read)}/${getRole(t.min_role_write)}`,
65
- },
66
- !tagId
67
- ? {
68
- label: req.__("Delete"),
69
- key: (r) =>
70
- r.name === "users" || r.external
71
- ? ""
72
- : post_delete_btn(`/table/delete/${r.id}`, req, r.name),
73
- }
74
- : {
75
- label: req.__("Remove From Tag"),
76
- key: (r) =>
77
- post_delete_btn(
78
- `/tag-entries/remove/tables/${r.id}/${tagId}`,
79
- req,
80
- `${r.name} from this tag`
81
- ),
54
+ const tags = await Tag.find();
55
+ const tag_entries = await TagEntry.find({
56
+ not: { table_id: null },
57
+ });
58
+ const tagsById = {};
59
+ tags.forEach((t) => (tagsById[t.id] = t));
60
+
61
+ const tagBadges = (table) => {
62
+ const myTags = tag_entries.filter((te) => te.table_id === table.id);
63
+ return myTags
64
+ .map((te) => tagBadge(tagsById[te.tag_id], "tables"))
65
+ .join(nbsp);
66
+ };
67
+
68
+ return (
69
+ mkTable(
70
+ [
71
+ {
72
+ label: req.__("Name"),
73
+ key: (r) => link(`/table/${r.id || r.name}`, text(r.name)),
74
+ },
75
+ ...(tagId
76
+ ? []
77
+ : [
78
+ {
79
+ label: tagsDropdown(
80
+ tags,
81
+ filterOnTag ? `Tag:${filterOnTag.name}` : undefined
82
+ ),
83
+ key: (r) => tagBadges(r),
82
84
  },
83
- ],
84
- tables,
85
+ ]),
85
86
  {
86
- hover: true,
87
- tableClass: listClass(tagId, showList),
88
- tableId: domId,
89
- }
90
- )
91
- : div(
92
- { class: listClass(tagId, showList), id: domId },
93
- h4(req.__("No tables defined")),
94
- p(req.__("Tables hold collections of similar data"))
95
- );
87
+ label: "",
88
+ key: (r) => tableBadges(r, req),
89
+ },
90
+
91
+ {
92
+ label: req.__("Access Read/Write"),
93
+ key: (t) =>
94
+ t.external
95
+ ? `${getRole(t.min_role_read)} (read only)`
96
+ : `${getRole(t.min_role_read)}/${getRole(t.min_role_write)}`,
97
+ },
98
+ !tagId
99
+ ? {
100
+ label: req.__("Delete"),
101
+ key: (r) =>
102
+ r.name === "users" || r.external
103
+ ? ""
104
+ : post_delete_btn(`/table/delete/${r.id}`, req, r.name),
105
+ }
106
+ : {
107
+ label: req.__("Remove From Tag"),
108
+ key: (r) =>
109
+ post_delete_btn(
110
+ `/tag-entries/remove/tables/${r.id}/${tagId}`,
111
+ req,
112
+ `${r.name} from this tag`
113
+ ),
114
+ },
115
+ ],
116
+ tables,
117
+ {
118
+ hover: true,
119
+ tableClass: listClass(tagId, showList),
120
+ tableId: domId,
121
+ }
122
+ ) +
123
+ (tables.length == 0 && !filterOnTag
124
+ ? div(
125
+ { class: listClass(tagId, showList), id: domId },
126
+ h4(req.__("No tables defined")),
127
+ p(req.__("Tables hold collections of similar data"))
128
+ )
129
+ : "")
130
+ );
96
131
  };
97
132
 
98
133
  /**
@@ -177,100 +212,176 @@ const setTableRefs = async (views) => {
177
212
  return views;
178
213
  };
179
214
 
215
+ const tagBadge = (tag, type) =>
216
+ a(
217
+ {
218
+ href: `/tag/${tag.id}?show_list=${type}`,
219
+ class: "badge bg-secondary",
220
+ },
221
+ tag.name
222
+ );
223
+
224
+ const tagsDropdown = (tags, altHeader) =>
225
+ div(
226
+ { class: "dropdown" },
227
+ div(
228
+ {
229
+ class: "link-style",
230
+ "data-boundary": "viewport",
231
+ type: "button",
232
+ id: "tagsselector",
233
+ "data-bs-toggle": "dropdown",
234
+ "aria-haspopup": "true",
235
+ "aria-expanded": "false",
236
+ },
237
+ altHeader || "Tags",
238
+ i({ class: "ms-1 fas fa-caret-down" })
239
+ ),
240
+ div(
241
+ {
242
+ class: "dropdown-menu",
243
+ "aria-labelledby": "tagsselector",
244
+ },
245
+ a(
246
+ {
247
+ class: "dropdown-item",
248
+ // TODO check url why view for page, what do we need for page group
249
+ href: `javascript:unset_state_field('_tag', this)`,
250
+ },
251
+ "All tags"
252
+ ),
253
+ tags.map((tag) =>
254
+ a(
255
+ {
256
+ class: "dropdown-item",
257
+ // TODO check url why view for page, what do we need for page group
258
+ href: `javascript:set_state_field('_tag', ${tag.id}, this)`,
259
+ },
260
+ tag.name
261
+ )
262
+ ),
263
+ a(
264
+ {
265
+ class: "dropdown-item",
266
+ // TODO check url why view for page, what do we need for page group
267
+ href: `tag`,
268
+ },
269
+ "Manage tags..."
270
+ )
271
+ )
272
+ );
273
+
180
274
  const viewsList = async (
181
275
  views,
182
276
  req,
183
- { tagId, domId, showList, on_done_redirect, notable } = {}
277
+ { tagId, domId, showList, on_done_redirect, notable, filterOnTag } = {}
184
278
  ) => {
185
279
  const roles = await User.get_roles();
186
280
  const on_done_redirect_str = on_done_redirect
187
281
  ? `?on_done_redirect=${on_done_redirect}`
188
282
  : "";
189
- return views.length > 0
190
- ? mkTable(
191
- [
192
- {
193
- label: req.__("Name"),
194
- key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
195
- sortlink: !tagId
196
- ? `set_state_field('_sortby', 'name', this)`
197
- : undefined,
198
- },
199
- // description - currently I dont want to show description in view list
200
- // because description can be long
201
- /*
202
- {
203
- label: req.__("Description"),
204
- key: "description",
205
- // this is sorting by column
206
- sortlink: `javascript:set_state_field('_sortby', 'description')`,
207
- },
208
- */
209
- // template
210
- {
211
- label: req.__("Pattern"),
212
- key: "viewtemplate",
213
- sortlink: !tagId
214
- ? `set_state_field('_sortby', 'viewtemplate', this)`
215
- : undefined,
216
- },
217
- ...(notable
218
- ? []
219
- : [
220
- {
221
- label: req.__("Table"),
222
- key: (r) => link(`/table/${r.table}`, r.table),
223
- sortlink: !tagId
224
- ? `set_state_field('_sortby', 'table', this)`
225
- : undefined,
226
- },
227
- ]),
228
- {
229
- label: req.__("Role to access"),
230
- key: (row) =>
231
- row.id
232
- ? editViewRoleForm(row, roles, req, on_done_redirect_str)
233
- : "admin",
234
- },
235
- {
236
- label: "",
237
- key: (r) =>
238
- r.id && r.viewtemplateObj?.configuration_workflow
239
- ? link(
240
- `/viewedit/config/${encodeURIComponent(
241
- r.name
242
- )}${on_done_redirect_str}`,
243
- req.__("Configure")
244
- )
245
- : "",
246
- },
247
- !tagId
248
- ? {
249
- label: "",
250
- key: (r) => view_dropdown(r, req, on_done_redirect_str),
251
- }
252
- : {
253
- label: req.__("Remove From Tag"),
254
- key: (r) =>
255
- post_delete_btn(
256
- `/tag-entries/remove/views/${r.id}/${tagId}`,
257
- req,
258
- `${r.name} from this tag`
259
- ),
283
+ const tags = await Tag.find();
284
+ const tag_entries = await TagEntry.find({
285
+ not: { view_id: null },
286
+ });
287
+ const tagsById = {};
288
+ tags.forEach((t) => (tagsById[t.id] = t));
289
+
290
+ const tagBadges = (view) => {
291
+ const myTags = tag_entries.filter((te) => te.view_id === view.id);
292
+ return myTags
293
+ .map((te) => tagBadge(tagsById[te.tag_id], "views"))
294
+ .join(nbsp);
295
+ };
296
+
297
+ return (
298
+ mkTable(
299
+ [
300
+ {
301
+ label: req.__("Name"),
302
+ key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
303
+ sortlink: !tagId
304
+ ? `set_state_field('_sortby', 'name', this)`
305
+ : undefined,
306
+ },
307
+ ...(tagId
308
+ ? []
309
+ : [
310
+ {
311
+ label: tagsDropdown(
312
+ tags,
313
+ filterOnTag ? `Tag:${filterOnTag.name}` : undefined
314
+ ),
315
+ key: (r) => tagBadges(r),
260
316
  },
261
- ],
262
- views,
317
+ ]),
263
318
  {
264
- hover: true,
265
- tableClass: listClass(tagId, showList),
266
- tableId: domId,
267
- }
268
- )
269
- : div(
270
- { class: listClass(tagId, showList), id: domId },
271
- h4(req.__("No views defined")),
272
- p(req.__("Views define how table rows are displayed to the user"))
273
- );
319
+ label: req.__("Pattern"),
320
+ key: "viewtemplate",
321
+ sortlink: !tagId
322
+ ? `set_state_field('_sortby', 'viewtemplate', this)`
323
+ : undefined,
324
+ },
325
+ ...(notable
326
+ ? []
327
+ : [
328
+ {
329
+ label: req.__("Table"),
330
+ key: (r) => link(`/table/${r.table}`, r.table),
331
+ sortlink: !tagId
332
+ ? `set_state_field('_sortby', 'table', this)`
333
+ : undefined,
334
+ },
335
+ ]),
336
+ {
337
+ label: req.__("Role to access"),
338
+ key: (row) =>
339
+ row.id
340
+ ? editViewRoleForm(row, roles, req, on_done_redirect_str)
341
+ : "admin",
342
+ },
343
+ {
344
+ label: "",
345
+ key: (r) =>
346
+ r.id && r.viewtemplateObj?.configuration_workflow
347
+ ? link(
348
+ `/viewedit/config/${encodeURIComponent(
349
+ r.name
350
+ )}${on_done_redirect_str}`,
351
+ req.__("Configure")
352
+ )
353
+ : "",
354
+ },
355
+ !tagId
356
+ ? {
357
+ label: "",
358
+ key: (r) => view_dropdown(r, req, on_done_redirect_str),
359
+ }
360
+ : {
361
+ label: req.__("Remove From Tag"),
362
+ key: (r) =>
363
+ post_delete_btn(
364
+ `/tag-entries/remove/views/${r.id}/${tagId}`,
365
+ req,
366
+ `${r.name} from this tag`
367
+ ),
368
+ },
369
+ ],
370
+ views,
371
+ {
372
+ hover: true,
373
+ tableClass: listClass(tagId, showList),
374
+ tableId: domId,
375
+ }
376
+ ) +
377
+ (views.length == 0 && !filterOnTag
378
+ ? div(
379
+ { class: listClass(tagId, showList), id: domId },
380
+ h4(req.__("No views defined")),
381
+ p(req.__("Views define how table rows are displayed to the user"))
382
+ )
383
+ : "")
384
+ );
274
385
  };
275
386
 
276
387
  const page_group_dropdown = (page_group, req) =>
@@ -371,20 +482,50 @@ const editPageRoleForm = (page, roles, req, isGroup) =>
371
482
  * @param {object} req
372
483
  * @returns {div}
373
484
  */
374
- const getPageList = (rows, roles, req, { tagId, domId, showList } = {}) => {
485
+ const getPageList = async (
486
+ rows,
487
+ roles,
488
+ req,
489
+ { tagId, domId, showList, filterOnTag } = {}
490
+ ) => {
491
+ const tags = await Tag.find();
492
+ const tag_entries = await TagEntry.find({
493
+ not: { page_id: null },
494
+ });
495
+ const tagsById = {};
496
+ tags.forEach((t) => (tagsById[t.id] = t));
497
+
498
+ const tagBadges = (page) => {
499
+ const myTags = tag_entries.filter((te) => te.page_id === page.id);
500
+ return myTags
501
+ .map((te) => tagBadge(tagsById[te.tag_id], "pages"))
502
+ .join(nbsp);
503
+ };
375
504
  return mkTable(
376
505
  [
377
506
  {
378
507
  label: req.__("Name"),
379
- key: (r) => link(`/page/${r.name}`, r.name),
508
+ key: (r) => link(`/page/${encodeURIComponent(r.name)}`, r.name),
380
509
  },
510
+ ...(tagId
511
+ ? []
512
+ : [
513
+ {
514
+ label: tagsDropdown(
515
+ tags,
516
+ filterOnTag ? `Tag:${filterOnTag.name}` : undefined
517
+ ),
518
+ key: (r) => tagBadges(r),
519
+ },
520
+ ]),
381
521
  {
382
522
  label: req.__("Role to access"),
383
523
  key: (row) => editPageRoleForm(row, roles, req),
384
524
  },
385
525
  {
386
526
  label: req.__("Edit"),
387
- key: (r) => link(`/pageedit/edit/${r.name}`, req.__("Edit")),
527
+ key: (r) =>
528
+ link(`/pageedit/edit/${encodeURIComponent(r.name)}`, req.__("Edit")),
388
529
  },
389
530
  !tagId
390
531
  ? {
@@ -442,19 +583,70 @@ const getPageGroupList = (rows, roles, req) => {
442
583
  );
443
584
  };
444
585
 
445
- const getTriggerList = (triggers, req, { tagId, domId, showList } = {}) => {
586
+ const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
587
+ settingsDropdown(`dropdownMenuButton${trigger.id}`, [
588
+ a(
589
+ {
590
+ class: "dropdown-item",
591
+ href: `/actions/edit/${trigger.id}${on_done_redirect_str}`,
592
+ },
593
+ '<i class="fas fa-edit"></i>&nbsp;' + req.__("Edit")
594
+ ),
595
+
596
+ a(
597
+ {
598
+ class: "dropdown-item",
599
+ href: `javascript:ajax_modal('/admin/snapshot-restore/trigger/${trigger.name}')`,
600
+ },
601
+ '<i class="fas fa-undo-alt"></i>&nbsp;' + req.__("Restore")
602
+ ),
603
+ div({ class: "dropdown-divider" }),
604
+
605
+ post_dropdown_item(
606
+ `/actions/delete/${trigger.id}${on_done_redirect_str}`,
607
+ '<i class="far fa-trash-alt"></i>&nbsp;' + req.__("Delete"),
608
+ req,
609
+ true,
610
+ trigger.name
611
+ ),
612
+ ]);
613
+
614
+ const getTriggerList = async (
615
+ triggers,
616
+ req,
617
+ { tagId, domId, showList, filterOnTag } = {}
618
+ ) => {
446
619
  const base_url = get_base_url(req);
620
+ const tags = await Tag.find();
621
+
622
+ const tag_entries = await TagEntry.find({
623
+ not: { trigger_id: null },
624
+ });
625
+ const tagsById = {};
626
+ tags.forEach((t) => (tagsById[t.id] = t));
627
+
628
+ const tagBadges = (trigger) => {
629
+ const myTags = tag_entries.filter((te) => te.trigger_id === trigger.id);
630
+ return myTags
631
+ .map((te) => tagBadge(tagsById[te.tag_id], "triggers"))
632
+ .join(nbsp);
633
+ };
447
634
  return mkTable(
448
635
  [
449
636
  { label: req.__("Name"), key: "name" },
637
+ ...(tagId
638
+ ? []
639
+ : [
640
+ {
641
+ label: tagsDropdown(
642
+ tags,
643
+ filterOnTag ? `Tag:${filterOnTag.name}` : undefined
644
+ ),
645
+ key: (r) => tagBadges(r),
646
+ },
647
+ ]),
450
648
  { label: req.__("Action"), key: "action" },
451
- {
452
- label: req.__("Table or Channel"),
453
- key: (r) =>
454
- r.table_name
455
- ? a({ href: `/table/${r.table_name}` }, r.table_name)
456
- : r.channel,
457
- },
649
+
458
650
  {
459
651
  label: req.__("When"),
460
652
  key: (act) =>
@@ -469,15 +661,15 @@ const getTriggerList = (triggers, req, { tagId, domId, showList } = {}) => {
469
661
  : ""),
470
662
  },
471
663
  {
472
- label: req.__("Test run"),
664
+ label: req.__("Table or Channel"),
473
665
  key: (r) =>
474
- r.table_id
475
- ? ""
476
- : link(`/actions/testrun/${r.id}`, req.__("Test run")),
666
+ r.table_name
667
+ ? a({ href: `/table/${r.table_name}` }, r.table_name)
668
+ : r.channel,
477
669
  },
478
670
  {
479
- label: req.__("Edit"),
480
- key: (r) => link(`/actions/edit/${r.id}`, req.__("Edit")),
671
+ label: req.__("Test run"),
672
+ key: (r) => link(`/actions/testrun/${r.id}`, req.__("Test run")),
481
673
  },
482
674
  {
483
675
  label: req.__("Configure"),
@@ -485,8 +677,8 @@ const getTriggerList = (triggers, req, { tagId, domId, showList } = {}) => {
485
677
  },
486
678
  !tagId
487
679
  ? {
488
- label: req.__("Delete"),
489
- key: (r) => post_delete_btn(`/actions/delete/${r.id}`, req),
680
+ label: "",
681
+ key: (r) => trigger_dropdown(r, req),
490
682
  }
491
683
  : {
492
684
  label: req.__("Remove From Tag"),
package/routes/fields.js CHANGED
@@ -1180,9 +1180,24 @@ router.post(
1180
1180
  type,
1181
1181
  join_field,
1182
1182
  join_fieldview,
1183
+ agg_outcome_type,
1184
+ agg_fieldview,
1185
+ agg_field,
1183
1186
  _columndef,
1184
1187
  } = req.body;
1185
1188
  const table = Table.findOne({ name: tableName });
1189
+ if (agg_outcome_type && agg_fieldview) {
1190
+ const type = getState().types[agg_outcome_type];
1191
+ const fv = type?.fieldviews?.[agg_fieldview];
1192
+ if (!fv?.configFields) {
1193
+ res.send(req.query?.accept == "json" ? "[]" : "");
1194
+ return;
1195
+ }
1196
+ const field = table.getField(agg_field);
1197
+ const cfgfields = await applyAsync(fv.configFields, field || { table });
1198
+ res.json(cfgfields);
1199
+ return;
1200
+ }
1186
1201
  if (typeof type !== "string") {
1187
1202
  try {
1188
1203
  type = JSON.parse(_columndef).type;
@@ -1193,19 +1208,19 @@ router.post(
1193
1208
  const fieldName = type == "Field" ? field_name : join_field;
1194
1209
  const fv_name = type == "Field" ? fieldview : join_fieldview;
1195
1210
  if (!fieldName) {
1196
- res.send("");
1211
+ res.send(req.query?.accept == "json" ? "[]" : "");
1197
1212
  return;
1198
1213
  }
1199
1214
 
1200
1215
  const field = table.getField(fieldName);
1201
1216
  if (!field) {
1202
- res.send("");
1217
+ res.send(req.query?.accept == "json" ? "[]" : "");
1203
1218
  return;
1204
1219
  }
1205
1220
  const fieldViewConfigForms = await calcfldViewConfig([field], false, 0);
1206
1221
  const formFields = fieldViewConfigForms[field.name][fv_name];
1207
1222
  if (!formFields) {
1208
- res.send("");
1223
+ res.send(req.query?.accept == "json" ? "[]" : "");
1209
1224
  return;
1210
1225
  }
1211
1226
  formFields.forEach((ff) => {
@@ -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(