@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.
- package/app.js +6 -1
- package/auth/admin.js +55 -53
- package/auth/routes.js +28 -10
- package/auth/testhelp.js +86 -0
- package/help/Field label.tmd +11 -0
- package/help/Field types.tmd +39 -0
- package/help/Ownership field.tmd +76 -0
- package/help/Ownership formula.tmd +75 -0
- package/help/Table roles.tmd +20 -0
- package/help/User groups.tmd +35 -0
- package/load_plugins.js +33 -5
- package/locales/en.json +29 -1
- package/locales/it.json +3 -2
- package/markup/admin.js +1 -0
- package/markup/forms.js +5 -1
- package/package.json +9 -9
- package/public/log_viewer_utils.js +32 -0
- package/public/mermaid.min.js +705 -306
- package/public/saltcorn-builder.css +23 -0
- package/public/saltcorn-common.js +248 -80
- package/public/saltcorn.css +80 -0
- package/public/saltcorn.js +86 -2
- package/restart_watcher.js +1 -0
- package/routes/actions.js +27 -0
- package/routes/admin.js +175 -64
- package/routes/api.js +6 -0
- package/routes/common_lists.js +42 -32
- package/routes/fields.js +70 -42
- package/routes/homepage.js +2 -0
- package/routes/index.js +2 -0
- package/routes/menu.js +69 -4
- package/routes/notifications.js +90 -10
- package/routes/pageedit.js +18 -13
- package/routes/plugins.js +11 -2
- package/routes/registry.js +289 -0
- package/routes/search.js +10 -4
- package/routes/tables.js +51 -27
- package/routes/tenant.js +4 -15
- package/routes/utils.js +25 -8
- package/routes/view.js +1 -1
- package/routes/viewedit.js +11 -7
- package/serve.js +27 -5
- package/tests/edit.test.js +426 -0
- package/tests/fields.test.js +21 -0
- package/tests/filter.test.js +68 -0
- package/tests/page.test.js +2 -2
- package/tests/plugins.test.js +2 -0
- package/tests/sync.test.js +59 -0
- 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: {
|
|
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: [
|
|
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",
|
package/routes/notifications.js
CHANGED
|
@@ -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
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
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
|
+
"← " + req.__("Newest")
|
|
97
|
+
),
|
|
98
|
+
a(
|
|
99
|
+
{ href: `/notifications?after=${nots[19].id}` },
|
|
100
|
+
req.__("Older") + " →"
|
|
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(
|
|
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
|
);
|
package/routes/pageedit.js
CHANGED
|
@@ -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
|
|
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.__(
|
|
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.__("
|
|
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.
|
|
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
|
-
...
|
|
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(
|
|
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) {
|