@saltcorn/server 1.1.1-beta.7 → 1.1.1-rc.1
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/CHANGELOG.md +27 -1
- package/auth/admin.js +1 -0
- package/locales/en.json +3 -1
- package/markup/admin.js +15 -10
- package/package.json +9 -9
- package/public/saltcorn-common.js +85 -1
- package/public/saltcorn.css +39 -0
- package/public/saltcorn.js +17 -4
- package/routes/actions.js +1 -0
- package/routes/admin.js +29 -25
- package/routes/api.js +68 -5
- package/routes/common_lists.js +67 -16
- package/routes/fields.js +113 -2
- package/routes/list.js +9 -8
- package/routes/menu.js +3 -3
- package/routes/tag_entries.js +43 -1
- package/routes/tags.js +1 -0
- package/routes/utils.js +15 -0
- package/serve.js +17 -13
- package/tests/api.test.js +13 -0
- package/tests/clientjs.test.js +20 -0
- package/wrapper.js +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## 1.1.1 - In beta
|
|
4
4
|
|
|
5
|
+
* select_by_view fieldview for Key fields: the user selects the value of a
|
|
6
|
+
Key field based on an clicking in a row of rendered views (typically a Show view) of the joined table. Works for both Edit and Filter views.
|
|
7
|
+
|
|
8
|
+
* Click to edit (Show and List view patterns) is now implemented by rendering
|
|
9
|
+
the first available edit fieldview. This should be more robust and work with
|
|
10
|
+
more data types.
|
|
11
|
+
|
|
12
|
+
* Data in the admin's data edit grid is now loaded by page. This makes it
|
|
13
|
+
possible to work with much larger datasets.
|
|
14
|
+
|
|
5
15
|
* You can now permit to non-admin (role ID > 1) users to edit or inspect tables, or
|
|
6
16
|
edit views, pages or triggers. In the permissions tab of the Users and security
|
|
7
17
|
settings, minimum roles can be set for these capabilities. The appropriate
|
|
@@ -35,12 +45,23 @@
|
|
|
35
45
|
is changed.
|
|
36
46
|
|
|
37
47
|
* Mobile builder:
|
|
38
|
-
- PJAX view loading.
|
|
48
|
+
- PJAX view loading: Use pjax for all functions like on the web version.
|
|
49
|
+
- Share content to your app on mobile and PWA.
|
|
50
|
+
- Ensure at least one ReceiveMobileShareData trigger exists when the app is built or the PWA is installed.
|
|
51
|
+
- Shared content is accessible via the row variable.
|
|
52
|
+
- Android: No additional configuration is needed.
|
|
53
|
+
- PWA: Ensure a trusted HTTPS connection is used.
|
|
54
|
+
- iOS:
|
|
55
|
+
- A second provisioning profile is required, with the bundle ID of the main app followed by share-ext (e.g., com.saltcorn.share-ext).
|
|
56
|
+
- The iOS project needs a Share Extension target. To set this up, open Xcode and add a Share Extension target from a template (more documentation is is about to come).
|
|
57
|
+
- The build will stop when the Xcode integration is required, and a "Finish the Build" shows up.
|
|
39
58
|
|
|
40
59
|
### Fixes
|
|
41
60
|
|
|
61
|
+
* Increase plugin install reliability
|
|
42
62
|
* fix workflows on SQLite
|
|
43
63
|
* fix query string build on check_state_field (#2948). Author: St0rml
|
|
64
|
+
* multiple fixes for the Capacitor port
|
|
44
65
|
|
|
45
66
|
### Translations
|
|
46
67
|
|
|
@@ -71,6 +92,11 @@
|
|
|
71
92
|
|
|
72
93
|
* Webhook action has more options: method, set reponse value, headers.
|
|
73
94
|
|
|
95
|
+
* Mobile builder:
|
|
96
|
+
- Ported from Cordova to Capacitor: Cordova's core functionalities and plugins are well-maintained, but for some time now, the trend for mobile application development goes new directions. Capacitor aims to be a drop-in replacement with a more modern approach and an active Community. Existing Cordova plugins do still work, and plugins from the Capacitor ecosystem are available as well. This should make the mobile app development more future-proof.
|
|
97
|
+
- Screen orientation change handling: A Saltcorn plugin can register a listener for screen orientation changes (Landscape / Portrait modes). For an example, take a look at the [metronic-theme](https://github.com/saltcorn/metronic-theme/blob/35b69ba7b4e94e2bcfe2f1c61508bc579c1d914f/index.js#L844). It registers a listener to adjust the mobile bottom navigation bar when the phone rotates.
|
|
98
|
+
- PJAX view loading: Changed the full reload to pjax for sortby and gopage (paging). The remainig set_state calls are still full reloads.
|
|
99
|
+
|
|
74
100
|
### Security
|
|
75
101
|
|
|
76
102
|
- SameSite cookie settings
|
package/auth/admin.js
CHANGED
package/locales/en.json
CHANGED
|
@@ -1543,5 +1543,7 @@
|
|
|
1543
1543
|
"Minimum role to inspect (see, without editing) tables": "Minimum role to inspect (see, without editing) tables",
|
|
1544
1544
|
"Home pages": "Home pages",
|
|
1545
1545
|
"The home page is the page that is served when the user visits the home location (/). This can be set for each user role.": "The home page is the page that is served when the user visits the home location (/). This can be set for each user role.",
|
|
1546
|
-
"Trigger %s deleted": "Trigger %s deleted"
|
|
1546
|
+
"Trigger %s deleted": "Trigger %s deleted",
|
|
1547
|
+
"Edit menu": "Edit menu",
|
|
1548
|
+
"Minimum role to edit menu": "Minimum role to edit menu"
|
|
1547
1549
|
}
|
package/markup/admin.js
CHANGED
|
@@ -224,24 +224,29 @@ const send_infoarch_page = (args) => {
|
|
|
224
224
|
const tenant_list =
|
|
225
225
|
db.is_it_multi_tenant() &&
|
|
226
226
|
db.getTenantSchema() === db.connectObj.default_schema;
|
|
227
|
+
const isUserAdmin = args.req?.user.role_id === 1;
|
|
227
228
|
return send_settings_page({
|
|
228
229
|
main_section: "Site structure",
|
|
229
230
|
main_section_href: "/site-structure",
|
|
230
231
|
sub_sections: [
|
|
231
232
|
{ text: "Menu", href: "/menu" },
|
|
232
|
-
|
|
233
|
-
{ text: "Library", href: "/library/list" },
|
|
234
|
-
{ text: "Languages", href: "/site-structure/localizer" },
|
|
235
|
-
...(tenant_list
|
|
233
|
+
...(isUserAdmin
|
|
236
234
|
? [
|
|
237
|
-
{ text: "
|
|
238
|
-
{ text: "
|
|
235
|
+
{ text: "Search", href: "/search/config" },
|
|
236
|
+
{ text: "Library", href: "/library/list" },
|
|
237
|
+
{ text: "Languages", href: "/site-structure/localizer" },
|
|
238
|
+
...(tenant_list
|
|
239
|
+
? [
|
|
240
|
+
{ text: "Tenants", href: "/tenant/list" },
|
|
241
|
+
{ text: "Multitenancy", href: "/tenant/settings" },
|
|
242
|
+
]
|
|
243
|
+
: []),
|
|
244
|
+
{ text: "Pagegroups", href: "/page_group/settings" },
|
|
245
|
+
{ text: "Tags", href: "/tag" },
|
|
246
|
+
{ text: "Diagram", href: "/diagram" },
|
|
247
|
+
{ text: "Registry editor", href: "/registry-editor" },
|
|
239
248
|
]
|
|
240
249
|
: []),
|
|
241
|
-
{ text: "Pagegroups", href: "/page_group/settings" },
|
|
242
|
-
{ text: "Tags", href: "/tag" },
|
|
243
|
-
{ text: "Diagram", href: "/diagram" },
|
|
244
|
-
{ text: "Registry editor", href: "/registry-editor" },
|
|
245
250
|
],
|
|
246
251
|
...args,
|
|
247
252
|
});
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "1.1.1-
|
|
3
|
+
"version": "1.1.1-rc.1",
|
|
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
9
|
"@aws-sdk/client-s3": "^3.451.0",
|
|
10
|
-
"@saltcorn/base-plugin": "1.1.1-
|
|
11
|
-
"@saltcorn/builder": "1.1.1-
|
|
12
|
-
"@saltcorn/data": "1.1.1-
|
|
13
|
-
"@saltcorn/admin-models": "1.1.1-
|
|
14
|
-
"@saltcorn/filemanager": "1.1.1-
|
|
15
|
-
"@saltcorn/markup": "1.1.1-
|
|
16
|
-
"@saltcorn/plugins-loader": "1.1.1-
|
|
17
|
-
"@saltcorn/sbadmin2": "1.1.1-
|
|
10
|
+
"@saltcorn/base-plugin": "1.1.1-rc.1",
|
|
11
|
+
"@saltcorn/builder": "1.1.1-rc.1",
|
|
12
|
+
"@saltcorn/data": "1.1.1-rc.1",
|
|
13
|
+
"@saltcorn/admin-models": "1.1.1-rc.1",
|
|
14
|
+
"@saltcorn/filemanager": "1.1.1-rc.1",
|
|
15
|
+
"@saltcorn/markup": "1.1.1-rc.1",
|
|
16
|
+
"@saltcorn/plugins-loader": "1.1.1-rc.1",
|
|
17
|
+
"@saltcorn/sbadmin2": "1.1.1-rc.1",
|
|
18
18
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
19
19
|
"@socket.io/sticky": "^1.0.1",
|
|
20
20
|
"adm-zip": "0.5.10",
|
|
@@ -965,6 +965,38 @@ function initialize_page() {
|
|
|
965
965
|
var current =
|
|
966
966
|
$(this).attr("data-inline-edit-current") ||
|
|
967
967
|
$(this).children("span.current").html();
|
|
968
|
+
const resetHtml = this.outerHTML;
|
|
969
|
+
|
|
970
|
+
let fielddata = $(this).attr("data-inline-edit-fielddata");
|
|
971
|
+
if (fielddata) {
|
|
972
|
+
//fetch edit
|
|
973
|
+
$.ajax(`/field/edit-get-fieldview`, {
|
|
974
|
+
type: "POST",
|
|
975
|
+
headers: {
|
|
976
|
+
"CSRF-Token": _sc_globalCsrf,
|
|
977
|
+
},
|
|
978
|
+
contentType: "application/json",
|
|
979
|
+
data: decodeURIComponent(fielddata),
|
|
980
|
+
}).then((resp) => {
|
|
981
|
+
const opts = encodeURIComponent(
|
|
982
|
+
JSON.stringify({
|
|
983
|
+
resetHtml,
|
|
984
|
+
})
|
|
985
|
+
);
|
|
986
|
+
$(this).replaceWith(
|
|
987
|
+
`<form method="post" action="/field/save-click-edit" onsubmit="inline_ajax_submit_with_fielddata(event, '${opts}')"
|
|
988
|
+
<input type="hidden" name="_csrf" value="${_sc_globalCsrf}">
|
|
989
|
+
<input type="hidden" name="_fielddata" value="${fielddata}">
|
|
990
|
+
<div class="input-group">
|
|
991
|
+
${resp}
|
|
992
|
+
<button type="submit" class="btn btn-sm btn-primary">OK</button>
|
|
993
|
+
<button onclick="cancel_inline_edit(event, '${opts}')" type="button" class="btn btn-sm btn-danger"><i class="fas fa-times"></i></button>
|
|
994
|
+
</div>
|
|
995
|
+
</form>`
|
|
996
|
+
);
|
|
997
|
+
});
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
968
1000
|
var key = $(this).attr("data-inline-edit-field") || "value";
|
|
969
1001
|
var ajax = !!$(this).attr("data-inline-edit-ajax");
|
|
970
1002
|
var type = $(this).attr("data-inline-edit-type");
|
|
@@ -984,7 +1016,6 @@ function initialize_page() {
|
|
|
984
1016
|
current = current === "true";
|
|
985
1017
|
}
|
|
986
1018
|
var is_key = type?.startsWith("Key:");
|
|
987
|
-
const resetHtml = this.outerHTML;
|
|
988
1019
|
const opts = encodeURIComponent(
|
|
989
1020
|
JSON.stringify({
|
|
990
1021
|
url,
|
|
@@ -1267,6 +1298,37 @@ function inline_submit_success(e, form, opts) {
|
|
|
1267
1298
|
} else location.reload();
|
|
1268
1299
|
}
|
|
1269
1300
|
|
|
1301
|
+
function inline_ajax_submit_with_fielddata(e, opts1) {
|
|
1302
|
+
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
1303
|
+
e.preventDefault();
|
|
1304
|
+
|
|
1305
|
+
var form = $(e.target).closest("form");
|
|
1306
|
+
var form_data = form.serialize();
|
|
1307
|
+
var url = form.attr("action");
|
|
1308
|
+
if (opts.type === "Bool" && !form_data.includes(`${opts.key}=on`)) {
|
|
1309
|
+
form_data += `&${opts.key}=off`;
|
|
1310
|
+
}
|
|
1311
|
+
$.ajax(url, {
|
|
1312
|
+
type: "POST",
|
|
1313
|
+
headers: {
|
|
1314
|
+
"CSRF-Token": _sc_globalCsrf,
|
|
1315
|
+
},
|
|
1316
|
+
data: form_data,
|
|
1317
|
+
success: function (res) {
|
|
1318
|
+
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
1319
|
+
var form = $(e.target).closest("form");
|
|
1320
|
+
form.replaceWith(res);
|
|
1321
|
+
initialize_page();
|
|
1322
|
+
},
|
|
1323
|
+
error: function (e) {
|
|
1324
|
+
if (!checkNetworkError(e))
|
|
1325
|
+
ajax_done(
|
|
1326
|
+
e.responseJSON || { error: "Unknown error: " + e.responseText }
|
|
1327
|
+
);
|
|
1328
|
+
},
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1270
1332
|
function inline_ajax_submit(e, opts1) {
|
|
1271
1333
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
1272
1334
|
e.preventDefault();
|
|
@@ -2032,6 +2094,28 @@ function update_time_of_week(nm) {
|
|
|
2032
2094
|
};
|
|
2033
2095
|
}
|
|
2034
2096
|
|
|
2097
|
+
function select_by_view_click(element, event, required) {
|
|
2098
|
+
const isAlreadySelected = $(element).hasClass("selected");
|
|
2099
|
+
$(element)
|
|
2100
|
+
.closest(".select-by-view-container")
|
|
2101
|
+
.find(".select-by-view-option")
|
|
2102
|
+
.removeClass("selected");
|
|
2103
|
+
if (!required && isAlreadySelected) {
|
|
2104
|
+
$(element)
|
|
2105
|
+
.closest(".select-by-view-container")
|
|
2106
|
+
.find("input[type=hidden]")
|
|
2107
|
+
.val("")
|
|
2108
|
+
.trigger("change");
|
|
2109
|
+
} else {
|
|
2110
|
+
$(element).addClass("selected");
|
|
2111
|
+
$(element)
|
|
2112
|
+
.closest(".select-by-view-container")
|
|
2113
|
+
.find("input[type=hidden]")
|
|
2114
|
+
.val($(element).attr("data-id"))
|
|
2115
|
+
.trigger("change");
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2035
2119
|
const observer = new IntersectionObserver(
|
|
2036
2120
|
(entries, observer) => {
|
|
2037
2121
|
entries.forEach((entry) => {
|
package/public/saltcorn.css
CHANGED
|
@@ -773,3 +773,42 @@ i[class*=" unicode-"] {
|
|
|
773
773
|
[data-animate-initial-hide] {
|
|
774
774
|
opacity: 0;
|
|
775
775
|
}
|
|
776
|
+
|
|
777
|
+
div.select-by-view-container {
|
|
778
|
+
display: flex;
|
|
779
|
+
}
|
|
780
|
+
div.select-by-view-container.justify-start {
|
|
781
|
+
justify-content: flex-start;
|
|
782
|
+
}
|
|
783
|
+
div.select-by-view-container.justify-end {
|
|
784
|
+
justify-content: flex-end;
|
|
785
|
+
}
|
|
786
|
+
div.select-by-view-container.justify-center {
|
|
787
|
+
justify-content: center;
|
|
788
|
+
}
|
|
789
|
+
div.select-by-view-container.justify-between {
|
|
790
|
+
justify-content: space-between;
|
|
791
|
+
}
|
|
792
|
+
div.select-by-view-container.justify-around {
|
|
793
|
+
justify-content: space-around;
|
|
794
|
+
}
|
|
795
|
+
div.select-by-view-container.justify-evenly {
|
|
796
|
+
justify-content: space-evenly;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
div.select-by-view-option.no-card {
|
|
800
|
+
border: 1px solid var(--bs-secondary, var(--tblr-secondary, blue));
|
|
801
|
+
}
|
|
802
|
+
div.select-by-view-option:hover {
|
|
803
|
+
border: 1px solid var(--bs-primary, var(--tblr-primary, blue));
|
|
804
|
+
}
|
|
805
|
+
div.select-by-view-option.selected {
|
|
806
|
+
border: 2px solid var(--bs-primary, var(--tblr-primary, blue));
|
|
807
|
+
}
|
|
808
|
+
tr span.add-tag {
|
|
809
|
+
opacity: 0;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
tr:hover span.add-tag {
|
|
813
|
+
opacity: 1;
|
|
814
|
+
}
|
package/public/saltcorn.js
CHANGED
|
@@ -34,7 +34,10 @@ function updateQueryStringParameter(uri1, key, value) {
|
|
|
34
34
|
uri = uris[0];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
var re = new RegExp(
|
|
37
|
+
var re = new RegExp(
|
|
38
|
+
"([?&])" + escapeRegExp(key) + "=.*?(&|$)",
|
|
39
|
+
"i"
|
|
40
|
+
);
|
|
38
41
|
var separator = uri.indexOf("?") !== -1 ? "&" : "?";
|
|
39
42
|
if (uri.match(re)) {
|
|
40
43
|
if (Array.isArray(value)) {
|
|
@@ -75,9 +78,12 @@ function removeQueryStringParameter(uri1, key, value) {
|
|
|
75
78
|
}
|
|
76
79
|
let re;
|
|
77
80
|
if (value) {
|
|
78
|
-
re = new RegExp(
|
|
81
|
+
re = new RegExp(
|
|
82
|
+
"([?&])" + escapeRegExp(key) + "=" + value + "?(&|$)",
|
|
83
|
+
"gi"
|
|
84
|
+
);
|
|
79
85
|
} else {
|
|
80
|
-
re = new RegExp("([?&])" + key + "=.*?(&|$)", "gi");
|
|
86
|
+
re = new RegExp("([?&])" + escapeRegExp(key) + "=.*?(&|$)", "gi");
|
|
81
87
|
}
|
|
82
88
|
if (uri.match(re)) {
|
|
83
89
|
uri = uri.replace(re, "$1" + "$2");
|
|
@@ -88,6 +94,10 @@ function removeQueryStringParameter(uri1, key, value) {
|
|
|
88
94
|
return uri + hash;
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
function escapeRegExp(string) {
|
|
98
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
function addQueryStringParameter(uri1, key, value) {
|
|
92
102
|
let hash = "";
|
|
93
103
|
let uri = uri1;
|
|
@@ -96,7 +106,10 @@ function addQueryStringParameter(uri1, key, value) {
|
|
|
96
106
|
hash = "#" + uris[1];
|
|
97
107
|
uri = uris[0];
|
|
98
108
|
}
|
|
99
|
-
var re = new RegExp(
|
|
109
|
+
var re = new RegExp(
|
|
110
|
+
"([?&])" + escapeRegExp(key) + "=" + value + "?(&|$)",
|
|
111
|
+
"gi"
|
|
112
|
+
);
|
|
100
113
|
if (uri.match(re)) return uri1;
|
|
101
114
|
|
|
102
115
|
var separator = uri.indexOf("?") !== -1 ? "&" : "?";
|
package/routes/actions.js
CHANGED
package/routes/admin.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
get_sys_info,
|
|
15
15
|
tenant_letsencrypt_name,
|
|
16
16
|
isAdminOrHasConfigMinRole,
|
|
17
|
+
checkEditPermission,
|
|
17
18
|
} = require("./utils.js");
|
|
18
19
|
const Table = require("@saltcorn/data/models/table");
|
|
19
20
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
@@ -567,7 +568,11 @@ router.get(
|
|
|
567
568
|
error_catcher(async (req, res) => {
|
|
568
569
|
const snaps = await Snapshot.find(
|
|
569
570
|
{},
|
|
570
|
-
{
|
|
571
|
+
{
|
|
572
|
+
orderBy: "created",
|
|
573
|
+
orderDesc: true,
|
|
574
|
+
fields: ["id", "created", "hash", "name"],
|
|
575
|
+
}
|
|
571
576
|
);
|
|
572
577
|
const locale = getState().getConfig("default_locale", "en");
|
|
573
578
|
send_admin_page({
|
|
@@ -595,7 +600,9 @@ router.get(
|
|
|
595
600
|
snap.created,
|
|
596
601
|
{},
|
|
597
602
|
locale
|
|
598
|
-
)} (${moment(snap.created).fromNow()})
|
|
603
|
+
)} (${moment(snap.created).fromNow()})${
|
|
604
|
+
snap.name ? ` [${snap.name}]` : ""
|
|
605
|
+
}`
|
|
599
606
|
)
|
|
600
607
|
)
|
|
601
608
|
)
|
|
@@ -627,20 +634,6 @@ router.get(
|
|
|
627
634
|
})
|
|
628
635
|
);
|
|
629
636
|
|
|
630
|
-
const checkEditPermission = (type, user) => {
|
|
631
|
-
if (user.role_id === 1) return true;
|
|
632
|
-
switch (type) {
|
|
633
|
-
case "view":
|
|
634
|
-
return getState().getConfig("min_role_edit_views", 1) >= user.role_id;
|
|
635
|
-
case "page":
|
|
636
|
-
return getState().getConfig("min_role_edit_pages", 1) >= user.role_id;
|
|
637
|
-
case "trigger":
|
|
638
|
-
return getState().getConfig("min_role_edit_triggers", 1) >= user.role_id;
|
|
639
|
-
default:
|
|
640
|
-
return false;
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
|
|
644
637
|
router.get(
|
|
645
638
|
"/snapshot-restore/:type/:name",
|
|
646
639
|
isAdminOrHasConfigMinRole([
|
|
@@ -652,7 +645,7 @@ router.get(
|
|
|
652
645
|
const { type, name } = req.params;
|
|
653
646
|
const snaps = await Snapshot.entity_history(type, name);
|
|
654
647
|
const locale = getState().getConfig("default_locale", "en");
|
|
655
|
-
const auth = checkEditPermission(type, req.user);
|
|
648
|
+
const auth = checkEditPermission(type + "s", req.user);
|
|
656
649
|
if (!auth) {
|
|
657
650
|
res.send("Not authorized");
|
|
658
651
|
return;
|
|
@@ -664,11 +657,14 @@ router.get(
|
|
|
664
657
|
{
|
|
665
658
|
label: req.__("When"),
|
|
666
659
|
key: (r) =>
|
|
667
|
-
`${
|
|
660
|
+
`${moment(
|
|
668
661
|
r.created
|
|
669
|
-
).fromNow()})
|
|
662
|
+
).fromNow()}<br><small>${localeDateTime(r.created, {}, locale)}</small>`,
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
label: req.__("Name"),
|
|
666
|
+
key: (r) => r.name || "",
|
|
670
667
|
},
|
|
671
|
-
|
|
672
668
|
{
|
|
673
669
|
label: req.__("Restore"),
|
|
674
670
|
key: (r) =>
|
|
@@ -694,7 +690,7 @@ router.post(
|
|
|
694
690
|
]),
|
|
695
691
|
error_catcher(async (req, res) => {
|
|
696
692
|
const { type, name, id } = req.params;
|
|
697
|
-
const auth = checkEditPermission(type, req.user);
|
|
693
|
+
const auth = checkEditPermission(type + "s", req.user);
|
|
698
694
|
if (!auth) {
|
|
699
695
|
req.flash("error", "Not authorized");
|
|
700
696
|
} else {
|
|
@@ -963,7 +959,8 @@ const snapshotForm = (req) =>
|
|
|
963
959
|
label: req.__("Snapshot now"),
|
|
964
960
|
id: "btnSnapNow",
|
|
965
961
|
class: "btn btn-outline-secondary",
|
|
966
|
-
onclick:
|
|
962
|
+
onclick:
|
|
963
|
+
"ajax_post('/admin/snapshot-now/'+prompt('Name of snapshot (optional)'))",
|
|
967
964
|
},
|
|
968
965
|
],
|
|
969
966
|
fields: [
|
|
@@ -1075,11 +1072,18 @@ router.post(
|
|
|
1075
1072
|
* Do Snapshot now
|
|
1076
1073
|
*/
|
|
1077
1074
|
router.post(
|
|
1078
|
-
"/snapshot-now",
|
|
1075
|
+
"/snapshot-now/:snapshotname?",
|
|
1079
1076
|
isAdmin,
|
|
1080
1077
|
error_catcher(async (req, res) => {
|
|
1078
|
+
const { snapshotname } = req.params;
|
|
1079
|
+
if (snapshotname == "null") {
|
|
1080
|
+
//user clicked cancel on prompt
|
|
1081
|
+
res.json({ success: true });
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1081
1085
|
try {
|
|
1082
|
-
const taken = await Snapshot.take_if_changed();
|
|
1086
|
+
const taken = await Snapshot.take_if_changed(snapshotname);
|
|
1083
1087
|
if (taken) req.flash("success", req.__("Snapshot successful"));
|
|
1084
1088
|
else
|
|
1085
1089
|
req.flash("success", req.__("No changes detected, snapshot skipped"));
|
|
@@ -1574,7 +1578,7 @@ const doInstall = async (req, res, version, deepClean, runPull) => {
|
|
|
1574
1578
|
}
|
|
1575
1579
|
const child = spawn(
|
|
1576
1580
|
"npm",
|
|
1577
|
-
["install", "-g", `@saltcorn/cli@${version}`, "--
|
|
1581
|
+
["install", "-g", `@saltcorn/cli@${version}`, "--omit=dev"],
|
|
1578
1582
|
{
|
|
1579
1583
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1580
1584
|
}
|
package/routes/api.js
CHANGED
|
@@ -279,14 +279,58 @@ router.get(
|
|
|
279
279
|
* @function
|
|
280
280
|
* @memberof module:routes/api~apiRouter
|
|
281
281
|
*/
|
|
282
|
-
|
|
282
|
+
|
|
283
|
+
function validateNumberMin(value, min) {
|
|
284
|
+
if (typeof value !== "number") {
|
|
285
|
+
// return false; //throw new TypeError('Value is not a number');
|
|
286
|
+
value = strictParseInt(value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!Number.isSafeInteger(value)) {
|
|
290
|
+
return false; //throw new RangeError('Value is outside the valid range for an integer');
|
|
291
|
+
}
|
|
292
|
+
if (value < min) return false;
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
router.get(
|
|
284
297
|
"/:tableName/",
|
|
285
298
|
//passport.authenticate("api-bearer", { session: false }),
|
|
286
299
|
error_catcher(async (req, res, next) => {
|
|
287
300
|
let { tableName } = req.params;
|
|
288
|
-
const {
|
|
289
|
-
|
|
301
|
+
const {
|
|
302
|
+
fields,
|
|
303
|
+
versioncount,
|
|
304
|
+
limit,
|
|
305
|
+
offset,
|
|
306
|
+
sortBy,
|
|
307
|
+
sortDesc,
|
|
308
|
+
approximate,
|
|
309
|
+
dereference,
|
|
310
|
+
tabulator_pagination_format,
|
|
311
|
+
...req_query0
|
|
312
|
+
} = req.query;
|
|
313
|
+
|
|
314
|
+
let req_query = req_query0;
|
|
315
|
+
let tabulator_size, tabulator_page, tabulator_sort, tabulator_dir;
|
|
316
|
+
if (tabulator_pagination_format) {
|
|
317
|
+
const { page, size, sort, ...rq } = req_query0;
|
|
318
|
+
req_query = rq;
|
|
319
|
+
tabulator_page = page;
|
|
320
|
+
tabulator_size = size;
|
|
321
|
+
tabulator_sort = sort?.[0]?.field;
|
|
322
|
+
tabulator_dir = sort?.[0]?.dir;
|
|
323
|
+
}
|
|
324
|
+
if (typeof limit !== "undefined")
|
|
325
|
+
if (isNaN(limit) || !validateNumberMin(limit, 1)) {
|
|
326
|
+
getState().log(3, `API get ${tableName} Invalid limit parameter`);
|
|
327
|
+
return res.status(400).send({ error: "Invalid limit parameter" });
|
|
328
|
+
}
|
|
329
|
+
if (typeof offset !== "undefined")
|
|
330
|
+
if (isNaN(offset) || !validateNumberMin(offset, 1)) {
|
|
331
|
+
getState().log(3, `API get ${tableName} Invalid offset parameter`);
|
|
332
|
+
return res.status(400).send({ error: "Invalid offset parameter" });
|
|
333
|
+
}
|
|
290
334
|
const table = Table.findOne(
|
|
291
335
|
strictParseInt(tableName)
|
|
292
336
|
? { id: strictParseInt(tableName) }
|
|
@@ -303,6 +347,8 @@ router.get(
|
|
|
303
347
|
res.status(404).json({ error: req.__("Not found") });
|
|
304
348
|
return;
|
|
305
349
|
}
|
|
350
|
+
const orderByField =
|
|
351
|
+
(sortBy || tabulator_sort) && table.getField(sortBy || tabulator_sort);
|
|
306
352
|
|
|
307
353
|
await passport.authenticate(
|
|
308
354
|
["api-bearer", "jwt"],
|
|
@@ -312,9 +358,17 @@ router.get(
|
|
|
312
358
|
let rows;
|
|
313
359
|
if (versioncount === "on") {
|
|
314
360
|
const joinOpts = {
|
|
315
|
-
orderBy: "id",
|
|
316
361
|
forUser: req.user || user || { role_id: 100 },
|
|
317
362
|
forPublic: !(req.user || user),
|
|
363
|
+
limit: tabulator_pagination_format
|
|
364
|
+
? +tabulator_size
|
|
365
|
+
: limit && +limit,
|
|
366
|
+
offset: tabulator_pagination_format
|
|
367
|
+
? +tabulator_size * (+tabulator_page - 1)
|
|
368
|
+
: offset && +offset,
|
|
369
|
+
orderDesc:
|
|
370
|
+
(sortDesc && sortDesc !== "false") || tabulator_dir == "desc",
|
|
371
|
+
orderBy: orderByField?.name || "id",
|
|
318
372
|
aggregations: {
|
|
319
373
|
_versions: {
|
|
320
374
|
table: table.name + "__history",
|
|
@@ -352,11 +406,20 @@ router.get(
|
|
|
352
406
|
rows = await table.getJoinedRows({
|
|
353
407
|
where: qstate,
|
|
354
408
|
joinFields,
|
|
409
|
+
limit: limit && +limit,
|
|
410
|
+
offset: offset && +offset,
|
|
411
|
+
orderDesc: sortDesc && sortDesc !== "false",
|
|
412
|
+
orderBy: orderByField?.name || undefined,
|
|
355
413
|
forPublic: !(req.user || user),
|
|
356
414
|
forUser: req.user || user,
|
|
357
415
|
});
|
|
358
416
|
}
|
|
359
|
-
|
|
417
|
+
if (tabulator_pagination_format) {
|
|
418
|
+
res.json({
|
|
419
|
+
last_page: Math.ceil((await table.countRows()) / +tabulator_size),
|
|
420
|
+
data: rows.map(limitFields(fields)),
|
|
421
|
+
});
|
|
422
|
+
} else res.json({ success: rows.map(limitFields(fields)) });
|
|
360
423
|
} else {
|
|
361
424
|
getState().log(3, `API get ${table.name} not authorized`);
|
|
362
425
|
res.status(401).json({ error: req.__("Not authorized") });
|
package/routes/common_lists.js
CHANGED
|
@@ -13,7 +13,17 @@ const {
|
|
|
13
13
|
} = require("@saltcorn/markup");
|
|
14
14
|
const { get_base_url } = require("./utils.js");
|
|
15
15
|
const { getState } = require("@saltcorn/data/db/state");
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
h4,
|
|
18
|
+
p,
|
|
19
|
+
div,
|
|
20
|
+
a,
|
|
21
|
+
i,
|
|
22
|
+
text,
|
|
23
|
+
span,
|
|
24
|
+
nbsp,
|
|
25
|
+
button,
|
|
26
|
+
} = require("@saltcorn/markup/tags");
|
|
17
27
|
|
|
18
28
|
/**
|
|
19
29
|
* @param {string} col
|
|
@@ -63,9 +73,11 @@ const tablesList = async (
|
|
|
63
73
|
getState().getConfig("min_role_edit_tables", 1) >= req.user.role_id;
|
|
64
74
|
const tagBadges = (table) => {
|
|
65
75
|
const myTags = tag_entries.filter((te) => te.table_id === table.id);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.join(nbsp)
|
|
76
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
77
|
+
return (
|
|
78
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "tables")).join(nbsp) +
|
|
79
|
+
mkAddBtn(tags, "tables", table.id, req, myTagIds)
|
|
80
|
+
);
|
|
69
81
|
};
|
|
70
82
|
|
|
71
83
|
return (
|
|
@@ -278,6 +290,38 @@ const tagsDropdown = (tags, altHeader) =>
|
|
|
278
290
|
)
|
|
279
291
|
);
|
|
280
292
|
|
|
293
|
+
const mkAddBtn = (tags, entityType, id, req, myTagIds) =>
|
|
294
|
+
div(
|
|
295
|
+
{ class: "dropdown d-inline ms-1" },
|
|
296
|
+
span(
|
|
297
|
+
{
|
|
298
|
+
class: "badge bg-secondary add-tag",
|
|
299
|
+
"data-bs-toggle": "dropdown",
|
|
300
|
+
"aria-haspopup": "true",
|
|
301
|
+
"aria-expanded": "false",
|
|
302
|
+
"data-boundary": "viewport",
|
|
303
|
+
},
|
|
304
|
+
i({ class: "fas fa-plus fa-sm" })
|
|
305
|
+
),
|
|
306
|
+
div(
|
|
307
|
+
{
|
|
308
|
+
class: "dropdown-menu dropdown-menu-end",
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
tags
|
|
312
|
+
.filter((t) => !myTagIds.has(t.id))
|
|
313
|
+
.map((t) =>
|
|
314
|
+
post_dropdown_item(
|
|
315
|
+
`/tag-entries/add-tag-entity/${encodeURIComponent(
|
|
316
|
+
t.name
|
|
317
|
+
)}/${entityType}/${id}`,
|
|
318
|
+
t.name,
|
|
319
|
+
req
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
|
|
281
325
|
const viewsList = async (
|
|
282
326
|
views,
|
|
283
327
|
req,
|
|
@@ -300,9 +344,12 @@ const viewsList = async (
|
|
|
300
344
|
|
|
301
345
|
const tagBadges = (view) => {
|
|
302
346
|
const myTags = tag_entries.filter((te) => te.view_id === view.id);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
347
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
348
|
+
const addBtn = mkAddBtn(tags, "views", view.id, req, myTagIds);
|
|
349
|
+
return (
|
|
350
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "views")).join(nbsp) +
|
|
351
|
+
addBtn
|
|
352
|
+
);
|
|
306
353
|
};
|
|
307
354
|
|
|
308
355
|
return (
|
|
@@ -504,9 +551,11 @@ const getPageList = async (
|
|
|
504
551
|
|
|
505
552
|
const tagBadges = (page) => {
|
|
506
553
|
const myTags = tag_entries.filter((te) => te.page_id === page.id);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.join(nbsp)
|
|
554
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
555
|
+
return (
|
|
556
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "pages")).join(nbsp) +
|
|
557
|
+
mkAddBtn(tags, "pages", page.id, req, myTagIds)
|
|
558
|
+
);
|
|
510
559
|
};
|
|
511
560
|
return mkTable(
|
|
512
561
|
[
|
|
@@ -602,7 +651,7 @@ const getPageGroupList = (rows, roles, req) => {
|
|
|
602
651
|
);
|
|
603
652
|
};
|
|
604
653
|
|
|
605
|
-
const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
|
|
654
|
+
const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
|
|
606
655
|
settingsDropdown(`dropdownMenuButton${trigger.id}`, [
|
|
607
656
|
a(
|
|
608
657
|
{
|
|
@@ -643,8 +692,8 @@ const getTriggerList = async (
|
|
|
643
692
|
const base_url = get_base_url(req);
|
|
644
693
|
const tags = await Tag.find();
|
|
645
694
|
const on_done_redirect_str = on_done_redirect
|
|
646
|
-
|
|
647
|
-
|
|
695
|
+
? `?on_done_redirect=${on_done_redirect}`
|
|
696
|
+
: "";
|
|
648
697
|
const tag_entries = await TagEntry.find({
|
|
649
698
|
not: { trigger_id: null },
|
|
650
699
|
});
|
|
@@ -657,9 +706,11 @@ const getTriggerList = async (
|
|
|
657
706
|
|
|
658
707
|
const tagBadges = (trigger) => {
|
|
659
708
|
const myTags = tag_entries.filter((te) => te.trigger_id === trigger.id);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
.join(nbsp)
|
|
709
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
710
|
+
return (
|
|
711
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "triggers")).join(nbsp) +
|
|
712
|
+
mkAddBtn(tags, "triggers", trigger.id, req, myTagIds)
|
|
713
|
+
);
|
|
663
714
|
};
|
|
664
715
|
return mkTable(
|
|
665
716
|
[
|
package/routes/fields.js
CHANGED
|
@@ -37,8 +37,8 @@ const {
|
|
|
37
37
|
} = require("@saltcorn/data/plugin-helper");
|
|
38
38
|
const { wizardCardTitle } = require("../markup/forms.js");
|
|
39
39
|
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
40
|
-
const { applyAsync } = require("@saltcorn/data/utils");
|
|
41
|
-
const { text } = require("@saltcorn/markup/tags");
|
|
40
|
+
const { applyAsync, isWeb } = require("@saltcorn/data/utils");
|
|
41
|
+
const { text, div } = require("@saltcorn/markup/tags");
|
|
42
42
|
const { mkFormContentNoLayout } = require("@saltcorn/markup/form");
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -1301,6 +1301,19 @@ router.post(
|
|
|
1301
1301
|
// - disabled inputs do not dispactch click events
|
|
1302
1302
|
const firefox = true;
|
|
1303
1303
|
const fv = fieldviews[fieldview];
|
|
1304
|
+
field.fieldview === fieldview;
|
|
1305
|
+
field.fieldviewObj = fv;
|
|
1306
|
+
field.attributes = { ...configuration, ...field.attributes };
|
|
1307
|
+
if (field.type === "Key")
|
|
1308
|
+
await field.fill_fkey_options(
|
|
1309
|
+
false,
|
|
1310
|
+
{},
|
|
1311
|
+
{},
|
|
1312
|
+
undefined,
|
|
1313
|
+
undefined,
|
|
1314
|
+
undefined,
|
|
1315
|
+
req.user
|
|
1316
|
+
);
|
|
1304
1317
|
if (!fv && field.type === "Key" && fieldview === "select")
|
|
1305
1318
|
res.send(
|
|
1306
1319
|
`<input ${
|
|
@@ -1422,3 +1435,101 @@ router.post(
|
|
|
1422
1435
|
res.send(mkFormContentNoLayout(form));
|
|
1423
1436
|
})
|
|
1424
1437
|
);
|
|
1438
|
+
|
|
1439
|
+
router.post(
|
|
1440
|
+
"/edit-get-fieldview",
|
|
1441
|
+
error_catcher(async (req, res) => {
|
|
1442
|
+
const { field_name, table_name, pk, fieldview, configuration } = req.body;
|
|
1443
|
+
const table = Table.findOne({ name: table_name });
|
|
1444
|
+
const row = await table.getRow(
|
|
1445
|
+
{ [table.pk_name]: pk },
|
|
1446
|
+
{ forUser: req.user, forPublic: !req.user }
|
|
1447
|
+
);
|
|
1448
|
+
const field = table.getField(field_name);
|
|
1449
|
+
let fv;
|
|
1450
|
+
if (field.is_fkey) {
|
|
1451
|
+
await field.fill_fkey_options(
|
|
1452
|
+
false,
|
|
1453
|
+
undefined,
|
|
1454
|
+
undefined,
|
|
1455
|
+
undefined,
|
|
1456
|
+
undefined,
|
|
1457
|
+
row[field_name],
|
|
1458
|
+
req.user
|
|
1459
|
+
);
|
|
1460
|
+
fv = getState().keyFieldviews.select;
|
|
1461
|
+
} else if (fieldview === "subfield" && field.type?.name === "JSON") {
|
|
1462
|
+
fv = field.type.fieldviews.edit_subfield;
|
|
1463
|
+
} else {
|
|
1464
|
+
//TODO: json subfield is special
|
|
1465
|
+
const fieldviews = field.type.fieldviews;
|
|
1466
|
+
fv = Object.values(fieldviews).find((v) => v.isEdit);
|
|
1467
|
+
}
|
|
1468
|
+
res.send(
|
|
1469
|
+
fv.run(
|
|
1470
|
+
field_name,
|
|
1471
|
+
row[field_name],
|
|
1472
|
+
{
|
|
1473
|
+
...field.attributes,
|
|
1474
|
+
...configuration,
|
|
1475
|
+
},
|
|
1476
|
+
"",
|
|
1477
|
+
false,
|
|
1478
|
+
field
|
|
1479
|
+
)
|
|
1480
|
+
);
|
|
1481
|
+
})
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
router.post(
|
|
1485
|
+
"/save-click-edit",
|
|
1486
|
+
error_catcher(async (req, res) => {
|
|
1487
|
+
const fielddata = JSON.parse(decodeURIComponent(req.body._fielddata));
|
|
1488
|
+
const { field_name, table_name, pk, fieldview, configuration, join_field } =
|
|
1489
|
+
fielddata;
|
|
1490
|
+
const table = Table.findOne({ name: table_name });
|
|
1491
|
+
const field = table.getField(field_name);
|
|
1492
|
+
let val = field.type?.read
|
|
1493
|
+
? field.type?.read(req.body[field_name])
|
|
1494
|
+
: req.body[field_name];
|
|
1495
|
+
await table.updateRow({ [field_name]: val }, pk, req.user);
|
|
1496
|
+
let fv;
|
|
1497
|
+
if (field.is_fkey) {
|
|
1498
|
+
if (join_field) {
|
|
1499
|
+
const refTable = Table.findOne({ name: field.reftable_name });
|
|
1500
|
+
const refRow = await refTable.getRow({ [refTable.pk_name]: val });
|
|
1501
|
+
val = refRow[join_field];
|
|
1502
|
+
const targetField = refTable.getField(join_field);
|
|
1503
|
+
const fieldviews = targetField.type.fieldviews;
|
|
1504
|
+
|
|
1505
|
+
fv = fieldviews[fieldview];
|
|
1506
|
+
} else fv = { run: (v) => `${v}` };
|
|
1507
|
+
} else {
|
|
1508
|
+
const fieldviews = field.type.fieldviews;
|
|
1509
|
+
|
|
1510
|
+
fv = fieldviews[fieldview];
|
|
1511
|
+
|
|
1512
|
+
if (!fv) {
|
|
1513
|
+
const fv1 = Object.values(fieldviews).find(
|
|
1514
|
+
(v) => !v.isEdit && !v.isFilter
|
|
1515
|
+
);
|
|
1516
|
+
fv = fv1;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
res.send(
|
|
1521
|
+
div(
|
|
1522
|
+
{
|
|
1523
|
+
"data-inline-edit-fielddata": req.body._fielddata,
|
|
1524
|
+
"data-inline-edit-ajax": "true",
|
|
1525
|
+
"data-inline-edit-dest-url": `/api/${table.name}/${pk}`,
|
|
1526
|
+
class: !isWeb(req) ? "mobile-data-inline-edit" : "",
|
|
1527
|
+
},
|
|
1528
|
+
fv.run(val, req, {
|
|
1529
|
+
...field.attributes,
|
|
1530
|
+
...configuration,
|
|
1531
|
+
})
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
})
|
|
1535
|
+
);
|
package/routes/list.js
CHANGED
|
@@ -402,25 +402,26 @@ router.get(
|
|
|
402
402
|
})
|
|
403
403
|
})
|
|
404
404
|
window.tabulator_table = new Tabulator("#jsGrid", {
|
|
405
|
-
ajaxURL:"/api/${encodeURIComponent(
|
|
406
|
-
|
|
405
|
+
ajaxURL:"/api/${encodeURIComponent(
|
|
406
|
+
table.name
|
|
407
|
+
)}?tabulator_pagination_format=true${
|
|
408
|
+
table.versioned ? "&versioncount=on" : ""
|
|
407
409
|
}",
|
|
408
410
|
layout:"fitColumns",
|
|
409
411
|
columns,
|
|
410
412
|
height:"100%",
|
|
411
413
|
pagination:true,
|
|
412
|
-
|
|
414
|
+
paginationMode:"remote",
|
|
415
|
+
paginationSize:10,
|
|
413
416
|
clipboard:true,
|
|
414
417
|
persistence:true,
|
|
415
418
|
persistenceID:"table_tab_${table.name}",
|
|
416
419
|
movableColumns: true,
|
|
420
|
+
ajaxContentType:"json",
|
|
421
|
+
sortMode:"remote",
|
|
417
422
|
initialSort:[
|
|
418
423
|
{column:"id", dir:"asc"},
|
|
419
|
-
],
|
|
420
|
-
ajaxResponse:function(url, params, response){
|
|
421
|
-
|
|
422
|
-
return response.success; //return the tableData property of a response json object
|
|
423
|
-
},
|
|
424
|
+
],
|
|
424
425
|
});
|
|
425
426
|
window.tabulator_table.on("cellEdited", function(cell){
|
|
426
427
|
const row = cell.getRow().getData()
|
package/routes/menu.js
CHANGED
|
@@ -9,7 +9,7 @@ const Router = require("express-promise-router");
|
|
|
9
9
|
|
|
10
10
|
//const Field = require("@saltcorn/data/models/field");
|
|
11
11
|
const Form = require("@saltcorn/data/models/form");
|
|
12
|
-
const { isAdmin, error_catcher } = require("./utils.js");
|
|
12
|
+
const { isAdmin, error_catcher, isAdminOrHasConfigMinRole } = require("./utils.js");
|
|
13
13
|
const { getState } = require("@saltcorn/data/db/state");
|
|
14
14
|
//const File = require("@saltcorn/data/models/file");
|
|
15
15
|
const User = require("@saltcorn/data/models/user");
|
|
@@ -490,7 +490,7 @@ const menuTojQME = (menu_items) =>
|
|
|
490
490
|
*/
|
|
491
491
|
router.get(
|
|
492
492
|
"/",
|
|
493
|
-
|
|
493
|
+
isAdminOrHasConfigMinRole("min_role_edit_menu"),
|
|
494
494
|
error_catcher(async (req, res) => {
|
|
495
495
|
const form = await menuForm(req);
|
|
496
496
|
const state = getState();
|
|
@@ -566,7 +566,7 @@ const jQMEtoMenu = (menu_items) =>
|
|
|
566
566
|
*/
|
|
567
567
|
router.post(
|
|
568
568
|
"/",
|
|
569
|
-
|
|
569
|
+
isAdminOrHasConfigMinRole("min_role_edit_menu"),
|
|
570
570
|
error_catcher(async (req, res) => {
|
|
571
571
|
const new_menu = req.body;
|
|
572
572
|
const menu_items = jQMEtoMenu(new_menu);
|
package/routes/tag_entries.js
CHANGED
|
@@ -12,7 +12,13 @@ const Tag = require("@saltcorn/data/models/tag");
|
|
|
12
12
|
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
13
13
|
const Router = require("express-promise-router");
|
|
14
14
|
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
isAdmin,
|
|
17
|
+
error_catcher,
|
|
18
|
+
csrfField,
|
|
19
|
+
isAdminOrHasConfigMinRole,
|
|
20
|
+
checkEditPermission,
|
|
21
|
+
} = require("./utils");
|
|
16
22
|
|
|
17
23
|
const Table = require("@saltcorn/data/models/table");
|
|
18
24
|
const View = require("@saltcorn/data/models/view");
|
|
@@ -170,6 +176,42 @@ router.post(
|
|
|
170
176
|
})
|
|
171
177
|
);
|
|
172
178
|
|
|
179
|
+
router.post(
|
|
180
|
+
"/add-tag-entity/:tagname/:entitytype/:entityid",
|
|
181
|
+
isAdminOrHasConfigMinRole([
|
|
182
|
+
"min_role_edit_tables",
|
|
183
|
+
"min_role_edit_views",
|
|
184
|
+
"min_role_edit_pages",
|
|
185
|
+
"min_role_edit_triggers",
|
|
186
|
+
]),
|
|
187
|
+
error_catcher(async (req, res) => {
|
|
188
|
+
const { tagname, entitytype, entityid } = req.params;
|
|
189
|
+
const tag = await Tag.findOne({ name: tagname });
|
|
190
|
+
|
|
191
|
+
const fieldName = idField(entitytype);
|
|
192
|
+
const auth = checkEditPermission(entitytype, req.user);
|
|
193
|
+
if (!auth) req.flash("error", "Not authorized");
|
|
194
|
+
else await tag.addEntry({ [fieldName]: +entityid });
|
|
195
|
+
switch (entitytype) {
|
|
196
|
+
case "views":
|
|
197
|
+
res.redirect(`/viewedit`);
|
|
198
|
+
break;
|
|
199
|
+
case "pages":
|
|
200
|
+
res.redirect(`/pageedit`);
|
|
201
|
+
break;
|
|
202
|
+
case "tables":
|
|
203
|
+
res.redirect(`/table`);
|
|
204
|
+
break;
|
|
205
|
+
case "triggers":
|
|
206
|
+
res.redirect(`/actions`);
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
|
|
173
215
|
// add one object to multiple tags
|
|
174
216
|
router.post(
|
|
175
217
|
"/add/multiple_tags/:entry_type/:object_id",
|
package/routes/tags.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { a, text, i, div } = require("@saltcorn/markup/tags");
|
|
2
2
|
|
|
3
3
|
const Tag = require("@saltcorn/data/models/tag");
|
|
4
|
+
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
4
5
|
const Router = require("express-promise-router");
|
|
5
6
|
const Form = require("@saltcorn/data/models/form");
|
|
6
7
|
const User = require("@saltcorn/data/models/user");
|
package/routes/utils.js
CHANGED
|
@@ -611,6 +611,20 @@ const getRandomPage = (pageGroup, req) => {
|
|
|
611
611
|
return Page.findOne({ id: sessionMember.page_id });
|
|
612
612
|
};
|
|
613
613
|
|
|
614
|
+
const checkEditPermission = (type, user) => {
|
|
615
|
+
if (user.role_id === 1) return true;
|
|
616
|
+
switch (type) {
|
|
617
|
+
case "views":
|
|
618
|
+
return getState().getConfig("min_role_edit_views", 1) >= user.role_id;
|
|
619
|
+
case "pages":
|
|
620
|
+
return getState().getConfig("min_role_edit_pages", 1) >= user.role_id;
|
|
621
|
+
case "triggers":
|
|
622
|
+
return getState().getConfig("min_role_edit_triggers", 1) >= user.role_id;
|
|
623
|
+
default:
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
614
628
|
module.exports = {
|
|
615
629
|
sqlsanitize,
|
|
616
630
|
csrfField,
|
|
@@ -634,4 +648,5 @@ module.exports = {
|
|
|
634
648
|
getEligiblePage,
|
|
635
649
|
getRandomPage,
|
|
636
650
|
tenant_letsencrypt_name,
|
|
651
|
+
checkEditPermission,
|
|
637
652
|
};
|
package/serve.js
CHANGED
|
@@ -99,20 +99,24 @@ const ensurePluginsFolder = async () => {
|
|
|
99
99
|
const staticDeps = ["@saltcorn/markup", "@saltcorn/data", "jest"];
|
|
100
100
|
const allPluginFolders = new Set();
|
|
101
101
|
await eachTenant(async () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (const plugin of allPlugins) {
|
|
106
|
-
const tokens =
|
|
107
|
-
plugin.source === "npm"
|
|
108
|
-
? plugin.location.split("/")
|
|
109
|
-
: plugin.name.split("/");
|
|
110
|
-
const pluginDir = path.join(
|
|
111
|
-
rootFolder,
|
|
112
|
-
plugin.source === "git" ? "git_plugins" : "plugins_folder",
|
|
113
|
-
...tokens
|
|
102
|
+
try {
|
|
103
|
+
const allPlugins = (await Plugin.find()).filter(
|
|
104
|
+
(p) => !["base", "sbadmin2"].includes(p.name)
|
|
114
105
|
);
|
|
115
|
-
|
|
106
|
+
for (const plugin of allPlugins) {
|
|
107
|
+
const tokens =
|
|
108
|
+
plugin.source === "npm"
|
|
109
|
+
? plugin.location.split("/")
|
|
110
|
+
: plugin.name.split("/");
|
|
111
|
+
const pluginDir = path.join(
|
|
112
|
+
rootFolder,
|
|
113
|
+
plugin.source === "git" ? "git_plugins" : "plugins_folder",
|
|
114
|
+
...tokens
|
|
115
|
+
);
|
|
116
|
+
allPluginFolders.add(pluginDir);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
//ignore
|
|
116
120
|
}
|
|
117
121
|
});
|
|
118
122
|
for (const folder of allPluginFolders) {
|
package/tests/api.test.js
CHANGED
|
@@ -107,6 +107,19 @@ describe("API read", () => {
|
|
|
107
107
|
)
|
|
108
108
|
);
|
|
109
109
|
});
|
|
110
|
+
it("should get books limit", async () => {
|
|
111
|
+
const app = await getApp({ disableCsrf: true });
|
|
112
|
+
await request(app)
|
|
113
|
+
.get("/api/books/?limit=1&offset=1&sortBy=pages")
|
|
114
|
+
.expect(
|
|
115
|
+
succeedJsonWith(
|
|
116
|
+
(rows) =>
|
|
117
|
+
rows.length == 1 &&
|
|
118
|
+
rows[0].author === "Herman Melville" &&
|
|
119
|
+
rows[0].pages === 967
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
});
|
|
110
123
|
it("should handle fkey args ", async () => {
|
|
111
124
|
const loginCookie = await getAdminLoginCookie();
|
|
112
125
|
const app = await getApp({ disableCsrf: true });
|
package/tests/clientjs.test.js
CHANGED
|
@@ -54,6 +54,26 @@ test("updateQueryStringParameter", () => {
|
|
|
54
54
|
"AK"
|
|
55
55
|
)
|
|
56
56
|
).toBe("/foo?publisher.publisher->name=AK");
|
|
57
|
+
expect(
|
|
58
|
+
updateQueryStringParameter(
|
|
59
|
+
"/foo?factor.Factors->focus_area.Focus%20area->short_name=Leadership",
|
|
60
|
+
"factor.Factors->focus_area.Focus%20area->short_name",
|
|
61
|
+
"Marketing"
|
|
62
|
+
)
|
|
63
|
+
).toBe("/foo?factor.Factors->focus_area.Focus%20area->short_name=Marketing");
|
|
64
|
+
expect(
|
|
65
|
+
updateQueryStringParameter(
|
|
66
|
+
"/foo?factor.Factors%20(Solaris)->focus_area.Focus%20area%20(Solaris)->short_name=Leadership",
|
|
67
|
+
"factor.Factors%20(Solaris)->focus_area.Focus%20area%20(Solaris)->short_name",
|
|
68
|
+
"Marketing"
|
|
69
|
+
)
|
|
70
|
+
).toBe(
|
|
71
|
+
"/foo?factor.Factors%20(Solaris)->focus_area.Focus%20area%20(Solaris)->short_name=Marketing"
|
|
72
|
+
);
|
|
73
|
+
expect(removeQueryStringParameter("/foo?name=Bar&factor.Factors%20(Solaris)->focus_area.Focus%20area%20(Solaris)->short_name=Leadership", "factor.Factors%20(Solaris)->focus_area.Focus%20area%20(Solaris)->short_name")).toBe(
|
|
74
|
+
"/foo?name=Bar"
|
|
75
|
+
);
|
|
76
|
+
|
|
57
77
|
expect(updateQueryStringParameter("/foo", "_or_field", ["baz", "bar"])).toBe(
|
|
58
78
|
"/foo?_or_field=baz&_or_field=bar"
|
|
59
79
|
);
|
package/wrapper.js
CHANGED
|
@@ -96,6 +96,7 @@ const get_menu = (req) => {
|
|
|
96
96
|
const canEditViews = state.getConfig("min_role_edit_views", 1) >= role;
|
|
97
97
|
const canEditPages = state.getConfig("min_role_edit_pages", 1) >= role;
|
|
98
98
|
const canEditTriggers = state.getConfig("min_role_edit_triggers", 1) >= role;
|
|
99
|
+
const canEditMenu = state.getConfig("min_role_edit_menu", 1) >= role;
|
|
99
100
|
const isAdmin = role === 1;
|
|
100
101
|
const hasAdmin =
|
|
101
102
|
isAdmin ||
|
|
@@ -103,6 +104,7 @@ const get_menu = (req) => {
|
|
|
103
104
|
canInspectTables ||
|
|
104
105
|
canEditPages ||
|
|
105
106
|
canEditViews ||
|
|
107
|
+
canEditMenu ||
|
|
106
108
|
canEditTriggers;
|
|
107
109
|
/*
|
|
108
110
|
* Admin Menu items
|
|
@@ -128,13 +130,41 @@ const get_menu = (req) => {
|
|
|
128
130
|
icon: "far fa-file",
|
|
129
131
|
label: req.__("Pages"),
|
|
130
132
|
});
|
|
131
|
-
if (canEditTriggers && !isAdmin)
|
|
133
|
+
if (canEditTriggers && !canEditMenu && !isAdmin)
|
|
132
134
|
adminItems.push({
|
|
133
135
|
link: "/actions",
|
|
134
136
|
altlinks: ["/events", "/eventlog", "/crashlog"],
|
|
135
137
|
icon: "fas fa-calendar-check",
|
|
136
138
|
label: req.__("Triggers"),
|
|
137
139
|
});
|
|
140
|
+
if (canEditMenu && !isAdmin) {
|
|
141
|
+
const subitems = [
|
|
142
|
+
{
|
|
143
|
+
link: "/menu",
|
|
144
|
+
altlinks: [
|
|
145
|
+
"/site-structure",
|
|
146
|
+
"/search/config",
|
|
147
|
+
"/library/list",
|
|
148
|
+
"/tenant/list",
|
|
149
|
+
],
|
|
150
|
+
icon: "fas fa-compass",
|
|
151
|
+
label: req.__("Menu"),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
if (canEditTriggers)
|
|
155
|
+
subitems.push({
|
|
156
|
+
link: "/actions",
|
|
157
|
+
altlinks: ["/events", "/eventlog", "/crashlog"],
|
|
158
|
+
icon: "fas fa-calendar-check",
|
|
159
|
+
label: req.__("Triggers"),
|
|
160
|
+
});
|
|
161
|
+
adminItems.push({
|
|
162
|
+
label: req.__("Settings"),
|
|
163
|
+
icon: "fas fa-wrench",
|
|
164
|
+
subitems,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
138
168
|
if (isAdmin)
|
|
139
169
|
adminItems.push({
|
|
140
170
|
label: req.__("Settings"),
|