@saltcorn/server 0.9.6-beta.2 → 0.9.6-beta.20

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 (49) hide show
  1. package/app.js +6 -1
  2. package/auth/admin.js +55 -53
  3. package/auth/routes.js +28 -10
  4. package/auth/testhelp.js +86 -0
  5. package/help/Field label.tmd +11 -0
  6. package/help/Field types.tmd +39 -0
  7. package/help/Ownership field.tmd +76 -0
  8. package/help/Ownership formula.tmd +75 -0
  9. package/help/Table roles.tmd +20 -0
  10. package/help/User groups.tmd +35 -0
  11. package/load_plugins.js +33 -5
  12. package/locales/en.json +29 -1
  13. package/locales/it.json +3 -2
  14. package/markup/admin.js +1 -0
  15. package/markup/forms.js +5 -1
  16. package/package.json +9 -9
  17. package/public/log_viewer_utils.js +32 -0
  18. package/public/mermaid.min.js +705 -306
  19. package/public/saltcorn-builder.css +23 -0
  20. package/public/saltcorn-common.js +248 -80
  21. package/public/saltcorn.css +80 -0
  22. package/public/saltcorn.js +86 -2
  23. package/restart_watcher.js +1 -0
  24. package/routes/actions.js +27 -0
  25. package/routes/admin.js +175 -64
  26. package/routes/api.js +6 -0
  27. package/routes/common_lists.js +42 -32
  28. package/routes/fields.js +70 -42
  29. package/routes/homepage.js +2 -0
  30. package/routes/index.js +2 -0
  31. package/routes/menu.js +69 -4
  32. package/routes/notifications.js +90 -10
  33. package/routes/pageedit.js +18 -13
  34. package/routes/plugins.js +11 -2
  35. package/routes/registry.js +289 -0
  36. package/routes/search.js +10 -4
  37. package/routes/tables.js +51 -27
  38. package/routes/tenant.js +4 -15
  39. package/routes/utils.js +25 -8
  40. package/routes/view.js +1 -1
  41. package/routes/viewedit.js +11 -7
  42. package/serve.js +27 -5
  43. package/tests/edit.test.js +426 -0
  44. package/tests/fields.test.js +21 -0
  45. package/tests/filter.test.js +68 -0
  46. package/tests/page.test.js +2 -2
  47. package/tests/plugins.test.js +2 -0
  48. package/tests/sync.test.js +59 -0
  49. package/wrapper.js +4 -1
package/routes/menu.js CHANGED
@@ -15,6 +15,7 @@ const { getState } = require("@saltcorn/data/db/state");
15
15
  const User = require("@saltcorn/data/models/user");
16
16
  const View = require("@saltcorn/data/models/view");
17
17
  const Page = require("@saltcorn/data/models/page");
18
+ const PageGroup = require("@saltcorn/data/models/page_group");
18
19
  const { save_menu_items } = require("@saltcorn/data/models/config");
19
20
  const db = require("@saltcorn/data/db");
20
21
 
@@ -43,6 +44,10 @@ module.exports = router;
43
44
  const menuForm = async (req) => {
44
45
  const views = await View.find({}, { orderBy: "name", nocase: true });
45
46
  const pages = await Page.find({}, { orderBy: "name", nocase: true });
47
+ const pageGroups = await PageGroup.find(
48
+ {},
49
+ { orderBy: "name", nocase: true }
50
+ );
46
51
  const roles = await User.get_roles();
47
52
  const tables = await Table.find_with_external({});
48
53
  const dynTableOptions = tables.map((t) => t.name);
@@ -101,6 +106,7 @@ const menuForm = async (req) => {
101
106
  options: [
102
107
  "View",
103
108
  "Page",
109
+ "Page Group",
104
110
  "Link",
105
111
  "Header",
106
112
  "Dynamic",
@@ -141,6 +147,14 @@ const menuForm = async (req) => {
141
147
  attributes: { options: views.map((r) => r.select_option) },
142
148
  showIf: { type: "View" },
143
149
  },
150
+ {
151
+ name: "page_group",
152
+ label: req.__("Page group"),
153
+ input_type: "select",
154
+ class: "item-menu",
155
+ options: pageGroups.map((r) => r.name),
156
+ showIf: { type: "Page Group" },
157
+ },
144
158
  {
145
159
  name: "action_name",
146
160
  label: req.__("Action"),
@@ -194,6 +208,14 @@ const menuForm = async (req) => {
194
208
  required: true,
195
209
  showIf: { type: "Dynamic" },
196
210
  },
211
+ {
212
+ name: "dyn_tooltip_fml",
213
+ label: req.__("Tooltip formula"),
214
+ class: "item-menu",
215
+ type: "String",
216
+ required: false,
217
+ showIf: { type: "Dynamic" },
218
+ },
197
219
  {
198
220
  name: "dyn_url_fml",
199
221
  label: req.__("URL formula"),
@@ -223,6 +245,7 @@ const menuForm = async (req) => {
223
245
  type: [
224
246
  "View",
225
247
  "Page",
248
+ "Page Group",
226
249
  "Link",
227
250
  "Header",
228
251
  "Dynamic",
@@ -238,13 +261,34 @@ const menuForm = async (req) => {
238
261
  attributes: {
239
262
  html: `<button type="button" id="myEditor_icon" class="btn btn-outline-secondary"></button>`,
240
263
  },
241
- showIf: { type: ["View", "Page", "Link", "Header", "Action"] },
264
+ showIf: {
265
+ type: ["View", "Page", "Page Group", "Link", "Header", "Action"],
266
+ },
242
267
  },
243
268
  {
244
269
  name: "icon",
245
270
  class: "item-menu",
246
271
  input_type: "hidden",
247
272
  },
273
+ {
274
+ name: "tooltip",
275
+ label: req.__("Tooltip"),
276
+ class: "item-menu",
277
+ input_type: "text",
278
+ required: false,
279
+ showIf: {
280
+ type: [
281
+ "View",
282
+ "Page",
283
+ "Page Group",
284
+ "Link",
285
+ "Header",
286
+ "Dynamic",
287
+ "Search",
288
+ "Action",
289
+ ],
290
+ },
291
+ },
248
292
  {
249
293
  name: "min_role",
250
294
  label: req.__("Minimum role"),
@@ -258,6 +302,18 @@ const menuForm = async (req) => {
258
302
  type: "Bool",
259
303
  class: "item-menu",
260
304
  required: false,
305
+ default: false,
306
+ },
307
+ {
308
+ name: "mobile_item_html",
309
+ label: req.__("Mobile HTML"),
310
+ sublabel: req.__(
311
+ "HTML for the item in the bottom navigation bar. Currently, only supported by the metronic theme."
312
+ ),
313
+ type: "String",
314
+ class: "item-menu",
315
+ input_type: "textarea",
316
+ showIf: { disable_on_mobile: false, location: "Mobile Bottom" },
261
317
  },
262
318
  {
263
319
  name: "target_blank",
@@ -265,7 +321,7 @@ const menuForm = async (req) => {
265
321
  type: "Bool",
266
322
  required: false,
267
323
  class: "item-menu",
268
- showIf: { type: ["View", "Page", "Link"] },
324
+ showIf: { type: ["View", "Page", "Page Group", "Link"] },
269
325
  },
270
326
  {
271
327
  name: "in_modal",
@@ -273,7 +329,7 @@ const menuForm = async (req) => {
273
329
  type: "Bool",
274
330
  required: false,
275
331
  class: "item-menu",
276
- showIf: { type: ["View", "Page", "Link"] },
332
+ showIf: { type: ["View", "Page", "Page Group", "Link"] },
277
333
  },
278
334
  {
279
335
  name: "style",
@@ -283,7 +339,15 @@ const menuForm = async (req) => {
283
339
  type: "String",
284
340
  required: true,
285
341
  showIf: {
286
- type: ["View", "Page", "Link", "Header", "Dynamic", "Action"],
342
+ type: [
343
+ "View",
344
+ "Page",
345
+ "Page Group",
346
+ "Link",
347
+ "Header",
348
+ "Dynamic",
349
+ "Action",
350
+ ],
287
351
  },
288
352
  attributes: {
289
353
  options: [
@@ -310,6 +374,7 @@ const menuForm = async (req) => {
310
374
  type: [
311
375
  "View",
312
376
  "Page",
377
+ "Page Group",
313
378
  "Link",
314
379
  "Header",
315
380
  "Dynamic",
@@ -13,7 +13,7 @@ const { getState } = require("@saltcorn/data/db/state");
13
13
  const Form = require("@saltcorn/data/models/form");
14
14
  const File = require("@saltcorn/data/models/file");
15
15
  const User = require("@saltcorn/data/models/user");
16
- const { renderForm } = require("@saltcorn/markup");
16
+ const { renderForm, post_btn } = require("@saltcorn/markup");
17
17
 
18
18
  const router = new Router();
19
19
  module.exports = router;
@@ -31,22 +31,50 @@ router.get(
31
31
  "/",
32
32
  loggedIn,
33
33
  error_catcher(async (req, res) => {
34
- const nots = await Notification.find(
35
- { user_id: req.user.id },
36
- { orderBy: "id", orderDesc: true, limit: 20 }
37
- );
34
+ const { after } = req.query;
35
+ const where = { user_id: req.user.id };
36
+ if (after) where.id = { lt: after };
37
+ const nots = await Notification.find(where, {
38
+ orderBy: "id",
39
+ orderDesc: true,
40
+ limit: 20,
41
+ });
38
42
  await Notification.mark_as_read({
39
43
  id: { in: nots.filter((n) => !n.read).map((n) => n.id) },
40
44
  });
45
+ const form = notificationSettingsForm();
46
+ const user = await User.findOne({ id: req.user?.id });
47
+ form.values = { notify_email: user?._attributes?.notify_email };
41
48
  const notifyCards = nots.length
42
49
  ? nots.map((not) => ({
43
50
  type: "card",
44
51
  class: [!not.read && "unread-notify"],
52
+ id: `notify-${not.id}`,
45
53
  contents: [
46
54
  div(
47
55
  { class: "d-flex" },
48
56
  span({ class: "fw-bold" }, not.title),
49
- span({ class: "ms-2 text-muted" }, moment(not.created).fromNow())
57
+ span(
58
+ {
59
+ class: "ms-2 text-muted",
60
+ title: not.created.toLocaleString(req.getLocale()),
61
+ },
62
+ moment(not.created).fromNow()
63
+ ),
64
+ div(
65
+ { class: "ms-auto" },
66
+ post_btn(
67
+ `/notifications/delete/${not.id}`,
68
+ "",
69
+ req.csrfToken(),
70
+ {
71
+ icon: "fas fa-times-circle",
72
+ klass: "btn-link text-muted text-decoration-none p-0",
73
+ ajax: true,
74
+ onClick: `$('#notify-${not.id}').remove()`,
75
+ }
76
+ )
77
+ )
50
78
  ),
51
79
  not.body && p(not.body),
52
80
  not.link && a({ href: not.link }, "Link"),
@@ -58,6 +86,35 @@ router.get(
58
86
  contents: [h5(req.__("No notifications"))],
59
87
  },
60
88
  ];
89
+ const pageLinks = div(
90
+ { class: "d-flex mt-3 mb-3" },
91
+ nots.length == 20
92
+ ? div(
93
+ after &&
94
+ a(
95
+ { href: `/notifications`, class: "me-2" },
96
+ "&larr; " + req.__("Newest")
97
+ ),
98
+ a(
99
+ { href: `/notifications?after=${nots[19].id}` },
100
+ req.__("Older") + " &rarr;"
101
+ )
102
+ )
103
+ : div(),
104
+ nots.length > 0 &&
105
+ div(
106
+ { class: "ms-auto" },
107
+ post_btn(
108
+ `/notifications/delete/read`,
109
+ req.__("Delete all read"),
110
+ req.csrfToken(),
111
+ {
112
+ icon: "fas fa-trash",
113
+ klass: "btn-sm btn-danger",
114
+ }
115
+ )
116
+ )
117
+ );
61
118
  res.sendWrap(req.__("Notifications"), {
62
119
  above: [
63
120
  {
@@ -72,10 +129,10 @@ router.get(
72
129
  type: "card",
73
130
  contents: [
74
131
  req.__("Receive notifications by:"),
75
- renderForm(notificationSettingsForm(), req.csrfToken()),
132
+ renderForm(form, req.csrfToken()),
76
133
  ],
77
134
  },
78
- { above: notifyCards },
135
+ { above: [...notifyCards, pageLinks] },
79
136
  ],
80
137
  },
81
138
  ],
@@ -109,9 +166,27 @@ router.post(
109
166
  })
110
167
  );
111
168
 
169
+ router.post(
170
+ "/delete/:idlike",
171
+ loggedIn,
172
+ error_catcher(async (req, res) => {
173
+ const { idlike } = req.params;
174
+ if (idlike == "read") {
175
+ await Notification.deleteRead(req.user.id);
176
+ } else {
177
+ const id = +idlike;
178
+ const notif = await Notification.findOne({ id });
179
+ if (notif?.user_id == req.user?.id) await notif.delete();
180
+ }
181
+ if (req.xhr) res.json({ success: "ok" });
182
+ else res.redirect("/notifications");
183
+ })
184
+ );
185
+
112
186
  router.get(
113
- "/manifest.json",
187
+ "/manifest.json:opt_cache_bust?",
114
188
  error_catcher(async (req, res) => {
189
+ const { pretty } = req.query;
115
190
  const state = getState();
116
191
  const manifest = {
117
192
  name: state.getConfig("site_name"),
@@ -142,6 +217,11 @@ router.get(
142
217
  "red"
143
218
  );
144
219
  }
145
- res.json(manifest);
220
+ if (!pretty) res.json(manifest);
221
+ else {
222
+ const prettyJson = JSON.stringify(manifest, null, 2);
223
+ res.setHeader("Content-Type", "application/json");
224
+ res.send(prettyJson);
225
+ }
146
226
  })
147
227
  );
@@ -94,7 +94,7 @@ const pagePropertiesForm = async (req, isNew) => {
94
94
  if (groups.includes(s) && isNew)
95
95
  return req.__("A page group with this name already exists");
96
96
  },
97
- sublabel: req.__("A short name that will be in your URL"),
97
+ sublabel: req.__("A short name that will be in the page URL"),
98
98
  type: "String",
99
99
  attributes: { autofocus: true },
100
100
  }),
@@ -107,13 +107,15 @@ const pagePropertiesForm = async (req, isNew) => {
107
107
  new Field({
108
108
  label: req.__("Description"),
109
109
  name: "description",
110
- sublabel: req.__("A longer description"),
110
+ sublabel: req.__(
111
+ "A longer description that is not visible but appears in the page header and is indexed by search engines"
112
+ ),
111
113
  input_type: "text",
112
114
  }),
113
115
  {
114
116
  name: "min_role",
115
117
  label: req.__("Minimum role"),
116
- sublabel: req.__("Role required to access page"),
118
+ sublabel: req.__("User role required to access page"),
117
119
  input_type: "select",
118
120
  options: roles.map((r) => ({ value: r.id, label: r.role })),
119
121
  },
@@ -336,6 +338,15 @@ router.get(
336
338
  )
337
339
  ),
338
340
  },
341
+ {
342
+ type: "card",
343
+ title: req.__("Root pages"),
344
+ titleAjaxIndicator: true,
345
+ contents: renderForm(
346
+ getRootPageForm(pages, pageGroups, roles, req),
347
+ req.csrfToken()
348
+ ),
349
+ },
339
350
  {
340
351
  type: "card",
341
352
  title: req.__("Your page groups"),
@@ -357,15 +368,6 @@ router.get(
357
368
  )
358
369
  ),
359
370
  },
360
- {
361
- type: "card",
362
- title: req.__("Root pages"),
363
- titleAjaxIndicator: true,
364
- contents: renderForm(
365
- getRootPageForm(pages, pageGroups, roles, req),
366
- req.csrfToken()
367
- ),
368
- },
369
371
  ],
370
372
  });
371
373
  })
@@ -392,6 +394,7 @@ const wrap = (contents, noCard, req, page) => ({
392
394
  {
393
395
  type: noCard ? "container" : "card",
394
396
  title: page ? page.name : req.__("New"),
397
+ titleAjaxIndicator: true,
395
398
  contents,
396
399
  },
397
400
  ],
@@ -418,6 +421,7 @@ router.get(
418
421
  form.hidden("id");
419
422
  form.values = page;
420
423
  form.values.no_menu = page.attributes?.no_menu;
424
+ form.onChange = `saveAndContinue(this)`;
421
425
  res.sendWrap(
422
426
  req.__(`Page attributes`),
423
427
  wrap(renderForm(form, req.csrfToken()), false, req, page)
@@ -477,7 +481,8 @@ router.post(
477
481
  pageRow.layout = {};
478
482
  }
479
483
  await Page.update(+id, pageRow);
480
- res.redirect(`/pageedit/`);
484
+ if (req.xhr) res.json({ success: "ok" });
485
+ else res.redirect(`/pageedit/`);
481
486
  } else {
482
487
  if (!pageRow.layout) pageRow.layout = {};
483
488
  if (!pageRow.fixed_states) pageRow.fixed_states = {};
package/routes/plugins.js CHANGED
@@ -865,9 +865,13 @@ router.get(
865
865
  if (!module) {
866
866
  module = getState().plugins[getState().plugin_module_names[plugin.name]];
867
867
  }
868
+ const userLayout =
869
+ user._attributes?.layout?.plugin === plugin.name
870
+ ? user._attributes.layout.config || {}
871
+ : {};
868
872
  const form = await module.user_config_form({
869
873
  ...(plugin.configuration || {}),
870
- ...(user._attributes?.layout?.config || {}),
874
+ ...userLayout,
871
875
  });
872
876
  form.action = `/plugins/user_configure/${encodeURIComponent(plugin.name)}`;
873
877
  form.onChange = `applyViewConfig(this, '/plugins/user_saveconfig/${encodeURIComponent(
@@ -1315,7 +1319,12 @@ router.get(
1315
1319
  await upgrade_all_tenants_plugins((p, f) =>
1316
1320
  load_plugins.loadPlugin(p, f)
1317
1321
  );
1318
- req.flash("success", req.__(`Modules up-to-date. Please restart server`));
1322
+ req.flash(
1323
+ "success",
1324
+ req.__(`Modules up-to-date. Please restart server`) +
1325
+ ". " +
1326
+ a({ href: "/admin/system" }, req.__("Restart here"))
1327
+ );
1319
1328
  } else {
1320
1329
  const installed_plugins = await Plugin.find({});
1321
1330
  for (const plugin of installed_plugins) {