@saltcorn/server 0.7.1-beta.0 → 0.7.1-beta.3
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/locales/en.json +8 -1
- package/locales/zh.json +888 -0
- package/package.json +7 -7
- package/public/jquery-menu-editor.min.js +68 -35
- package/public/saltcorn.css +0 -4
- package/public/saltcorn.js +63 -6
- package/routes/admin.js +62 -0
- package/routes/fields.js +22 -1
- package/routes/menu.js +0 -1
- package/routes/plugins.js +31 -14
- package/routes/viewedit.js +18 -1
- package/tests/clientjs.test.js +55 -0
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.7.1-beta.
|
|
3
|
+
"version": "0.7.1-beta.3",
|
|
4
4
|
"description": "Server app for Saltcorn, open-source no-code platform",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@saltcorn/base-plugin": "0.7.1-beta.
|
|
10
|
-
"@saltcorn/builder": "0.7.1-beta.
|
|
11
|
-
"@saltcorn/data": "0.7.1-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.7.1-beta.
|
|
13
|
-
"@saltcorn/markup": "0.7.1-beta.
|
|
14
|
-
"@saltcorn/sbadmin2": "0.7.1-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.7.1-beta.3",
|
|
10
|
+
"@saltcorn/builder": "0.7.1-beta.3",
|
|
11
|
+
"@saltcorn/data": "0.7.1-beta.3",
|
|
12
|
+
"@saltcorn/admin-models": "0.7.1-beta.3",
|
|
13
|
+
"@saltcorn/markup": "0.7.1-beta.3",
|
|
14
|
+
"@saltcorn/sbadmin2": "0.7.1-beta.3",
|
|
15
15
|
"@socket.io/cluster-adapter": "^0.1.0",
|
|
16
16
|
"@socket.io/sticky": "^1.0.1",
|
|
17
17
|
"aws-sdk": "^2.1037.0",
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
function MenuEditor(e, t) {
|
|
2
|
+
const radioValue = (e) =>
|
|
3
|
+
$(`input:radio[name ='${$(e).attr("name")}']:checked`).val();
|
|
2
4
|
var s = $("#" + e).data("level", "0"),
|
|
3
5
|
l = {
|
|
4
6
|
labelEdit: '<i class="fas fa-edit clickable"></i>',
|
|
5
7
|
labelRemove: '<i class="fas fa-trash-alt clickable"></i>',
|
|
6
8
|
textConfirmDelete: "This item will be deleted. Are you sure?",
|
|
9
|
+
getLabelText: (e) => e.text,
|
|
10
|
+
onUpdate: () => {},
|
|
7
11
|
iconPicker: { cols: 4, rows: 4, footer: !1, iconset: "fontawesome5" },
|
|
8
12
|
maxLevel: -1,
|
|
9
13
|
listOptions: {
|
|
@@ -28,6 +32,8 @@ function MenuEditor(e, t) {
|
|
|
28
32
|
},
|
|
29
33
|
};
|
|
30
34
|
$.extend(!0, l, t);
|
|
35
|
+
var mqeSets = l;
|
|
36
|
+
|
|
31
37
|
var n = null,
|
|
32
38
|
o = !0,
|
|
33
39
|
i = null,
|
|
@@ -35,10 +41,11 @@ function MenuEditor(e, t) {
|
|
|
35
41
|
a = l.iconPicker,
|
|
36
42
|
c = ((t = l.listOptions), $("#" + e + "_icon").iconpicker(a));
|
|
37
43
|
function d() {
|
|
38
|
-
i[0]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
if (!i || !i[0])
|
|
45
|
+
i[0].reset(),
|
|
46
|
+
(c = c.iconpicker(a)).iconpicker("setIcon", "empty"),
|
|
47
|
+
r.attr("disabled", !0),
|
|
48
|
+
(n = null);
|
|
42
49
|
}
|
|
43
50
|
function p(e) {
|
|
44
51
|
return $("<a>")
|
|
@@ -117,7 +124,14 @@ function MenuEditor(e, t) {
|
|
|
117
124
|
(function (e) {
|
|
118
125
|
var t = e.data();
|
|
119
126
|
$.each(t, function (e, t) {
|
|
120
|
-
i.find("[name=" + e + "]")
|
|
127
|
+
var el = i.find("[name=" + e + "]");
|
|
128
|
+
if (el.prop("type") == "checkbox") el.prop("checked", !!t);
|
|
129
|
+
else if (el.prop("type") == "radio") {
|
|
130
|
+
$(`input[name=${el.prop("name")}][value="${t}"]`).prop(
|
|
131
|
+
"checked",
|
|
132
|
+
true
|
|
133
|
+
);
|
|
134
|
+
} else el.val(t);
|
|
121
135
|
}),
|
|
122
136
|
i.find(".item-menu").first().focus(),
|
|
123
137
|
t.hasOwnProperty("icon")
|
|
@@ -125,6 +139,7 @@ function MenuEditor(e, t) {
|
|
|
125
139
|
: c.iconpicker("setIcon", "empty");
|
|
126
140
|
r.removeAttr("disabled");
|
|
127
141
|
})((n = $(this).closest("li")));
|
|
142
|
+
l.onUpdate();
|
|
128
143
|
}),
|
|
129
144
|
s.on("click", ".btnUp", function (e) {
|
|
130
145
|
e.preventDefault();
|
|
@@ -180,20 +195,34 @@ function MenuEditor(e, t) {
|
|
|
180
195
|
if (null !== e) {
|
|
181
196
|
var t = e.data("icon");
|
|
182
197
|
i.find(".item-menu").each(function () {
|
|
183
|
-
|
|
198
|
+
if (!$(this).prop("disabled"))
|
|
199
|
+
e.data(
|
|
200
|
+
$(this).attr("name"),
|
|
201
|
+
$(this).attr("type") === "radio"
|
|
202
|
+
? radioValue(this)
|
|
203
|
+
: $(this).attr("type") === "checkbox"
|
|
204
|
+
? $(this).prop("checked")
|
|
205
|
+
: $(this).val()
|
|
206
|
+
);
|
|
184
207
|
}),
|
|
185
208
|
e.children().children("i").removeClass(t).addClass(e.data("icon")),
|
|
186
|
-
e.find("span.txt").first().text(e.data(
|
|
209
|
+
e.find("span.txt").first().text(mqeSets.getLabelText(e.data())),
|
|
187
210
|
d();
|
|
188
211
|
}
|
|
189
212
|
}),
|
|
190
213
|
(this.add = function () {
|
|
191
214
|
var e = {};
|
|
192
215
|
i.find(".item-menu").each(function () {
|
|
193
|
-
|
|
216
|
+
if (!$(this).prop("disabled"))
|
|
217
|
+
e[$(this).attr("name")] =
|
|
218
|
+
$(this).attr("type") === "radio"
|
|
219
|
+
? radioValue(this)
|
|
220
|
+
: $(this).attr("type") === "checkbox"
|
|
221
|
+
? $(this).prop("checked")
|
|
222
|
+
: $(this).val();
|
|
194
223
|
});
|
|
195
224
|
var t = u(),
|
|
196
|
-
l = $("<span>").addClass("txt").text(e
|
|
225
|
+
l = $("<span>").addClass("txt").text(mqeSets.getLabelText(e)),
|
|
197
226
|
n = $("<i>").addClass(e.icon),
|
|
198
227
|
o = $("<div>")
|
|
199
228
|
.css({ overflow: "auto" })
|
|
@@ -251,7 +280,7 @@ function MenuEditor(e, t) {
|
|
|
251
280
|
d = $("<i>").addClass(s.icon),
|
|
252
281
|
p = $("<span>")
|
|
253
282
|
.addClass("txt")
|
|
254
|
-
.append(s
|
|
283
|
+
.append(mqeSets.getLabelText(s))
|
|
255
284
|
.css("margin-right", "5px"),
|
|
256
285
|
f = u();
|
|
257
286
|
c.append(d).append(" ").append(p).append(f),
|
|
@@ -486,34 +515,38 @@ function MenuEditor(e, t) {
|
|
|
486
515
|
: h(d),
|
|
487
516
|
(d.oElOld = d.oEl),
|
|
488
517
|
(s.el[0].style.visibility = "hidden"),
|
|
489
|
-
(d.oEl = oEl =
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
(
|
|
496
|
-
null ==
|
|
497
|
-
(o = document.elementFromPoint(
|
|
498
|
-
0,
|
|
499
|
-
n + e(window).height() - 1
|
|
500
|
-
)) || "HTML" == o.tagName.toUpperCase()),
|
|
501
|
-
(n = d.doc.scrollLeft()) > 0 &&
|
|
518
|
+
(d.oEl = oEl =
|
|
519
|
+
(function (t, s) {
|
|
520
|
+
if (!document.elementFromPoint) return null;
|
|
521
|
+
var l = d.isRelEFP;
|
|
522
|
+
if (null === l) {
|
|
523
|
+
var n, o;
|
|
524
|
+
(n = d.doc.scrollTop()) > 0 &&
|
|
502
525
|
(l =
|
|
503
526
|
null ==
|
|
504
527
|
(o = document.elementFromPoint(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
)) || "HTML" == o.tagName.toUpperCase())
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
528
|
+
0,
|
|
529
|
+
n + e(window).height() - 1
|
|
530
|
+
)) || "HTML" == o.tagName.toUpperCase()),
|
|
531
|
+
(n = d.doc.scrollLeft()) > 0 &&
|
|
532
|
+
(l =
|
|
533
|
+
null ==
|
|
534
|
+
(o = document.elementFromPoint(
|
|
535
|
+
n + e(window).width() - 1,
|
|
536
|
+
0
|
|
537
|
+
)) || "HTML" == o.tagName.toUpperCase());
|
|
538
|
+
}
|
|
539
|
+
l && ((t -= d.doc.scrollLeft()), (s -= d.doc.scrollTop()));
|
|
540
|
+
var i = e(document.elementFromPoint(t, s));
|
|
541
|
+
if (!d.rootEl.el.find(i).length) return null;
|
|
542
|
+
if (
|
|
543
|
+
i.is("#sortableListsPlaceholder") ||
|
|
544
|
+
i.is("#sortableListsHint")
|
|
545
|
+
)
|
|
546
|
+
return null;
|
|
547
|
+
if (!i.is("li")) return (i = i.closest("li"))[0] ? i : null;
|
|
548
|
+
if (i.is("li")) return i;
|
|
549
|
+
})(t.pageX, t.pageY)),
|
|
517
550
|
(s.el[0].style.visibility = "visible"),
|
|
518
551
|
(function (e, t) {
|
|
519
552
|
var s = t.oEl;
|
package/public/saltcorn.css
CHANGED
package/public/saltcorn.js
CHANGED
|
@@ -8,6 +8,20 @@ jQuery.fn.swapWith = function (to) {
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
//avoids hiding in overflow:hidden
|
|
12
|
+
function init_bs5_dropdowns() {
|
|
13
|
+
$("body").on(
|
|
14
|
+
"show.bs.dropdown",
|
|
15
|
+
"table [data-bs-toggle=dropdown]",
|
|
16
|
+
function () {
|
|
17
|
+
let target;
|
|
18
|
+
if (!$("#page-inner-content").length) target = $("body");
|
|
19
|
+
else target = $("#page-inner-content");
|
|
20
|
+
let dropdown = bootstrap.Dropdown.getInstance(this);
|
|
21
|
+
$(dropdown._menu).insertAfter(target);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
}
|
|
11
25
|
function sortby(k, desc) {
|
|
12
26
|
set_state_fields({ _sortby: k, _sortdesc: desc ? "on" : { unset: true } });
|
|
13
27
|
}
|
|
@@ -98,6 +112,8 @@ function get_form_record(e, select_labels) {
|
|
|
98
112
|
.each(function () {
|
|
99
113
|
if (select_labels && $(this).prop("tagName").toLowerCase() === "select")
|
|
100
114
|
rec[$(this).attr("name")] = $(this).find("option:selected").text();
|
|
115
|
+
else if ($(this).prop("type") === "checkbox")
|
|
116
|
+
rec[$(this).attr("name")] = $(this).prop("checked");
|
|
101
117
|
else rec[$(this).attr("name")] = $(this).val();
|
|
102
118
|
});
|
|
103
119
|
return rec;
|
|
@@ -293,6 +309,7 @@ function initialize_page() {
|
|
|
293
309
|
el.text(date.toLocaleDateString(locale, options));
|
|
294
310
|
});
|
|
295
311
|
$('a[data-bs-toggle="tab"].deeplink').historyTabs();
|
|
312
|
+
init_bs5_dropdowns();
|
|
296
313
|
}
|
|
297
314
|
|
|
298
315
|
$(initialize_page);
|
|
@@ -312,17 +329,36 @@ function enable_codemirror(f) {
|
|
|
312
329
|
}
|
|
313
330
|
|
|
314
331
|
//https://stackoverflow.com/a/6021027
|
|
315
|
-
function updateQueryStringParameter(
|
|
332
|
+
function updateQueryStringParameter(uri1, key, value) {
|
|
333
|
+
let hash = "";
|
|
334
|
+
let uri = uri1;
|
|
335
|
+
if (uri && uri.includes("#")) {
|
|
336
|
+
let uris = uri1.split("#");
|
|
337
|
+
hash = "#" + uris[1];
|
|
338
|
+
uri = uris[0];
|
|
339
|
+
}
|
|
340
|
+
|
|
316
341
|
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
|
|
317
342
|
var separator = uri.indexOf("?") !== -1 ? "&" : "?";
|
|
318
343
|
if (uri.match(re)) {
|
|
319
|
-
return
|
|
344
|
+
return (
|
|
345
|
+
uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2") +
|
|
346
|
+
hash
|
|
347
|
+
);
|
|
320
348
|
} else {
|
|
321
|
-
return uri + separator + key + "=" + encodeURIComponent(value);
|
|
349
|
+
return uri + separator + key + "=" + encodeURIComponent(value) + hash;
|
|
322
350
|
}
|
|
323
351
|
}
|
|
324
352
|
|
|
325
|
-
function removeQueryStringParameter(
|
|
353
|
+
function removeQueryStringParameter(uri1, key) {
|
|
354
|
+
let hash = "";
|
|
355
|
+
let uri = uri1;
|
|
356
|
+
if (uri && uri.includes("#")) {
|
|
357
|
+
let uris = uri1.split("#");
|
|
358
|
+
hash = "#" + uris[1];
|
|
359
|
+
uri = uris[0];
|
|
360
|
+
}
|
|
361
|
+
|
|
326
362
|
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
|
|
327
363
|
var separator = uri.indexOf("?") !== -1 ? "&" : "?";
|
|
328
364
|
if (uri.match(re)) {
|
|
@@ -330,7 +366,7 @@ function removeQueryStringParameter(uri, key) {
|
|
|
330
366
|
}
|
|
331
367
|
if (uri[uri.length - 1] === "?" || uri[uri.length - 1] === "&")
|
|
332
368
|
uri = uri.substring(0, uri.length - 1);
|
|
333
|
-
return uri;
|
|
369
|
+
return uri + hash;
|
|
334
370
|
}
|
|
335
371
|
|
|
336
372
|
function select_id(id) {
|
|
@@ -511,7 +547,8 @@ function globalErrorCatcher(message, source, lineno, colno, error) {
|
|
|
511
547
|
}
|
|
512
548
|
|
|
513
549
|
function press_store_button(clicked) {
|
|
514
|
-
$(clicked).
|
|
550
|
+
const width = $(clicked).width();
|
|
551
|
+
$(clicked).html('<i class="fas fa-spinner fa-spin"></i>').width(width);
|
|
515
552
|
}
|
|
516
553
|
|
|
517
554
|
function ajax_modal(url, opts = {}) {
|
|
@@ -789,6 +826,26 @@ async function fill_formula_btn_click(btn, k) {
|
|
|
789
826
|
if (k) k();
|
|
790
827
|
}
|
|
791
828
|
|
|
829
|
+
const columnSummary = (col) => {
|
|
830
|
+
if (!col) return "Unknown";
|
|
831
|
+
switch (col.type) {
|
|
832
|
+
case "Field":
|
|
833
|
+
return `Field ${col.field_name} ${col.fieldview}`;
|
|
834
|
+
case "Link":
|
|
835
|
+
return `Link ${col.link_text}`;
|
|
836
|
+
case "JoinField":
|
|
837
|
+
return `Join ${col.join_field}`;
|
|
838
|
+
case "ViewLink":
|
|
839
|
+
return `View ${col.view_label || col.view.split(":")[1] || ""}`;
|
|
840
|
+
case "Action":
|
|
841
|
+
return `Action ${col.action_label || col.action_name}`;
|
|
842
|
+
case "Aggregation":
|
|
843
|
+
return `${col.stat} ${col.agg_field} ${col.agg_relation}`;
|
|
844
|
+
default:
|
|
845
|
+
return "Unknown";
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
792
849
|
/*
|
|
793
850
|
https://github.com/jeffdavidgreen/bootstrap-html5-history-tabs/blob/master/bootstrap-history-tabs.js
|
|
794
851
|
Copyright (c) 2015 Jeff Green
|
package/routes/admin.js
CHANGED
|
@@ -36,6 +36,9 @@ const {
|
|
|
36
36
|
button,
|
|
37
37
|
span,
|
|
38
38
|
p,
|
|
39
|
+
code,
|
|
40
|
+
h5,
|
|
41
|
+
pre,
|
|
39
42
|
} = require("@saltcorn/markup/tags");
|
|
40
43
|
const db = require("@saltcorn/data/db");
|
|
41
44
|
const {
|
|
@@ -50,6 +53,9 @@ const {
|
|
|
50
53
|
create_backup,
|
|
51
54
|
restore,
|
|
52
55
|
} = require("@saltcorn/admin-models/models/backup");
|
|
56
|
+
const {
|
|
57
|
+
runConfigurationCheck,
|
|
58
|
+
} = require("@saltcorn/admin-models/models/config-check");
|
|
53
59
|
const fs = require("fs");
|
|
54
60
|
const load_plugins = require("../load_plugins");
|
|
55
61
|
const {
|
|
@@ -365,6 +371,18 @@ router.get(
|
|
|
365
371
|
),
|
|
366
372
|
hr(),
|
|
367
373
|
|
|
374
|
+
a(
|
|
375
|
+
{
|
|
376
|
+
href: "/admin/configuration-check",
|
|
377
|
+
class: "btn btn-info",
|
|
378
|
+
onClick: "press_store_button(this)",
|
|
379
|
+
},
|
|
380
|
+
i({ class: "fas fa-stethoscope" }),
|
|
381
|
+
" ",
|
|
382
|
+
req.__("Configuration check")
|
|
383
|
+
),
|
|
384
|
+
hr(),
|
|
385
|
+
|
|
368
386
|
a(
|
|
369
387
|
{ href: "/admin/clear-all", class: "btn btn-danger" },
|
|
370
388
|
i({ class: "fas fa-trash-alt" }),
|
|
@@ -502,6 +520,7 @@ router.post(
|
|
|
502
520
|
`Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
|
|
503
521
|
);
|
|
504
522
|
setTimeout(() => {
|
|
523
|
+
if (process.send) process.send("RestartServer");
|
|
505
524
|
process.exit(0);
|
|
506
525
|
}, 100);
|
|
507
526
|
});
|
|
@@ -740,6 +759,49 @@ router.get(
|
|
|
740
759
|
})
|
|
741
760
|
);
|
|
742
761
|
|
|
762
|
+
router.get(
|
|
763
|
+
"/configuration-check",
|
|
764
|
+
isAdmin,
|
|
765
|
+
error_catcher(async (req, res) => {
|
|
766
|
+
const { passes, errors, pass } = await runConfigurationCheck(req);
|
|
767
|
+
const mkError = (err) =>
|
|
768
|
+
div(
|
|
769
|
+
{ class: "alert alert-danger", role: "alert" },
|
|
770
|
+
pre({ class: "mb-0" }, code(err))
|
|
771
|
+
);
|
|
772
|
+
res.sendWrap(req.__(`Admin`), {
|
|
773
|
+
above: [
|
|
774
|
+
{
|
|
775
|
+
type: "breadcrumbs",
|
|
776
|
+
crumbs: [
|
|
777
|
+
{ text: req.__("Settings") },
|
|
778
|
+
{ text: req.__("Admin"), href: "/admin" },
|
|
779
|
+
{ text: req.__("Configuration check") },
|
|
780
|
+
],
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
type: "card",
|
|
784
|
+
title: req.__("Configuration errors"),
|
|
785
|
+
contents: div(
|
|
786
|
+
pass
|
|
787
|
+
? div(
|
|
788
|
+
{ class: "alert alert-success", role: "alert" },
|
|
789
|
+
i({ class: "fas fa-check-circle fa-lg me-2" }),
|
|
790
|
+
h5({ class: "d-inline" }, "No errors detected")
|
|
791
|
+
)
|
|
792
|
+
: errors.map(mkError)
|
|
793
|
+
),
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
type: "card",
|
|
797
|
+
title: req.__("Configuration checks passed"),
|
|
798
|
+
contents: div(pre(code(passes.join("\n")))),
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
});
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
|
|
743
805
|
/**
|
|
744
806
|
* @name post/clear-all
|
|
745
807
|
* @function
|
package/routes/fields.js
CHANGED
|
@@ -181,6 +181,7 @@ const fieldFlow = (req) =>
|
|
|
181
181
|
var attributes = context.attributes || {};
|
|
182
182
|
attributes.default = context.default;
|
|
183
183
|
attributes.summary_field = context.summary_field;
|
|
184
|
+
attributes.on_delete_cascade = context.on_delete_cascade;
|
|
184
185
|
const {
|
|
185
186
|
table_id,
|
|
186
187
|
name,
|
|
@@ -377,6 +378,14 @@ const fieldFlow = (req) =>
|
|
|
377
378
|
input_type: "select",
|
|
378
379
|
options: keyfields,
|
|
379
380
|
}),
|
|
381
|
+
new Field({
|
|
382
|
+
name: "on_delete_cascade",
|
|
383
|
+
label: req.__("On delete cascade"),
|
|
384
|
+
type: "Bool",
|
|
385
|
+
sublabel: req.__(
|
|
386
|
+
"If the parent row is deleted, automatically delete the child rows."
|
|
387
|
+
),
|
|
388
|
+
}),
|
|
380
389
|
],
|
|
381
390
|
});
|
|
382
391
|
},
|
|
@@ -620,10 +629,14 @@ router.post(
|
|
|
620
629
|
*/
|
|
621
630
|
router.post(
|
|
622
631
|
"/show-calculated/:tableName/:fieldName/:fieldview",
|
|
623
|
-
isAdmin,
|
|
624
632
|
error_catcher(async (req, res) => {
|
|
625
633
|
const { tableName, fieldName, fieldview } = req.params;
|
|
626
634
|
const table = await Table.findOne({ name: tableName });
|
|
635
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
636
|
+
if (role > table.min_role_read) {
|
|
637
|
+
res.status(401).send("");
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
627
640
|
const fields = await table.getFields();
|
|
628
641
|
const row = { ...req.body };
|
|
629
642
|
readState(row, fields);
|
|
@@ -634,6 +647,10 @@ router.post(
|
|
|
634
647
|
if (kpath.length === 2 && row[kpath[0]]) {
|
|
635
648
|
const field = fields.find((f) => f.name === kpath[0]);
|
|
636
649
|
const reftable = await Table.findOne({ name: field.reftable_name });
|
|
650
|
+
if (role > reftable.min_role_read) {
|
|
651
|
+
res.status(401).send("");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
637
654
|
const targetField = (await reftable.getFields()).find(
|
|
638
655
|
(f) => f.name === kpath[1]
|
|
639
656
|
);
|
|
@@ -662,6 +679,10 @@ router.post(
|
|
|
662
679
|
if (field.is_fkey) {
|
|
663
680
|
const reftable = await Table.findOne({ name: field.reftable_name });
|
|
664
681
|
if (!oldRow[ref]) break;
|
|
682
|
+
if (role > reftable.min_role_read) {
|
|
683
|
+
res.status(401).send("");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
665
686
|
const q = { [reftable.pk_name]: oldRow[ref] };
|
|
666
687
|
oldRow = await reftable.getRow(q);
|
|
667
688
|
oldTable = reftable;
|
package/routes/menu.js
CHANGED
|
@@ -10,7 +10,6 @@ const Field = require("@saltcorn/data/models/field");
|
|
|
10
10
|
const Form = require("@saltcorn/data/models/form");
|
|
11
11
|
const { isAdmin, setTenant, error_catcher } = require("./utils.js");
|
|
12
12
|
const { getState } = require("@saltcorn/data/db/state");
|
|
13
|
-
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
14
13
|
const File = require("@saltcorn/data/models/file");
|
|
15
14
|
const User = require("@saltcorn/data/models/user");
|
|
16
15
|
const View = require("@saltcorn/data/models/view");
|
package/routes/plugins.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
post_btn,
|
|
15
15
|
post_delete_btn,
|
|
16
16
|
} = require("@saltcorn/markup");
|
|
17
|
-
const { getState } = require("@saltcorn/data/db/state");
|
|
17
|
+
const { getState, restart_tenant } = require("@saltcorn/data/db/state");
|
|
18
18
|
const Form = require("@saltcorn/data/models/form");
|
|
19
19
|
const Field = require("@saltcorn/data/models/field");
|
|
20
20
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
@@ -52,6 +52,7 @@ const path = require("path");
|
|
|
52
52
|
const { get_latest_npm_version } = require("@saltcorn/data/models/config");
|
|
53
53
|
const { flash_restart } = require("../markup/admin.js");
|
|
54
54
|
const { sleep } = require("@saltcorn/data/utils");
|
|
55
|
+
const { loadAllPlugins } = require("../load_plugins");
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* @type {object}
|
|
@@ -148,6 +149,7 @@ const local_has_theme = (name) => {
|
|
|
148
149
|
*/
|
|
149
150
|
const get_store_items = async () => {
|
|
150
151
|
const installed_plugins = await Plugin.find({});
|
|
152
|
+
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
151
153
|
|
|
152
154
|
const instore = await Plugin.store_plugins_available();
|
|
153
155
|
const packs_available = await fetch_available_packs();
|
|
@@ -155,15 +157,18 @@ const get_store_items = async () => {
|
|
|
155
157
|
const schema = db.getTenantSchema();
|
|
156
158
|
const installed_plugin_names = installed_plugins.map((p) => p.name);
|
|
157
159
|
const store_plugin_names = instore.map((p) => p.name);
|
|
158
|
-
const plugins_item = instore
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
160
|
+
const plugins_item = instore
|
|
161
|
+
.map((plugin) => ({
|
|
162
|
+
name: plugin.name,
|
|
163
|
+
installed: installed_plugin_names.includes(plugin.name),
|
|
164
|
+
plugin: true,
|
|
165
|
+
description: plugin.description,
|
|
166
|
+
documentation_link: plugin.documentation_link,
|
|
167
|
+
has_theme: plugin.has_theme,
|
|
168
|
+
has_auth: plugin.has_auth,
|
|
169
|
+
unsafe: plugin.unsafe,
|
|
170
|
+
}))
|
|
171
|
+
.filter((p) => !p.unsafe || isRoot);
|
|
167
172
|
const local_logins = installed_plugins
|
|
168
173
|
.filter((p) => !store_plugin_names.includes(p.name) && p.name !== "base")
|
|
169
174
|
.map((plugin) => ({
|
|
@@ -647,9 +652,10 @@ router.get(
|
|
|
647
652
|
error_catcher(async (req, res) => {
|
|
648
653
|
const { plugin } = req.params;
|
|
649
654
|
const filepath = req.params[0];
|
|
650
|
-
const location =
|
|
651
|
-
|
|
652
|
-
|
|
655
|
+
const location =
|
|
656
|
+
getState().plugin_locations[
|
|
657
|
+
plugin.includes("@") ? plugin.split("@")[0] : plugin
|
|
658
|
+
];
|
|
653
659
|
if (location) {
|
|
654
660
|
const safeFile = path
|
|
655
661
|
.normalize(filepath)
|
|
@@ -841,7 +847,9 @@ router.get(
|
|
|
841
847
|
await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
|
|
842
848
|
}
|
|
843
849
|
req.flash("success", req.__(`Plugins up-to-date`));
|
|
844
|
-
|
|
850
|
+
await restart_tenant(loadAllPlugins);
|
|
851
|
+
process.send &&
|
|
852
|
+
process.send({ restart_tenant: true, tenant: db.getTenantSchema() });
|
|
845
853
|
res.redirect(`/plugins`);
|
|
846
854
|
})
|
|
847
855
|
);
|
|
@@ -957,6 +965,15 @@ router.post(
|
|
|
957
965
|
res.redirect(`/plugins`);
|
|
958
966
|
return;
|
|
959
967
|
}
|
|
968
|
+
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
969
|
+
if (!isRoot && plugin.unsafe) {
|
|
970
|
+
req.flash(
|
|
971
|
+
"error",
|
|
972
|
+
req.__("Cannot install unsafe plugins on subdomain tenants")
|
|
973
|
+
);
|
|
974
|
+
res.redirect(`/plugins`);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
960
977
|
delete plugin.id;
|
|
961
978
|
await load_plugins.loadAndSaveNewPlugin(plugin);
|
|
962
979
|
const plugin_module = getState().plugins[name];
|
package/routes/viewedit.js
CHANGED
|
@@ -38,6 +38,7 @@ const View = require("@saltcorn/data/models/view");
|
|
|
38
38
|
const Workflow = require("@saltcorn/data/models/workflow");
|
|
39
39
|
const User = require("@saltcorn/data/models/user");
|
|
40
40
|
const Page = require("@saltcorn/data/models/page");
|
|
41
|
+
const db = require("@saltcorn/data/db");
|
|
41
42
|
|
|
42
43
|
const { add_to_menu } = require("@saltcorn/admin-models/models/pack");
|
|
43
44
|
const { editRoleForm } = require("../markup/forms.js");
|
|
@@ -534,7 +535,23 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
|
|
|
534
535
|
if (wfres.flash) req.flash(wfres.flash[0], wfres.flash[1]);
|
|
535
536
|
if (wfres.renderForm)
|
|
536
537
|
res.sendWrap(
|
|
537
|
-
|
|
538
|
+
{
|
|
539
|
+
title: req.__(`View configuration`),
|
|
540
|
+
headers: [
|
|
541
|
+
{
|
|
542
|
+
script: `/static_assets/${db.connectObj.version_tag}/jquery-menu-editor.min.js`,
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
script: `/static_assets/${db.connectObj.version_tag}/iconset-fontawesome5-3-1.min.js`,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
script: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.js`,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
css: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.min.css`,
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
},
|
|
538
555
|
wrap(renderForm(wfres.renderForm, req.csrfToken()))
|
|
539
556
|
);
|
|
540
557
|
else if (wfres.renderBuilder) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const load_script = (fnm) => {
|
|
8
|
+
const srcFile = fs.readFileSync(path.join(__dirname, "..", "public", fnm), {
|
|
9
|
+
encoding: "utf-8",
|
|
10
|
+
});
|
|
11
|
+
const scriptEl = document.createElement("script");
|
|
12
|
+
scriptEl.textContent = srcFile;
|
|
13
|
+
document.body.appendChild(scriptEl);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
load_script("jquery-3.6.0.min.js");
|
|
17
|
+
load_script("saltcorn.js");
|
|
18
|
+
|
|
19
|
+
test("updateQueryStringParameter", () => {
|
|
20
|
+
const element = document.createElement("div");
|
|
21
|
+
expect(element).not.toBeNull();
|
|
22
|
+
expect(updateQueryStringParameter("/foo", "age", 43)).toBe("/foo?age=43");
|
|
23
|
+
expect(updateQueryStringParameter("/foo?age=44", "age", 43)).toBe(
|
|
24
|
+
"/foo?age=43"
|
|
25
|
+
);
|
|
26
|
+
expect(updateQueryStringParameter("/foo?name=Bar", "age", 43)).toBe(
|
|
27
|
+
"/foo?name=Bar&age=43"
|
|
28
|
+
);
|
|
29
|
+
expect(removeQueryStringParameter("/foo?age=44", "age")).toBe("/foo");
|
|
30
|
+
expect(removeQueryStringParameter("/foo?name=Bar", "age")).toBe(
|
|
31
|
+
"/foo?name=Bar"
|
|
32
|
+
);
|
|
33
|
+
expect(removeQueryStringParameter("/foo?name=Bar&age=45", "age")).toBe(
|
|
34
|
+
"/foo?name=Bar"
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("updateQueryStringParameter hash", () => {
|
|
39
|
+
expect(updateQueryStringParameter("/foo#baz", "age", 43)).toBe(
|
|
40
|
+
"/foo?age=43#baz"
|
|
41
|
+
);
|
|
42
|
+
expect(updateQueryStringParameter("/foo?age=44#Baz", "age", 43)).toBe(
|
|
43
|
+
"/foo?age=43#Baz"
|
|
44
|
+
);
|
|
45
|
+
expect(updateQueryStringParameter("/foo?name=Bar#Zap", "age", 43)).toBe(
|
|
46
|
+
"/foo?name=Bar&age=43#Zap"
|
|
47
|
+
);
|
|
48
|
+
expect(removeQueryStringParameter("/foo?age=44#Baz", "age")).toBe("/foo#Baz");
|
|
49
|
+
expect(removeQueryStringParameter("/foo?name=Bar#Baz", "age")).toBe(
|
|
50
|
+
"/foo?name=Bar#Baz"
|
|
51
|
+
);
|
|
52
|
+
expect(removeQueryStringParameter("/foo?name=Bar&age=45#Baz", "age")).toBe(
|
|
53
|
+
"/foo?name=Bar#Baz"
|
|
54
|
+
);
|
|
55
|
+
});
|