@saltcorn/server 0.8.8-beta.4 → 0.8.8-beta.6
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/auth/routes.js +46 -16
- package/locales/en.json +4 -2
- package/locales/it.json +17 -2
- package/package.json +8 -8
- package/public/saltcorn-common.js +34 -6
- package/public/saltcorn.css +5 -0
- package/public/saltcorn.js +55 -22
- package/routes/admin.js +8 -2
- package/routes/api.js +20 -11
- package/routes/common_lists.js +3 -3
- package/routes/crashlog.js +1 -1
- package/routes/eventlog.js +5 -4
- package/routes/search.js +1 -1
- package/routes/tables.js +7 -3
- package/tests/api.test.js +16 -0
- package/tests/view.test.js +65 -2
package/auth/routes.js
CHANGED
|
@@ -254,7 +254,9 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
|
|
|
254
254
|
res.json(token);
|
|
255
255
|
} else {
|
|
256
256
|
res.json({
|
|
257
|
-
alerts: [
|
|
257
|
+
alerts: [
|
|
258
|
+
{ type: "danger", msg: req.__("Incorrect user or password") },
|
|
259
|
+
],
|
|
258
260
|
});
|
|
259
261
|
}
|
|
260
262
|
} else if (publicUserLink) {
|
|
@@ -276,7 +278,9 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
|
|
|
276
278
|
res.json(token);
|
|
277
279
|
} else {
|
|
278
280
|
res.json({
|
|
279
|
-
alerts: [
|
|
281
|
+
alerts: [
|
|
282
|
+
{ type: "danger", msg: req.__("The public login is deactivated") },
|
|
283
|
+
],
|
|
280
284
|
});
|
|
281
285
|
}
|
|
282
286
|
};
|
|
@@ -628,9 +632,11 @@ router.post(
|
|
|
628
632
|
* @throws {InvalidConfiguration}
|
|
629
633
|
*/
|
|
630
634
|
const getNewUserForm = async (new_user_view_name, req, askEmail) => {
|
|
635
|
+
if (!new_user_view_name) return;
|
|
631
636
|
const view = await View.findOne({ name: new_user_view_name });
|
|
632
637
|
if (!view)
|
|
633
638
|
throw new InvalidConfiguration("New user form view does not exist");
|
|
639
|
+
if (view.viewtemplate !== "Edit") return;
|
|
634
640
|
const table = Table.findOne({ name: "users" });
|
|
635
641
|
const fields = table.getFields();
|
|
636
642
|
const { columns, layout } = view.configuration;
|
|
@@ -704,14 +710,14 @@ const getNewUserForm = async (new_user_view_name, req, askEmail) => {
|
|
|
704
710
|
* @param {object} res
|
|
705
711
|
* @returns {void}
|
|
706
712
|
*/
|
|
707
|
-
const signup_login_with_user = (u, req, res) =>
|
|
713
|
+
const signup_login_with_user = (u, req, res, redirUrl) =>
|
|
708
714
|
req.login(u.session_object, function (err) {
|
|
709
715
|
if (!err) {
|
|
710
716
|
Trigger.emitEvent("Login", null, u);
|
|
711
717
|
if (getState().verifier) res.redirect("/auth/verification-flow");
|
|
712
718
|
else if (getState().get2FApolicy(u) === "Mandatory")
|
|
713
719
|
res.redirect("/auth/twofa/setup/totp");
|
|
714
|
-
else res.redirect("/");
|
|
720
|
+
else res.redirect(redirUrl || "/");
|
|
715
721
|
} else {
|
|
716
722
|
req.flash("danger", err);
|
|
717
723
|
res.redirect("/auth/signup");
|
|
@@ -869,7 +875,8 @@ router.post(
|
|
|
869
875
|
return;
|
|
870
876
|
}
|
|
871
877
|
|
|
872
|
-
const unsuitableEmailPassword = async (
|
|
878
|
+
const unsuitableEmailPassword = async (urecord) => {
|
|
879
|
+
const { email, password, passwordRepeat } = urecord;
|
|
873
880
|
if (!email || !password) {
|
|
874
881
|
req.flash("danger", req.__("E-mail and password required"));
|
|
875
882
|
res.redirect("/auth/signup");
|
|
@@ -911,6 +918,12 @@ router.post(
|
|
|
911
918
|
res.redirect("/auth/signup");
|
|
912
919
|
return true;
|
|
913
920
|
}
|
|
921
|
+
let constraint_check_error = User.table.check_table_constraints(urecord);
|
|
922
|
+
if (constraint_check_error) {
|
|
923
|
+
req.flash("danger", constraint_check_error);
|
|
924
|
+
res.redirect("/auth/signup");
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
914
927
|
};
|
|
915
928
|
const new_user_form = getState().getConfig("new_user_form");
|
|
916
929
|
|
|
@@ -943,21 +956,32 @@ router.post(
|
|
|
943
956
|
signup_form.values[f.name] = signup_form.values[f.name] || "";
|
|
944
957
|
});
|
|
945
958
|
const userObject = signup_form.values;
|
|
946
|
-
const { email, password, passwordRepeat } = userObject;
|
|
947
|
-
if (await unsuitableEmailPassword(
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
const form = await getNewUserForm(new_user_form, req);
|
|
959
|
+
//const { email, password, passwordRepeat } = userObject;
|
|
960
|
+
if (await unsuitableEmailPassword(userObject)) return;
|
|
961
|
+
const new_user_form_form = await getNewUserForm(new_user_form, req);
|
|
962
|
+
if (new_user_form_form) {
|
|
951
963
|
Object.entries(userObject).forEach(([k, v]) => {
|
|
952
|
-
|
|
953
|
-
if (!
|
|
964
|
+
new_user_form_form.values[k] = v;
|
|
965
|
+
if (!new_user_form_form.fields.find((f) => f.name === k))
|
|
966
|
+
new_user_form_form.hidden(k);
|
|
954
967
|
});
|
|
955
|
-
res.sendAuthWrap(
|
|
968
|
+
res.sendAuthWrap(
|
|
969
|
+
new_user_form,
|
|
970
|
+
new_user_form_form,
|
|
971
|
+
getAuthLinks("signup", true)
|
|
972
|
+
);
|
|
956
973
|
} else {
|
|
957
974
|
const u = await User.create(userObject);
|
|
958
975
|
await send_verification_email(u, req);
|
|
959
976
|
|
|
960
|
-
signup_login_with_user(
|
|
977
|
+
signup_login_with_user(
|
|
978
|
+
u,
|
|
979
|
+
req,
|
|
980
|
+
res,
|
|
981
|
+
new_user_form && !new_user_form_form
|
|
982
|
+
? `/view/${new_user_form}?id=${u.id}`
|
|
983
|
+
: undefined
|
|
984
|
+
);
|
|
961
985
|
}
|
|
962
986
|
return;
|
|
963
987
|
}
|
|
@@ -972,7 +996,7 @@ router.post(
|
|
|
972
996
|
res.sendAuthWrap(req.__(`Sign up`), form, getAuthLinks("signup"));
|
|
973
997
|
} else {
|
|
974
998
|
const { email, password } = form.values;
|
|
975
|
-
if (await unsuitableEmailPassword(email, password)) return;
|
|
999
|
+
if (await unsuitableEmailPassword({ email, password })) return;
|
|
976
1000
|
if (new_user_form) {
|
|
977
1001
|
const form = await getNewUserForm(new_user_form, req);
|
|
978
1002
|
form.values.email = email;
|
|
@@ -1100,7 +1124,13 @@ router.get(
|
|
|
1100
1124
|
const { method } = req.params;
|
|
1101
1125
|
if (method === "jwt") {
|
|
1102
1126
|
const { email, password } = req.query;
|
|
1103
|
-
await loginWithJwt(
|
|
1127
|
+
await loginWithJwt(
|
|
1128
|
+
email,
|
|
1129
|
+
password,
|
|
1130
|
+
req.headers["x-saltcorn-app"],
|
|
1131
|
+
res,
|
|
1132
|
+
req
|
|
1133
|
+
);
|
|
1104
1134
|
} else {
|
|
1105
1135
|
const auth = getState().auth_methods[method];
|
|
1106
1136
|
if (auth) {
|
package/locales/en.json
CHANGED
|
@@ -1246,5 +1246,7 @@
|
|
|
1246
1246
|
"Included Plugins": "Included Plugins",
|
|
1247
1247
|
"exclude": "exclude",
|
|
1248
1248
|
"include": "include",
|
|
1249
|
-
"Auto public login": "Auto public login"
|
|
1250
|
-
|
|
1249
|
+
"Auto public login": "Auto public login",
|
|
1250
|
+
"New user view": "New user view",
|
|
1251
|
+
"A view to show to new users, to finalise registration (if Edit) or as a welcome view": "A view to show to new users, to finalise registration (if Edit) or as a welcome view"
|
|
1252
|
+
}
|
package/locales/it.json
CHANGED
|
@@ -503,5 +503,20 @@
|
|
|
503
503
|
"Table columns": "Table columns",
|
|
504
504
|
"Configuration items": "Configuration items",
|
|
505
505
|
"Crashlogs": "Crashlogs",
|
|
506
|
-
"Logged out user %s": "Logged out user %s"
|
|
507
|
-
|
|
506
|
+
"Logged out user %s": "Logged out user %s",
|
|
507
|
+
"CSV upload": "CSV upload",
|
|
508
|
+
"Pages are the web pages of your application built with a drag-and-drop builder. They have static content, and by embedding views, dynamic content.": "Pages are the web pages of your application built with a drag-and-drop builder. They have static content, and by embedding views, dynamic content.",
|
|
509
|
+
"Create page": "Create page",
|
|
510
|
+
"Triggers run actions in response to events.": "Triggers run actions in response to events.",
|
|
511
|
+
"No triggers": "No triggers",
|
|
512
|
+
"Upload file(s)": "Upload file(s)",
|
|
513
|
+
"Pattern": "Pattern",
|
|
514
|
+
"You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.": "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.",
|
|
515
|
+
"Views potentially affected": "Views potentially affected",
|
|
516
|
+
"Fixed and blocked fields": "Fixed and blocked fields",
|
|
517
|
+
"URL after delete": "URL after delete",
|
|
518
|
+
"Save before going back": "Save before going back",
|
|
519
|
+
"Reload after going back": "Reload after going back",
|
|
520
|
+
"Steps to go back": "Steps to go back",
|
|
521
|
+
"%s configuration": "%s configuration"
|
|
522
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.8.8-beta.
|
|
3
|
+
"version": "0.8.8-beta.6",
|
|
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.8.8-beta.
|
|
10
|
-
"@saltcorn/builder": "0.8.8-beta.
|
|
11
|
-
"@saltcorn/data": "0.8.8-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.8.8-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.8.8-beta.
|
|
14
|
-
"@saltcorn/markup": "0.8.8-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.8.8-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.8.8-beta.6",
|
|
10
|
+
"@saltcorn/builder": "0.8.8-beta.6",
|
|
11
|
+
"@saltcorn/data": "0.8.8-beta.6",
|
|
12
|
+
"@saltcorn/admin-models": "0.8.8-beta.6",
|
|
13
|
+
"@saltcorn/filemanager": "0.8.8-beta.6",
|
|
14
|
+
"@saltcorn/markup": "0.8.8-beta.6",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.8.8-beta.6",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
|
@@ -41,6 +41,15 @@ const _apply_showif_plugins = [];
|
|
|
41
41
|
const add_apply_showif_plugin = (p) => {
|
|
42
42
|
_apply_showif_plugins.push(p);
|
|
43
43
|
};
|
|
44
|
+
|
|
45
|
+
const nubBy = (prop, xs) => {
|
|
46
|
+
const vs = new Set();
|
|
47
|
+
return xs.filter((x) => {
|
|
48
|
+
if (vs.has(x[prop])) return false;
|
|
49
|
+
vs.add(x[prop]);
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
44
53
|
function apply_showif() {
|
|
45
54
|
$("[data-show-if]").each(function (ix, element) {
|
|
46
55
|
var e = $(element);
|
|
@@ -109,10 +118,16 @@ function apply_showif() {
|
|
|
109
118
|
const dynwhere = JSON.parse(
|
|
110
119
|
decodeURIComponent(e.attr("data-fetch-options"))
|
|
111
120
|
);
|
|
112
|
-
//console.log(dynwhere);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
//console.log("dynwhere", dynwhere);
|
|
122
|
+
const qss = Object.entries(dynwhere.whereParsed).map(
|
|
123
|
+
([k, v]) => `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`
|
|
124
|
+
);
|
|
125
|
+
if (dynwhere.dereference) {
|
|
126
|
+
if (Array.isArray(dynwhere.dereference))
|
|
127
|
+
qss.push(...dynwhere.dereference.map((d) => `dereference=${d}`));
|
|
128
|
+
else qss.push(`dereference=${dynwhere.dereference}`);
|
|
129
|
+
}
|
|
130
|
+
const qs = qss.join("&");
|
|
116
131
|
var current = e.attr("data-selected");
|
|
117
132
|
e.change(function (ec) {
|
|
118
133
|
e.attr("data-selected", ec.target.value);
|
|
@@ -129,7 +144,11 @@ function apply_showif() {
|
|
|
129
144
|
if (!dynwhere.required) toAppend.push(`<option></option>`);
|
|
130
145
|
let currentDataOption = undefined;
|
|
131
146
|
const dataOptions = [];
|
|
132
|
-
|
|
147
|
+
//console.log(success);
|
|
148
|
+
const success1 = dynwhere.nubBy
|
|
149
|
+
? nubBy(dynwhere.nubBy, success)
|
|
150
|
+
: success;
|
|
151
|
+
success1.forEach((r) => {
|
|
133
152
|
const label = dynwhere.label_formula
|
|
134
153
|
? new Function(
|
|
135
154
|
`{${Object.keys(r).join(",")}}`,
|
|
@@ -137,6 +156,7 @@ function apply_showif() {
|
|
|
137
156
|
)(r)
|
|
138
157
|
: r[dynwhere.summary_field];
|
|
139
158
|
const value = r[dynwhere.refname];
|
|
159
|
+
//console.log("lv", label, value, r, dynwhere.summary_field);
|
|
140
160
|
const selected = `${current}` === `${r[dynwhere.refname]}`;
|
|
141
161
|
dataOptions.push({ text: label, value });
|
|
142
162
|
if (selected) currentDataOption = value;
|
|
@@ -510,6 +530,14 @@ function initialize_page() {
|
|
|
510
530
|
if (schema) {
|
|
511
531
|
schema = JSON.parse(decodeURIComponent(schema));
|
|
512
532
|
}
|
|
533
|
+
if (type === "Date") {
|
|
534
|
+
console.log("timeelsems", $(this).find("span.current time"));
|
|
535
|
+
current =
|
|
536
|
+
$(this).attr("data-inline-edit-current") ||
|
|
537
|
+
$(this).find("span.current time").attr("datetime"); // ||
|
|
538
|
+
//$(this).children("span.current").html();
|
|
539
|
+
}
|
|
540
|
+
console.log({ type, current });
|
|
513
541
|
var is_key = type?.startsWith("Key:");
|
|
514
542
|
const opts = encodeURIComponent(
|
|
515
543
|
JSON.stringify({
|
|
@@ -875,7 +903,7 @@ function common_done(res, isWeb = true) {
|
|
|
875
903
|
if (res.eval_js) handle(res.eval_js, eval);
|
|
876
904
|
|
|
877
905
|
if (res.reload_page) {
|
|
878
|
-
(isWeb ? location : parent
|
|
906
|
+
(isWeb ? location : parent).reload(); //TODO notify to cookie if reload or goto
|
|
879
907
|
}
|
|
880
908
|
if (res.download) {
|
|
881
909
|
handle(res.download, (download) => {
|
package/public/saltcorn.css
CHANGED
package/public/saltcorn.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
function sortby(k, desc, viewIdentifier) {
|
|
2
|
-
set_state_fields(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
? "
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
function sortby(k, desc, viewIdentifier, e) {
|
|
2
|
+
set_state_fields(
|
|
3
|
+
{
|
|
4
|
+
[viewIdentifier ? `_${viewIdentifier}_sortby` : "_sortby"]: k,
|
|
5
|
+
[viewIdentifier ? `_${viewIdentifier}_sortdesc` : "_sortdesc"]: desc
|
|
6
|
+
? "on"
|
|
7
|
+
: { unset: true },
|
|
8
|
+
},
|
|
9
|
+
false,
|
|
10
|
+
e
|
|
11
|
+
);
|
|
8
12
|
}
|
|
9
|
-
function gopage(n, pagesize, viewIdentifier, extra = {}) {
|
|
13
|
+
function gopage(n, pagesize, viewIdentifier, extra = {}, e) {
|
|
10
14
|
const cfg = {
|
|
11
15
|
...extra,
|
|
12
16
|
[viewIdentifier ? `_${viewIdentifier}_page` : "_page"]: n,
|
|
13
17
|
[viewIdentifier ? `_${viewIdentifier}_pagesize` : "_pagesize"]: pagesize,
|
|
14
18
|
};
|
|
15
|
-
set_state_fields(cfg);
|
|
19
|
+
set_state_fields(cfg, false, e);
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
if (localStorage.getItem("reload_on_init")) {
|
|
@@ -72,28 +76,31 @@ function get_current_state_url(e) {
|
|
|
72
76
|
else return $modal.prop("data-modal-state");
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
function select_id(id) {
|
|
76
|
-
pjax_to(updateQueryStringParameter(get_current_state_url(), "id", id));
|
|
79
|
+
function select_id(id, e) {
|
|
80
|
+
pjax_to(updateQueryStringParameter(get_current_state_url(e), "id", id), e);
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
function set_state_field(key, value, e) {
|
|
80
84
|
pjax_to(updateQueryStringParameter(get_current_state_url(e), key, value), e);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
function check_state_field(that) {
|
|
87
|
+
function check_state_field(that, e) {
|
|
84
88
|
const checked = that.checked;
|
|
85
89
|
const name = that.name;
|
|
86
90
|
const value = encodeURIComponent(that.value);
|
|
87
|
-
var separator =
|
|
91
|
+
var separator = get_current_state_url(e).indexOf("?") !== -1 ? "&" : "?";
|
|
88
92
|
let dest;
|
|
89
|
-
if (checked) dest = get_current_state_url() + `${separator}${name}=${value}`;
|
|
90
|
-
else dest = get_current_state_url().replace(`${name}=${value}`, "");
|
|
91
|
-
pjax_to(dest.replace("&&", "&").replace("?&", "?"));
|
|
93
|
+
if (checked) dest = get_current_state_url(e) + `${separator}${name}=${value}`;
|
|
94
|
+
else dest = get_current_state_url(e).replace(`${name}=${value}`, "");
|
|
95
|
+
pjax_to(dest.replace("&&", "&").replace("?&", "?"), e);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
function invalidate_pagings(href) {
|
|
95
99
|
let newhref = href;
|
|
96
|
-
const
|
|
100
|
+
const prev = new URL(window.location.href);
|
|
101
|
+
const queryObj = Object.fromEntries(
|
|
102
|
+
new URL(newhref, prev.origin).searchParams.entries()
|
|
103
|
+
);
|
|
97
104
|
const toRemove = Object.keys(queryObj).filter((val) => is_paging_param(val));
|
|
98
105
|
for (const k of toRemove) {
|
|
99
106
|
newhref = removeQueryStringParameter(newhref, k);
|
|
@@ -173,12 +180,12 @@ function pjax_to(href, e) {
|
|
|
173
180
|
function href_to(href) {
|
|
174
181
|
window.location.href = href;
|
|
175
182
|
}
|
|
176
|
-
function clear_state(omit_fields_str) {
|
|
177
|
-
let newUrl = get_current_state_url().split("?")[0];
|
|
178
|
-
const hash = get_current_state_url().split("#")[1];
|
|
183
|
+
function clear_state(omit_fields_str, e) {
|
|
184
|
+
let newUrl = get_current_state_url(e).split("?")[0];
|
|
185
|
+
const hash = get_current_state_url(e).split("#")[1];
|
|
179
186
|
if (omit_fields_str) {
|
|
180
187
|
const omit_fields = omit_fields_str.split(",").map((s) => s.trim());
|
|
181
|
-
let qs = (get_current_state_url().split("?")[1] || "").split("#")[0];
|
|
188
|
+
let qs = (get_current_state_url(e).split("?")[1] || "").split("#")[0];
|
|
182
189
|
let params = new URLSearchParams(qs);
|
|
183
190
|
newUrl = newUrl + "?";
|
|
184
191
|
omit_fields.forEach((f) => {
|
|
@@ -188,7 +195,7 @@ function clear_state(omit_fields_str) {
|
|
|
188
195
|
}
|
|
189
196
|
if (hash) newUrl += "#" + hash;
|
|
190
197
|
|
|
191
|
-
pjax_to(newUrl);
|
|
198
|
+
pjax_to(newUrl, e);
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
function ajax_done(res) {
|
|
@@ -389,6 +396,20 @@ function saveAndContinue(e, k) {
|
|
|
389
396
|
return false;
|
|
390
397
|
}
|
|
391
398
|
|
|
399
|
+
function updateMatchingRows(e, viewname) {
|
|
400
|
+
const form = $(e).closest("form");
|
|
401
|
+
try {
|
|
402
|
+
const sp = `${new URL(get_current_state_url()).searchParams.toString()}`;
|
|
403
|
+
form.attr(
|
|
404
|
+
"action",
|
|
405
|
+
`/view/${viewname}/update_matching_rows${sp ? `?${sp}` : ""}`
|
|
406
|
+
);
|
|
407
|
+
form[0].submit();
|
|
408
|
+
} finally {
|
|
409
|
+
form.attr("action", `/view/${viewname}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
392
413
|
function applyViewConfig(e, url, k) {
|
|
393
414
|
var form = $(e).closest("form");
|
|
394
415
|
var form_data = form.serializeArray();
|
|
@@ -755,6 +776,18 @@ function toggle_tbl_sync() {
|
|
|
755
776
|
}
|
|
756
777
|
}
|
|
757
778
|
|
|
779
|
+
function toggle_android_platform() {
|
|
780
|
+
if ($("#androidCheckboxId")[0].checked === true) {
|
|
781
|
+
$("#dockerCheckboxId").attr("hidden", false);
|
|
782
|
+
$("#dockerCheckboxId").attr("checked", true);
|
|
783
|
+
$("#dockerLabelId").removeClass("d-none");
|
|
784
|
+
} else {
|
|
785
|
+
$("#dockerCheckboxId").attr("hidden", true);
|
|
786
|
+
$("#dockerCheckboxId").attr("checked", false);
|
|
787
|
+
$("#dockerLabelId").addClass("d-none");
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
758
791
|
function join_field_clicked(e, fieldPath) {
|
|
759
792
|
$("#inputjoin_field").val(fieldPath);
|
|
760
793
|
apply_showif();
|
package/routes/admin.js
CHANGED
|
@@ -1535,7 +1535,9 @@ router.get(
|
|
|
1535
1535
|
div({ class: "col-sm-4 fw-bold" }, req.__("Platform")),
|
|
1536
1536
|
div(
|
|
1537
1537
|
{
|
|
1538
|
-
class:
|
|
1538
|
+
class:
|
|
1539
|
+
"col-sm-1 fw-bold d-flex justify-content-center d-none",
|
|
1540
|
+
id: "dockerLabelId",
|
|
1539
1541
|
},
|
|
1540
1542
|
req.__("docker")
|
|
1541
1543
|
)
|
|
@@ -1596,7 +1598,7 @@ router.get(
|
|
|
1596
1598
|
),
|
|
1597
1599
|
div(
|
|
1598
1600
|
{ class: "col-sm-4" },
|
|
1599
|
-
|
|
1601
|
+
// android
|
|
1600
1602
|
div(
|
|
1601
1603
|
{ class: "container ps-0" },
|
|
1602
1604
|
div(
|
|
@@ -1609,9 +1611,11 @@ router.get(
|
|
|
1609
1611
|
class: "form-check-input",
|
|
1610
1612
|
name: "androidPlatform",
|
|
1611
1613
|
id: "androidCheckboxId",
|
|
1614
|
+
onClick: "toggle_android_platform()",
|
|
1612
1615
|
})
|
|
1613
1616
|
)
|
|
1614
1617
|
),
|
|
1618
|
+
// iOS
|
|
1615
1619
|
div(
|
|
1616
1620
|
{ class: "row" },
|
|
1617
1621
|
div({ class: "col-sm-8" }, req.__("iOS")),
|
|
@@ -1627,6 +1631,7 @@ router.get(
|
|
|
1627
1631
|
)
|
|
1628
1632
|
)
|
|
1629
1633
|
),
|
|
1634
|
+
// android with docker
|
|
1630
1635
|
div(
|
|
1631
1636
|
{ class: "col-sm-1 d-flex justify-content-center" },
|
|
1632
1637
|
input({
|
|
@@ -1634,6 +1639,7 @@ router.get(
|
|
|
1634
1639
|
class: "form-check-input",
|
|
1635
1640
|
name: "useDocker",
|
|
1636
1641
|
id: "dockerCheckboxId",
|
|
1642
|
+
hidden: true,
|
|
1637
1643
|
})
|
|
1638
1644
|
)
|
|
1639
1645
|
),
|
package/routes/api.js
CHANGED
|
@@ -251,7 +251,8 @@ router.get(
|
|
|
251
251
|
//passport.authenticate("api-bearer", { session: false }),
|
|
252
252
|
error_catcher(async (req, res, next) => {
|
|
253
253
|
let { tableName } = req.params;
|
|
254
|
-
const { fields, versioncount, approximate, ...req_query } =
|
|
254
|
+
const { fields, versioncount, approximate, dereference, ...req_query } =
|
|
255
|
+
req.query;
|
|
255
256
|
const table = Table.findOne(
|
|
256
257
|
strictParseInt(tableName)
|
|
257
258
|
? { id: strictParseInt(tableName) }
|
|
@@ -284,7 +285,7 @@ router.get(
|
|
|
284
285
|
},
|
|
285
286
|
};
|
|
286
287
|
rows = await table.getJoinedRows(joinOpts);
|
|
287
|
-
} else
|
|
288
|
+
} else {
|
|
288
289
|
const tbl_fields = table.getFields();
|
|
289
290
|
readState(req_query, tbl_fields, req);
|
|
290
291
|
const qstate = await stateFieldsToWhere({
|
|
@@ -293,18 +294,26 @@ router.get(
|
|
|
293
294
|
state: req_query,
|
|
294
295
|
table,
|
|
295
296
|
});
|
|
296
|
-
|
|
297
|
+
const joinFields = {};
|
|
298
|
+
const derefs = Array.isArray(dereference)
|
|
299
|
+
? dereference
|
|
300
|
+
: !dereference
|
|
301
|
+
? []
|
|
302
|
+
: [dereference];
|
|
303
|
+
derefs.forEach((f) => {
|
|
304
|
+
const field = table.getField(f);
|
|
305
|
+
if (field?.attributes?.summary_field)
|
|
306
|
+
joinFields[`${f}_${field?.attributes?.summary_field}`] = {
|
|
307
|
+
ref: f,
|
|
308
|
+
target: field?.attributes?.summary_field,
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
rows = await table.getJoinedRows({
|
|
312
|
+
where: qstate,
|
|
313
|
+
joinFields,
|
|
297
314
|
forPublic: !(req.user || user),
|
|
298
315
|
forUser: req.user || user,
|
|
299
316
|
});
|
|
300
|
-
} else {
|
|
301
|
-
rows = await table.getRows(
|
|
302
|
-
{},
|
|
303
|
-
{
|
|
304
|
-
forPublic: !(req.user || user),
|
|
305
|
-
forUser: req.user || user,
|
|
306
|
-
}
|
|
307
|
-
);
|
|
308
317
|
}
|
|
309
318
|
res.json({ success: rows.map(limitFields(fields)) });
|
|
310
319
|
} else {
|
package/routes/common_lists.js
CHANGED
|
@@ -187,7 +187,7 @@ const viewsList = async (
|
|
|
187
187
|
label: req.__("Name"),
|
|
188
188
|
key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
|
|
189
189
|
sortlink: !tagId
|
|
190
|
-
? `
|
|
190
|
+
? `set_state_field('_sortby', 'name', this)`
|
|
191
191
|
: undefined,
|
|
192
192
|
},
|
|
193
193
|
// description - currently I dont want to show description in view list
|
|
@@ -205,7 +205,7 @@ const viewsList = async (
|
|
|
205
205
|
label: req.__("Pattern"),
|
|
206
206
|
key: "viewtemplate",
|
|
207
207
|
sortlink: !tagId
|
|
208
|
-
? `
|
|
208
|
+
? `set_state_field('_sortby', 'viewtemplate', this)`
|
|
209
209
|
: undefined,
|
|
210
210
|
},
|
|
211
211
|
...(notable
|
|
@@ -215,7 +215,7 @@ const viewsList = async (
|
|
|
215
215
|
label: req.__("Table"),
|
|
216
216
|
key: (r) => link(`/table/${r.table}`, r.table),
|
|
217
217
|
sortlink: !tagId
|
|
218
|
-
? `
|
|
218
|
+
? `set_state_field('_sortby', 'table', this)`
|
|
219
219
|
: undefined,
|
|
220
220
|
},
|
|
221
221
|
]),
|
package/routes/crashlog.js
CHANGED
package/routes/eventlog.js
CHANGED
|
@@ -180,6 +180,7 @@ const customEventForm = async (req) => {
|
|
|
180
180
|
name: "name",
|
|
181
181
|
label: req.__("Event Name"),
|
|
182
182
|
type: "String",
|
|
183
|
+
required: true,
|
|
183
184
|
},
|
|
184
185
|
{
|
|
185
186
|
name: "hasChannel",
|
|
@@ -256,11 +257,11 @@ router.post(
|
|
|
256
257
|
* @function
|
|
257
258
|
*/
|
|
258
259
|
router.post(
|
|
259
|
-
"/custom/delete/:name",
|
|
260
|
+
"/custom/delete/:name?",
|
|
260
261
|
isAdmin,
|
|
261
262
|
error_catcher(async (req, res) => {
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
let { name } = req.params;
|
|
264
|
+
if (!name) name = "";
|
|
264
265
|
const cevs = getState().getConfig("custom_events", []);
|
|
265
266
|
|
|
266
267
|
await getState().setConfig(
|
|
@@ -331,7 +332,7 @@ router.get(
|
|
|
331
332
|
page_opts.pagination = {
|
|
332
333
|
current_page,
|
|
333
334
|
pages: Math.ceil(nrows / rows_per_page),
|
|
334
|
-
get_page_link: (n) => `
|
|
335
|
+
get_page_link: (n) => `gopage(${n}, ${rows_per_page})`,
|
|
335
336
|
};
|
|
336
337
|
}
|
|
337
338
|
}
|
package/routes/search.js
CHANGED
|
@@ -201,7 +201,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
201
201
|
pages: current_page + (vresps.length === page_size ? 1 : 0),
|
|
202
202
|
trailing_ellipsis: vresps.length === page_size,
|
|
203
203
|
get_page_link: (n) =>
|
|
204
|
-
`
|
|
204
|
+
`gopage(${n}, ${page_size}, undefined, {table:'${tableName}'}, this)`,
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
|
package/routes/tables.js
CHANGED
|
@@ -90,7 +90,7 @@ const tableForm = async (table, req) => {
|
|
|
90
90
|
noSubmitButton: true,
|
|
91
91
|
onChange: "saveAndContinue(this)",
|
|
92
92
|
fields: [
|
|
93
|
-
...(!table.external
|
|
93
|
+
...(!table.external && !table.provider_name
|
|
94
94
|
? [
|
|
95
95
|
{
|
|
96
96
|
label: req.__("Ownership field"),
|
|
@@ -146,9 +146,9 @@ const tableForm = async (table, req) => {
|
|
|
146
146
|
name: "min_role_read",
|
|
147
147
|
input_type: "select",
|
|
148
148
|
options: roleOptions,
|
|
149
|
-
attributes: { asideNext: !table.external },
|
|
149
|
+
attributes: { asideNext: !table.external && !table.provider_name },
|
|
150
150
|
},
|
|
151
|
-
...(table.external
|
|
151
|
+
...(table.external || table.provider_name
|
|
152
152
|
? []
|
|
153
153
|
: [
|
|
154
154
|
{
|
|
@@ -790,6 +790,7 @@ router.get(
|
|
|
790
790
|
"<br>"
|
|
791
791
|
: "",
|
|
792
792
|
!table.external &&
|
|
793
|
+
!table.provider_name &&
|
|
793
794
|
a(
|
|
794
795
|
{
|
|
795
796
|
href: `/field/new/${table.id}`,
|
|
@@ -903,6 +904,7 @@ router.get(
|
|
|
903
904
|
)
|
|
904
905
|
),
|
|
905
906
|
!table.external &&
|
|
907
|
+
!table.provider_name &&
|
|
906
908
|
div(
|
|
907
909
|
{ class: "mx-auto" },
|
|
908
910
|
form(
|
|
@@ -929,6 +931,7 @@ router.get(
|
|
|
929
931
|
)
|
|
930
932
|
),
|
|
931
933
|
!table.external &&
|
|
934
|
+
!table.provider_name &&
|
|
932
935
|
div(
|
|
933
936
|
{ class: "mx-auto" },
|
|
934
937
|
a(
|
|
@@ -944,6 +947,7 @@ router.get(
|
|
|
944
947
|
|
|
945
948
|
// only if table is not external
|
|
946
949
|
!table.external &&
|
|
950
|
+
!table.provider_name &&
|
|
947
951
|
div(
|
|
948
952
|
{ class: "mx-auto" },
|
|
949
953
|
settingsDropdown(`dataMenuButton`, [
|
package/tests/api.test.js
CHANGED
|
@@ -127,6 +127,22 @@ describe("API read", () => {
|
|
|
127
127
|
.set("Cookie", loginCookie)
|
|
128
128
|
.expect(succeedJsonWith((rows) => rows.length == 2));
|
|
129
129
|
});
|
|
130
|
+
it("should dereference", async () => {
|
|
131
|
+
const loginCookie = await getStaffLoginCookie();
|
|
132
|
+
|
|
133
|
+
const app = await getApp({ disableCsrf: true });
|
|
134
|
+
await request(app)
|
|
135
|
+
.get("/api/patients/?dereference=favbook")
|
|
136
|
+
.set("Cookie", loginCookie)
|
|
137
|
+
.expect(
|
|
138
|
+
succeedJsonWith(
|
|
139
|
+
(rows) =>
|
|
140
|
+
rows.length == 2 &&
|
|
141
|
+
rows.find((r) => r.favbook === 1).favbook_author ==
|
|
142
|
+
"Herman Melville"
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
});
|
|
130
146
|
it("should add version counts", async () => {
|
|
131
147
|
const patients = Table.findOne({ name: "patients" });
|
|
132
148
|
await patients.update({ versioned: true });
|
package/tests/view.test.js
CHANGED
|
@@ -9,13 +9,12 @@ const {
|
|
|
9
9
|
toNotInclude,
|
|
10
10
|
resetToFixtures,
|
|
11
11
|
respondJsonWith,
|
|
12
|
+
toSucceed,
|
|
12
13
|
} = require("../auth/testhelp");
|
|
13
14
|
const db = require("@saltcorn/data/db");
|
|
14
15
|
const { getState } = require("@saltcorn/data/db/state");
|
|
15
16
|
const View = require("@saltcorn/data/models/view");
|
|
16
17
|
const Table = require("@saltcorn/data/models/table");
|
|
17
|
-
const Trigger = require("@saltcorn/data/models/trigger");
|
|
18
|
-
const Page = require("@saltcorn/data/models/page");
|
|
19
18
|
|
|
20
19
|
const { plugin_with_routes } = require("@saltcorn/data/tests/mocks");
|
|
21
20
|
|
|
@@ -397,6 +396,70 @@ describe("action row_variable", () => {
|
|
|
397
396
|
});
|
|
398
397
|
});
|
|
399
398
|
|
|
399
|
+
describe("update matching rows", () => {
|
|
400
|
+
const updateMatchingRows = async ({ query, body }) => {
|
|
401
|
+
const app = await getApp({ disableCsrf: true });
|
|
402
|
+
const loginCookie = await getAdminLoginCookie();
|
|
403
|
+
await request(app)
|
|
404
|
+
.post(
|
|
405
|
+
`/view/author_multi_edit/update_matching_rows${
|
|
406
|
+
query ? `?${query}` : ""
|
|
407
|
+
}`
|
|
408
|
+
)
|
|
409
|
+
.set("Cookie", loginCookie)
|
|
410
|
+
.send(body)
|
|
411
|
+
.set("Content-Type", "application/json")
|
|
412
|
+
.set("Accept", "application/json")
|
|
413
|
+
.expect(toSucceed(302));
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
beforeAll(async () => {
|
|
417
|
+
const table = Table.findOne({ name: "books" });
|
|
418
|
+
const field = table.getFields().find((f) => f.name === "author");
|
|
419
|
+
await field.update({ is_unique: false });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("update matching books normal", async () => {
|
|
423
|
+
const table = Table.findOne({ name: "books" });
|
|
424
|
+
await updateMatchingRows({
|
|
425
|
+
query: "author=leo&publisher=1",
|
|
426
|
+
body: { author: "new_author" },
|
|
427
|
+
});
|
|
428
|
+
let actualRows = await table.getRows({ author: "new_author" });
|
|
429
|
+
expect(actualRows.length).toBe(1);
|
|
430
|
+
await updateMatchingRows({
|
|
431
|
+
query: "_gte_pages=600",
|
|
432
|
+
body: { author: "more_than" },
|
|
433
|
+
});
|
|
434
|
+
actualRows = await table.getRows({ author: "more_than" });
|
|
435
|
+
expect(actualRows.length >= 2).toBe(true);
|
|
436
|
+
const expected = (await table.getRows()).map((row) => {
|
|
437
|
+
return { id: row.id, author: "agi", pages: 100, publisher: null };
|
|
438
|
+
});
|
|
439
|
+
await updateMatchingRows({
|
|
440
|
+
body: { author: "agi", pages: 100, publisher: null },
|
|
441
|
+
});
|
|
442
|
+
actualRows = await table.getRows({});
|
|
443
|
+
expect(actualRows).toEqual(expected);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("update matching books with edit-in-edit", async () => {
|
|
447
|
+
const disBooks = Table.findOne({ name: "discusses_books" });
|
|
448
|
+
await updateMatchingRows({
|
|
449
|
+
query: "id=2",
|
|
450
|
+
body: { author: "Leo Tolstoy" },
|
|
451
|
+
});
|
|
452
|
+
await updateMatchingRows({
|
|
453
|
+
query: "author=leo",
|
|
454
|
+
body: { author: "agi", discussant_0: "1", discussant_1: "2" },
|
|
455
|
+
});
|
|
456
|
+
const discBooksRows = (await disBooks.getRows({ book: 2 })).filter(
|
|
457
|
+
({ discussant }) => discussant === 1 || discussant === 2
|
|
458
|
+
);
|
|
459
|
+
expect(discBooksRows.length).toBe(2);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
400
463
|
describe("inbound relations", () => {
|
|
401
464
|
it("view with inbound relation", async () => {
|
|
402
465
|
const app = await getApp({ disableCsrf: true });
|