@saltcorn/server 0.9.6-beta.16 → 0.9.6-beta.17
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 +16 -3
- package/auth/testhelp.js +86 -0
- package/load_plugins.js +28 -4
- package/locales/en.json +12 -2
- package/package.json +9 -9
- package/routes/admin.js +175 -64
- package/routes/tenant.js +3 -2
- package/routes/utils.js +3 -5
- package/tests/edit.test.js +426 -0
- package/tests/filter.test.js +68 -0
- package/tests/jsdom.test.js +0 -159
package/auth/routes.js
CHANGED
|
@@ -1155,7 +1155,11 @@ router.get(
|
|
|
1155
1155
|
} else {
|
|
1156
1156
|
const auth = getState().auth_methods[method];
|
|
1157
1157
|
if (auth) {
|
|
1158
|
-
|
|
1158
|
+
const passportParams =
|
|
1159
|
+
typeof auth.parameters === "function"
|
|
1160
|
+
? auth.parameters(req)
|
|
1161
|
+
: auth.parameters;
|
|
1162
|
+
passport.authenticate(method, passportParams)(req, res, next);
|
|
1159
1163
|
} else {
|
|
1160
1164
|
req.flash(
|
|
1161
1165
|
"danger",
|
|
@@ -1189,7 +1193,11 @@ router.post(
|
|
|
1189
1193
|
const { method } = req.params;
|
|
1190
1194
|
const auth = getState().auth_methods[method];
|
|
1191
1195
|
if (auth) {
|
|
1192
|
-
|
|
1196
|
+
const passportParams =
|
|
1197
|
+
typeof auth.parameters === "function"
|
|
1198
|
+
? auth.parameters(req)
|
|
1199
|
+
: auth.parameters;
|
|
1200
|
+
passport.authenticate(method, passportParams)(
|
|
1193
1201
|
req,
|
|
1194
1202
|
res,
|
|
1195
1203
|
loginCallback(req, res)
|
|
@@ -1227,7 +1235,12 @@ const callbackFn = async (req, res, next) => {
|
|
|
1227
1235
|
const { method } = req.params;
|
|
1228
1236
|
const auth = getState().auth_methods[method];
|
|
1229
1237
|
if (auth) {
|
|
1230
|
-
|
|
1238
|
+
const passportParams =
|
|
1239
|
+
typeof auth.parameters === "function"
|
|
1240
|
+
? auth.parameters(req)
|
|
1241
|
+
: auth.parameters;
|
|
1242
|
+
passportParams.failureRedirect = "/auth/login";
|
|
1243
|
+
passport.authenticate(method, passportParams)(
|
|
1231
1244
|
req,
|
|
1232
1245
|
res,
|
|
1233
1246
|
loginCallback(req, res)
|
package/auth/testhelp.js
CHANGED
|
@@ -9,6 +9,8 @@ const app = require("../app");
|
|
|
9
9
|
const getApp = require("../app");
|
|
10
10
|
const fixtures = require("@saltcorn/data/db/fixtures");
|
|
11
11
|
const reset = require("@saltcorn/data/db/reset_schema");
|
|
12
|
+
const jsdom = require("jsdom");
|
|
13
|
+
const { JSDOM, ResourceLoader } = jsdom;
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
*
|
|
@@ -307,6 +309,89 @@ const notFound = (res) => {
|
|
|
307
309
|
}
|
|
308
310
|
};
|
|
309
311
|
|
|
312
|
+
const load_url_dom = async (url) => {
|
|
313
|
+
const app = await getApp({ disableCsrf: true });
|
|
314
|
+
class CustomResourceLoader extends ResourceLoader {
|
|
315
|
+
async fetch(url, options) {
|
|
316
|
+
const url1 = url.replace("http://localhost", "");
|
|
317
|
+
//console.log("fetching", url, url1);
|
|
318
|
+
const res = await request(app).get(url1);
|
|
319
|
+
|
|
320
|
+
return Buffer.from(res.text);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const reqres = await request(app).get(url);
|
|
324
|
+
//console.log("rr1", reqres.text);
|
|
325
|
+
const virtualConsole = new jsdom.VirtualConsole();
|
|
326
|
+
virtualConsole.sendTo(console);
|
|
327
|
+
const dom = new JSDOM(reqres.text, {
|
|
328
|
+
url: "http://localhost" + url,
|
|
329
|
+
runScripts: "dangerously",
|
|
330
|
+
resources: new CustomResourceLoader(),
|
|
331
|
+
pretendToBeVisual: true,
|
|
332
|
+
virtualConsole,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
class FakeXHR {
|
|
336
|
+
constructor() {
|
|
337
|
+
this.readyState = 0;
|
|
338
|
+
this.requestHeaders = [];
|
|
339
|
+
//return traceMethodCalls(this);
|
|
340
|
+
}
|
|
341
|
+
open(method, url) {
|
|
342
|
+
//console.log("open xhr", method, url);
|
|
343
|
+
this.method = method;
|
|
344
|
+
this.url = url;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
addEventListener(ev, reqListener) {
|
|
348
|
+
if (ev === "load") this.reqListener = reqListener;
|
|
349
|
+
}
|
|
350
|
+
setRequestHeader(k, v) {
|
|
351
|
+
this.requestHeaders.push([k, v]);
|
|
352
|
+
}
|
|
353
|
+
overrideMimeType() {}
|
|
354
|
+
async send(body) {
|
|
355
|
+
//console.log("send1", this.url);
|
|
356
|
+
const url1 = this.url.replace("http://localhost", "");
|
|
357
|
+
//console.log("xhr fetching", url1);
|
|
358
|
+
let req =
|
|
359
|
+
this.method == "POST"
|
|
360
|
+
? request(app).post(url1)
|
|
361
|
+
: request(app).get(url1);
|
|
362
|
+
for (const [k, v] of this.requestHeaders) {
|
|
363
|
+
req = req.set(k, v);
|
|
364
|
+
}
|
|
365
|
+
if (this.method === "POST" && body) req.send(body);
|
|
366
|
+
const res = await req;
|
|
367
|
+
this.responseHeaders = res.headers;
|
|
368
|
+
if (res.headers["content-type"].includes("json"))
|
|
369
|
+
this.responseType = "json";
|
|
370
|
+
this.response = res.text;
|
|
371
|
+
this.responseText = res.text;
|
|
372
|
+
this.status = res.status;
|
|
373
|
+
this.statusText = "OK";
|
|
374
|
+
this.readyState = 4;
|
|
375
|
+
if (this.reqListener) this.reqListener(res.text);
|
|
376
|
+
if (this.onload) this.onload(res.text);
|
|
377
|
+
//console.log("agent res", res);
|
|
378
|
+
//console.log("xhr", this);
|
|
379
|
+
}
|
|
380
|
+
getAllResponseHeaders() {
|
|
381
|
+
return Object.entries(this.responseHeaders)
|
|
382
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
383
|
+
.join("\n");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
dom.window.XMLHttpRequest = FakeXHR;
|
|
387
|
+
await new Promise(function (resolve, reject) {
|
|
388
|
+
dom.window.addEventListener("DOMContentLoaded", (event) => {
|
|
389
|
+
resolve();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
return dom;
|
|
393
|
+
};
|
|
394
|
+
|
|
310
395
|
module.exports = {
|
|
311
396
|
getStaffLoginCookie,
|
|
312
397
|
getAdminLoginCookie,
|
|
@@ -328,4 +413,5 @@ module.exports = {
|
|
|
328
413
|
succeedJsonWithWholeBody,
|
|
329
414
|
resToLoginCookie,
|
|
330
415
|
itShouldIncludeTextForAdmin,
|
|
416
|
+
load_url_dom,
|
|
331
417
|
};
|
package/load_plugins.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
const db = require("@saltcorn/data/db");
|
|
9
9
|
const { getState, getRootState } = require("@saltcorn/data/db/state");
|
|
10
10
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
11
|
+
const { isRoot } = require("@saltcorn/data/utils");
|
|
12
|
+
const { eachTenant } = require("@saltcorn/admin-models/models/tenant");
|
|
11
13
|
|
|
12
14
|
const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
|
|
13
15
|
|
|
@@ -60,9 +62,23 @@ const loadPlugin = async (plugin, force) => {
|
|
|
60
62
|
console.error(error); // todo i think that situation is not resolved
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
|
|
66
|
+
if (isRoot() && res.plugin_module.authentication)
|
|
67
|
+
await eachTenant(reloadAuthFromRoot);
|
|
63
68
|
return res;
|
|
64
69
|
};
|
|
65
70
|
|
|
71
|
+
const reloadAuthFromRoot = () => {
|
|
72
|
+
if (isRoot()) return;
|
|
73
|
+
const rootState = getRootState();
|
|
74
|
+
const tenantState = getState();
|
|
75
|
+
if (!rootState || !tenantState || rootState === tenantState) return;
|
|
76
|
+
tenantState.auth_methods = {};
|
|
77
|
+
for (const [k, v] of Object.entries(rootState.auth_methods)) {
|
|
78
|
+
if (v.shareWithTenants) tenantState.auth_methods[k] = v;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
66
82
|
/**
|
|
67
83
|
* Install plugin
|
|
68
84
|
* @param plugin - plugin name
|
|
@@ -90,6 +106,7 @@ const loadAllPlugins = async (force) => {
|
|
|
90
106
|
}
|
|
91
107
|
await getState().refreshUserLayouts();
|
|
92
108
|
await getState().refresh(true);
|
|
109
|
+
if (!isRoot()) reloadAuthFromRoot();
|
|
93
110
|
};
|
|
94
111
|
|
|
95
112
|
/**
|
|
@@ -104,14 +121,14 @@ const loadAndSaveNewPlugin = async (
|
|
|
104
121
|
plugin,
|
|
105
122
|
force,
|
|
106
123
|
noSignalOrDB,
|
|
107
|
-
__ = (str) => str
|
|
124
|
+
__ = (str) => str,
|
|
125
|
+
allowUnsafeOnTenantsWithoutConfigSetting
|
|
108
126
|
) => {
|
|
109
127
|
const tenants_unsafe_plugins = getRootState().getConfig(
|
|
110
128
|
"tenants_unsafe_plugins",
|
|
111
129
|
false
|
|
112
130
|
);
|
|
113
|
-
|
|
114
|
-
if (!isRoot && !tenants_unsafe_plugins) {
|
|
131
|
+
if (!isRoot() && !tenants_unsafe_plugins) {
|
|
115
132
|
if (plugin.source !== "npm") {
|
|
116
133
|
console.error("\nWARNING: Skipping unsafe plugin ", plugin.name);
|
|
117
134
|
return;
|
|
@@ -126,7 +143,10 @@ const loadAndSaveNewPlugin = async (
|
|
|
126
143
|
|
|
127
144
|
const instore = getRootState().getConfig("available_plugins", []);
|
|
128
145
|
const safes = instore.filter((p) => !p.unsafe).map((p) => p.location);
|
|
129
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
!safes.includes(plugin.location) &&
|
|
148
|
+
!allowUnsafeOnTenantsWithoutConfigSetting
|
|
149
|
+
) {
|
|
130
150
|
console.error("\nWARNING: Skipping unsafe plugin ", plugin.name);
|
|
131
151
|
return;
|
|
132
152
|
}
|
|
@@ -196,6 +216,10 @@ const loadAndSaveNewPlugin = async (
|
|
|
196
216
|
}
|
|
197
217
|
}
|
|
198
218
|
if (version) plugin.version = version;
|
|
219
|
+
|
|
220
|
+
if (isRoot() && plugin_module.authentication)
|
|
221
|
+
await eachTenant(reloadAuthFromRoot);
|
|
222
|
+
|
|
199
223
|
if (!noSignalOrDB) {
|
|
200
224
|
await plugin.upsert();
|
|
201
225
|
getState().processSend({
|
package/locales/en.json
CHANGED
|
@@ -1438,5 +1438,15 @@
|
|
|
1438
1438
|
"Delete all read": "Delete all read",
|
|
1439
1439
|
"Trigger %s duplicated as %s": "Trigger %s duplicated as %s",
|
|
1440
1440
|
"Tooltip": "Tooltip",
|
|
1441
|
-
"Tooltip formula": "Tooltip formula"
|
|
1442
|
-
|
|
1441
|
+
"Tooltip formula": "Tooltip formula",
|
|
1442
|
+
"Install a different version": "Install a different version",
|
|
1443
|
+
"Page group": "Page group",
|
|
1444
|
+
"Starting upgrade, please wait...\n": "Starting upgrade, please wait...\n",
|
|
1445
|
+
"Upgrade done (if it was available) with code 0.\n\nPress the BACK button in your browser, then RELOAD the page.": "Upgrade done (if it was available) with code 0.\n\nPress the BACK button in your browser, then RELOAD the page.",
|
|
1446
|
+
"Installing %s, please wait...\n": "Installing %s, please wait...\n",
|
|
1447
|
+
"Install done with code 0.\n\nPress the BACK button in your browser, then RELOAD the page.": "Install done with code 0.\n\nPress the BACK button in your browser, then RELOAD the page.",
|
|
1448
|
+
"Pulling the cordova-builder docker image...": "Pulling the cordova-builder docker image...",
|
|
1449
|
+
"Check updates": "Check updates",
|
|
1450
|
+
"Choose version": "Choose version",
|
|
1451
|
+
"Unknown authentication method %s": "Unknown authentication method %s"
|
|
1452
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.6-beta.
|
|
3
|
+
"version": "0.9.6-beta.17",
|
|
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": "0.9.6-beta.
|
|
11
|
-
"@saltcorn/builder": "0.9.6-beta.
|
|
12
|
-
"@saltcorn/data": "0.9.6-beta.
|
|
13
|
-
"@saltcorn/admin-models": "0.9.6-beta.
|
|
14
|
-
"@saltcorn/filemanager": "0.9.6-beta.
|
|
15
|
-
"@saltcorn/markup": "0.9.6-beta.
|
|
16
|
-
"@saltcorn/plugins-loader": "0.9.6-beta.
|
|
17
|
-
"@saltcorn/sbadmin2": "0.9.6-beta.
|
|
10
|
+
"@saltcorn/base-plugin": "0.9.6-beta.17",
|
|
11
|
+
"@saltcorn/builder": "0.9.6-beta.17",
|
|
12
|
+
"@saltcorn/data": "0.9.6-beta.17",
|
|
13
|
+
"@saltcorn/admin-models": "0.9.6-beta.17",
|
|
14
|
+
"@saltcorn/filemanager": "0.9.6-beta.17",
|
|
15
|
+
"@saltcorn/markup": "0.9.6-beta.17",
|
|
16
|
+
"@saltcorn/plugins-loader": "0.9.6-beta.17",
|
|
17
|
+
"@saltcorn/sbadmin2": "0.9.6-beta.17",
|
|
18
18
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
19
19
|
"@socket.io/sticky": "^1.0.1",
|
|
20
20
|
"adm-zip": "0.5.10",
|
package/routes/admin.js
CHANGED
|
@@ -108,6 +108,7 @@ const stream = require("stream");
|
|
|
108
108
|
const Crash = require("@saltcorn/data/models/crash");
|
|
109
109
|
const { get_help_markup } = require("../help/index.js");
|
|
110
110
|
const Docker = require("dockerode");
|
|
111
|
+
const npmFetch = require("npm-registry-fetch");
|
|
111
112
|
|
|
112
113
|
const router = new Router();
|
|
113
114
|
module.exports = router;
|
|
@@ -1004,6 +1005,7 @@ router.get(
|
|
|
1004
1005
|
"custom_ssl_certificate",
|
|
1005
1006
|
false
|
|
1006
1007
|
);
|
|
1008
|
+
const rndid = `bs${Math.round(Math.random() * 100000)}`;
|
|
1007
1009
|
let expiry = "";
|
|
1008
1010
|
if (custom_ssl_certificate && X509Certificate) {
|
|
1009
1011
|
const { validTo } = new X509Certificate(custom_ssl_certificate);
|
|
@@ -1062,7 +1064,8 @@ router.get(
|
|
|
1062
1064
|
" ",
|
|
1063
1065
|
req.__("Clear all"),
|
|
1064
1066
|
" »"
|
|
1065
|
-
)
|
|
1067
|
+
),
|
|
1068
|
+
hr()
|
|
1066
1069
|
),
|
|
1067
1070
|
},
|
|
1068
1071
|
{
|
|
@@ -1075,32 +1078,45 @@ router.get(
|
|
|
1075
1078
|
tr(
|
|
1076
1079
|
th(req.__("Saltcorn version")),
|
|
1077
1080
|
td(
|
|
1078
|
-
packagejson.version
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1081
|
+
packagejson.version,
|
|
1082
|
+
isRoot && can_update
|
|
1083
|
+
? post_btn(
|
|
1084
|
+
"/admin/upgrade",
|
|
1085
|
+
req.__("Upgrade"),
|
|
1086
|
+
req.csrfToken(),
|
|
1087
|
+
{
|
|
1088
|
+
btnClass: "btn-primary btn-sm",
|
|
1089
|
+
formClass: "d-inline",
|
|
1090
|
+
}
|
|
1091
|
+
)
|
|
1092
|
+
: isRoot && is_latest
|
|
1093
|
+
? span(
|
|
1094
|
+
{ class: "badge bg-primary ms-2" },
|
|
1095
|
+
req.__("Latest")
|
|
1096
|
+
) +
|
|
1097
|
+
post_btn(
|
|
1098
|
+
"/admin/check-for-upgrade",
|
|
1099
|
+
req.__("Check updates"),
|
|
1100
|
+
req.csrfToken(),
|
|
1101
|
+
{
|
|
1102
|
+
btnClass: "btn-primary btn-sm px-1 py-0",
|
|
1103
|
+
formClass: "d-inline",
|
|
1104
|
+
}
|
|
1105
|
+
)
|
|
1106
|
+
: "",
|
|
1107
|
+
!git_commit &&
|
|
1108
|
+
a(
|
|
1109
|
+
{
|
|
1110
|
+
id: rndid,
|
|
1111
|
+
class: "btn btn-sm btn-secondary ms-1 px-1 py-0",
|
|
1112
|
+
onClick: "press_store_button(this, true)",
|
|
1113
|
+
href:
|
|
1114
|
+
`javascript:ajax_modal('/admin/install_dialog', ` +
|
|
1115
|
+
`{ onOpen: () => { restore_old_button('${rndid}'); }, ` +
|
|
1116
|
+
` onError: (res) => { selectVersionError(res, '${rndid}') } });`,
|
|
1117
|
+
},
|
|
1118
|
+
req.__("Choose version")
|
|
1119
|
+
)
|
|
1104
1120
|
)
|
|
1105
1121
|
),
|
|
1106
1122
|
git_commit &&
|
|
@@ -1222,6 +1238,137 @@ const pullCordovaBuilder = (req, res) => {
|
|
|
1222
1238
|
});
|
|
1223
1239
|
};
|
|
1224
1240
|
|
|
1241
|
+
/*
|
|
1242
|
+
* fetch available saltcorn versions and show a dialog to select one
|
|
1243
|
+
*/
|
|
1244
|
+
router.get(
|
|
1245
|
+
"/install_dialog",
|
|
1246
|
+
isAdmin,
|
|
1247
|
+
error_catcher(async (req, res) => {
|
|
1248
|
+
try {
|
|
1249
|
+
const pkgInfo = await npmFetch.json(
|
|
1250
|
+
"https://registry.npmjs.org/@saltcorn/cli"
|
|
1251
|
+
);
|
|
1252
|
+
if (!pkgInfo?.versions)
|
|
1253
|
+
throw new Error(req.__("Unable to fetch versions"));
|
|
1254
|
+
const versions = Object.keys(pkgInfo.versions);
|
|
1255
|
+
if (versions.length === 0) throw new Error(req.__("No versions found"));
|
|
1256
|
+
res.set("Page-Title", req.__("%s versions", "Saltcorn"));
|
|
1257
|
+
let selected = packagejson.version;
|
|
1258
|
+
res.send(
|
|
1259
|
+
form(
|
|
1260
|
+
{
|
|
1261
|
+
action: `/admin/install`,
|
|
1262
|
+
method: "post",
|
|
1263
|
+
},
|
|
1264
|
+
input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
|
|
1265
|
+
div(
|
|
1266
|
+
{ class: "form-group" },
|
|
1267
|
+
label(
|
|
1268
|
+
{
|
|
1269
|
+
for: "version_select",
|
|
1270
|
+
class: "form-label fw-bold",
|
|
1271
|
+
},
|
|
1272
|
+
req.__("Version")
|
|
1273
|
+
),
|
|
1274
|
+
select(
|
|
1275
|
+
{
|
|
1276
|
+
id: "version_select",
|
|
1277
|
+
class: "form-control form-select",
|
|
1278
|
+
name: "version",
|
|
1279
|
+
},
|
|
1280
|
+
versions.map((version) =>
|
|
1281
|
+
option({
|
|
1282
|
+
id: `${version}_opt`,
|
|
1283
|
+
value: version,
|
|
1284
|
+
label: version,
|
|
1285
|
+
selected: version === selected,
|
|
1286
|
+
})
|
|
1287
|
+
)
|
|
1288
|
+
)
|
|
1289
|
+
),
|
|
1290
|
+
div(
|
|
1291
|
+
{ class: "d-flex justify-content-end" },
|
|
1292
|
+
button(
|
|
1293
|
+
{
|
|
1294
|
+
type: "button",
|
|
1295
|
+
class: "btn btn-secondary me-2",
|
|
1296
|
+
"data-bs-dismiss": "modal",
|
|
1297
|
+
},
|
|
1298
|
+
req.__("Close")
|
|
1299
|
+
),
|
|
1300
|
+
button(
|
|
1301
|
+
{
|
|
1302
|
+
type: "submit",
|
|
1303
|
+
class: "btn btn-primary",
|
|
1304
|
+
onClick: "press_store_button(this)",
|
|
1305
|
+
},
|
|
1306
|
+
req.__("Install")
|
|
1307
|
+
)
|
|
1308
|
+
)
|
|
1309
|
+
)
|
|
1310
|
+
);
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
getState().log(
|
|
1313
|
+
2,
|
|
1314
|
+
`GET /install_dialog: ${error.message || "unknown error"}`
|
|
1315
|
+
);
|
|
1316
|
+
return res.status(500).json({ error: error.message || "unknown error" });
|
|
1317
|
+
}
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
const doInstall = async (req, res, version, runPull) => {
|
|
1322
|
+
if (db.getTenantSchema() !== db.connectObj.default_schema) {
|
|
1323
|
+
req.flash("error", req.__("Not possible for tenant"));
|
|
1324
|
+
res.redirect("/admin");
|
|
1325
|
+
} else {
|
|
1326
|
+
res.write(
|
|
1327
|
+
version === "latest"
|
|
1328
|
+
? req.__("Starting upgrade, please wait...\n")
|
|
1329
|
+
: req.__("Installing %s, please wait...\n", version)
|
|
1330
|
+
);
|
|
1331
|
+
const child = spawn(
|
|
1332
|
+
"npm",
|
|
1333
|
+
["install", "-g", `@saltcorn/cli@${version}`, "--unsafe"],
|
|
1334
|
+
{
|
|
1335
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1336
|
+
}
|
|
1337
|
+
);
|
|
1338
|
+
child.stdout.on("data", (data) => {
|
|
1339
|
+
res.write(data);
|
|
1340
|
+
});
|
|
1341
|
+
child.stderr?.on("data", (data) => {
|
|
1342
|
+
res.write(data);
|
|
1343
|
+
});
|
|
1344
|
+
child.on("exit", async function (code, signal) {
|
|
1345
|
+
if (code === 0 && runPull) {
|
|
1346
|
+
res.write(req.__("Pulling the cordova-builder docker image...") + "\n");
|
|
1347
|
+
const pullCode = await pullCordovaBuilder(req, res);
|
|
1348
|
+
res.write(req.__("Pull done with code %s", pullCode) + "\n");
|
|
1349
|
+
}
|
|
1350
|
+
res.end(
|
|
1351
|
+
version === "latest"
|
|
1352
|
+
? req.__(
|
|
1353
|
+
`Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
|
|
1354
|
+
)
|
|
1355
|
+
: req.__(
|
|
1356
|
+
`Install done with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
|
|
1357
|
+
)
|
|
1358
|
+
);
|
|
1359
|
+
setTimeout(() => {
|
|
1360
|
+
getState().processSend("RestartServer");
|
|
1361
|
+
process.exit(0);
|
|
1362
|
+
}, 100);
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
router.post("/install", isAdmin, async (req, res) => {
|
|
1368
|
+
const { version } = req.body;
|
|
1369
|
+
await doInstall(req, res, version, false);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1225
1372
|
/**
|
|
1226
1373
|
* Do Upgrade
|
|
1227
1374
|
* @name post/upgrade
|
|
@@ -1232,43 +1379,7 @@ router.post(
|
|
|
1232
1379
|
"/upgrade",
|
|
1233
1380
|
isAdmin,
|
|
1234
1381
|
error_catcher(async (req, res) => {
|
|
1235
|
-
|
|
1236
|
-
req.flash("error", req.__("Not possible for tenant"));
|
|
1237
|
-
res.redirect("/admin");
|
|
1238
|
-
} else {
|
|
1239
|
-
res.write(req.__("Starting upgrade, please wait...\n"));
|
|
1240
|
-
const child = spawn(
|
|
1241
|
-
"npm",
|
|
1242
|
-
["install", "-g", "@saltcorn/cli@latest", "--unsafe"],
|
|
1243
|
-
{
|
|
1244
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1245
|
-
}
|
|
1246
|
-
);
|
|
1247
|
-
child.stdout.on("data", (data) => {
|
|
1248
|
-
res.write(data);
|
|
1249
|
-
});
|
|
1250
|
-
child.stderr?.on("data", (data) => {
|
|
1251
|
-
res.write(data);
|
|
1252
|
-
});
|
|
1253
|
-
child.on("exit", async function (code, signal) {
|
|
1254
|
-
if (code === 0) {
|
|
1255
|
-
res.write(
|
|
1256
|
-
req.__("Pulling the cordova-builder docker image...") + "\n"
|
|
1257
|
-
);
|
|
1258
|
-
const pullCode = await pullCordovaBuilder(req, res);
|
|
1259
|
-
res.write(req.__("Pull done with code %s", pullCode) + "\n");
|
|
1260
|
-
}
|
|
1261
|
-
res.end(
|
|
1262
|
-
req.__(
|
|
1263
|
-
`Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
|
|
1264
|
-
)
|
|
1265
|
-
);
|
|
1266
|
-
setTimeout(() => {
|
|
1267
|
-
getState().processSend("RestartServer");
|
|
1268
|
-
process.exit(0);
|
|
1269
|
-
}, 100);
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1382
|
+
await doInstall(req, res, "latest", true);
|
|
1272
1383
|
})
|
|
1273
1384
|
);
|
|
1274
1385
|
/**
|
package/routes/tenant.js
CHANGED
|
@@ -132,7 +132,7 @@ const is_ip_address = (hostname) => {
|
|
|
132
132
|
const get_cfg_tenant_base_url = (req) =>
|
|
133
133
|
remove_leading_chars(
|
|
134
134
|
".",
|
|
135
|
-
getRootState().getConfig("tenant_baseurl", req.hostname)
|
|
135
|
+
getRootState().getConfig("tenant_baseurl", req.hostname) || req.hostname
|
|
136
136
|
)
|
|
137
137
|
.replace("http://", "")
|
|
138
138
|
.replace("https://", "");
|
|
@@ -268,7 +268,8 @@ router.post(
|
|
|
268
268
|
return;
|
|
269
269
|
}
|
|
270
270
|
// declare ui form
|
|
271
|
-
const
|
|
271
|
+
const base_url = get_cfg_tenant_base_url(req);
|
|
272
|
+
const form = tenant_form(req, base_url);
|
|
272
273
|
// validate ui form
|
|
273
274
|
const valres = form.validate(req.body);
|
|
274
275
|
if (valres.errors)
|
package/routes/utils.js
CHANGED
|
@@ -77,11 +77,9 @@ function loggedIn(req, res, next) {
|
|
|
77
77
|
* @returns {void}
|
|
78
78
|
*/
|
|
79
79
|
function isAdmin(req, res, next) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
req.user.tenant === db.getTenantSchema()
|
|
84
|
-
) {
|
|
80
|
+
const cur_tenant = db.getTenantSchema();
|
|
81
|
+
//console.log({ cur_tenant, user: req.user });
|
|
82
|
+
if (req.user && req.user.role_id === 1 && req.user.tenant === cur_tenant) {
|
|
85
83
|
next();
|
|
86
84
|
} else {
|
|
87
85
|
req.flash("danger", req.__("Must be admin"));
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
const request = require("supertest");
|
|
2
|
+
const getApp = require("../app");
|
|
3
|
+
const { resetToFixtures, load_url_dom } = require("../auth/testhelp");
|
|
4
|
+
const db = require("@saltcorn/data/db");
|
|
5
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
6
|
+
const View = require("@saltcorn/data/models/view");
|
|
7
|
+
const Field = require("@saltcorn/data/models/field");
|
|
8
|
+
const Table = require("@saltcorn/data/models/table");
|
|
9
|
+
const { plugin_with_routes, sleep } = require("@saltcorn/data/tests/mocks");
|
|
10
|
+
|
|
11
|
+
afterAll(db.close);
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
await resetToFixtures();
|
|
14
|
+
const table = Table.findOne("books");
|
|
15
|
+
await table.update({ min_role_read: 100 });
|
|
16
|
+
await Field.create({
|
|
17
|
+
table,
|
|
18
|
+
name: "sequel_to",
|
|
19
|
+
type: "Key to books",
|
|
20
|
+
attributes: { summary_field: "author" },
|
|
21
|
+
});
|
|
22
|
+
await Field.create({
|
|
23
|
+
table,
|
|
24
|
+
label: "pagesp1",
|
|
25
|
+
type: "Integer",
|
|
26
|
+
calculated: true,
|
|
27
|
+
expression: "pages+1",
|
|
28
|
+
});
|
|
29
|
+
await table.insertRow({
|
|
30
|
+
author: "Peter Kropotkin",
|
|
31
|
+
pages: 189,
|
|
32
|
+
publisher: 1,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await table.insertRow({
|
|
36
|
+
author: "Mary Boas",
|
|
37
|
+
pages: 864,
|
|
38
|
+
publisher: 2,
|
|
39
|
+
});
|
|
40
|
+
const ptable = Table.findOne("publisher");
|
|
41
|
+
await ptable.update({ min_role_read: 100 });
|
|
42
|
+
|
|
43
|
+
//await getState().setConfig("log_level", 6);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
jest.setTimeout(30000);
|
|
47
|
+
|
|
48
|
+
const makeJoinSelectView = async ({ name, showIfFormula }) => {
|
|
49
|
+
await View.create({
|
|
50
|
+
viewtemplate: "Edit",
|
|
51
|
+
description: "",
|
|
52
|
+
min_role: 100,
|
|
53
|
+
name,
|
|
54
|
+
table_id: Table.findOne("books")?.id,
|
|
55
|
+
default_render_page: "",
|
|
56
|
+
slug: {},
|
|
57
|
+
attributes: {},
|
|
58
|
+
configuration: {
|
|
59
|
+
layout: {
|
|
60
|
+
above: [
|
|
61
|
+
{
|
|
62
|
+
gx: null,
|
|
63
|
+
gy: null,
|
|
64
|
+
style: {
|
|
65
|
+
"margin-bottom": "1.5rem",
|
|
66
|
+
},
|
|
67
|
+
aligns: ["end", "start"],
|
|
68
|
+
widths: [2, 10],
|
|
69
|
+
besides: [
|
|
70
|
+
{
|
|
71
|
+
font: "",
|
|
72
|
+
type: "blank",
|
|
73
|
+
block: false,
|
|
74
|
+
style: {},
|
|
75
|
+
inline: false,
|
|
76
|
+
contents: "Publisher",
|
|
77
|
+
labelFor: "publisher",
|
|
78
|
+
isFormula: {},
|
|
79
|
+
textStyle: "",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
above: [
|
|
83
|
+
{
|
|
84
|
+
type: "field",
|
|
85
|
+
block: false,
|
|
86
|
+
fieldview: "select",
|
|
87
|
+
textStyle: "",
|
|
88
|
+
field_name: "publisher",
|
|
89
|
+
configuration: {},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: "container",
|
|
93
|
+
style: {},
|
|
94
|
+
bgType: "None",
|
|
95
|
+
hAlign: "left",
|
|
96
|
+
margin: [0, 0, 0, 0],
|
|
97
|
+
rotate: 0,
|
|
98
|
+
vAlign: "top",
|
|
99
|
+
bgColor: "#ffffff",
|
|
100
|
+
display: "block",
|
|
101
|
+
padding: [0, 0, 0, 0],
|
|
102
|
+
bgFileId: 0,
|
|
103
|
+
contents: {
|
|
104
|
+
above: [
|
|
105
|
+
{
|
|
106
|
+
font: "",
|
|
107
|
+
icon: "",
|
|
108
|
+
type: "blank",
|
|
109
|
+
block: false,
|
|
110
|
+
style: {},
|
|
111
|
+
inline: false,
|
|
112
|
+
contents: "Warning",
|
|
113
|
+
labelFor: "",
|
|
114
|
+
isFormula: {},
|
|
115
|
+
textStyle: "",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: "join_field",
|
|
119
|
+
block: false,
|
|
120
|
+
fieldview: "as_text",
|
|
121
|
+
textStyle: "",
|
|
122
|
+
join_field: "publisher.name",
|
|
123
|
+
configuration: {},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
imageSize: "contain",
|
|
128
|
+
isFormula: {},
|
|
129
|
+
minHeight: 0,
|
|
130
|
+
textColor: "#ffffff",
|
|
131
|
+
widthUnit: "px",
|
|
132
|
+
heightUnit: "px",
|
|
133
|
+
customClass: "pubwarn",
|
|
134
|
+
htmlElement: "div",
|
|
135
|
+
showForRole: [],
|
|
136
|
+
gradEndColor: "#88ff88",
|
|
137
|
+
setTextColor: false,
|
|
138
|
+
fullPageWidth: false,
|
|
139
|
+
gradDirection: "0",
|
|
140
|
+
minHeightUnit: "px",
|
|
141
|
+
showIfFormula,
|
|
142
|
+
gradStartColor: "#ff8888",
|
|
143
|
+
maxScreenWidth: "",
|
|
144
|
+
minScreenWidth: "",
|
|
145
|
+
show_for_owner: false,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
breakpoints: ["", ""],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
gx: null,
|
|
154
|
+
gy: null,
|
|
155
|
+
style: {
|
|
156
|
+
"margin-bottom": "1.5rem",
|
|
157
|
+
},
|
|
158
|
+
aligns: ["end", "start"],
|
|
159
|
+
widths: [2, 10],
|
|
160
|
+
besides: [
|
|
161
|
+
{
|
|
162
|
+
font: "",
|
|
163
|
+
type: "blank",
|
|
164
|
+
block: false,
|
|
165
|
+
style: {},
|
|
166
|
+
inline: false,
|
|
167
|
+
contents: "sequel_to",
|
|
168
|
+
labelFor: "sequel_to",
|
|
169
|
+
isFormula: {},
|
|
170
|
+
textStyle: "",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: "field",
|
|
174
|
+
block: false,
|
|
175
|
+
fieldview: "select",
|
|
176
|
+
textStyle: "",
|
|
177
|
+
field_name: "sequel_to",
|
|
178
|
+
configuration: {
|
|
179
|
+
where: "publisher == $publisher",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
breakpoints: ["", ""],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
gx: null,
|
|
187
|
+
gy: null,
|
|
188
|
+
style: {
|
|
189
|
+
"margin-bottom": "1.5rem",
|
|
190
|
+
},
|
|
191
|
+
aligns: ["end", "start"],
|
|
192
|
+
widths: [2, 10],
|
|
193
|
+
besides: [
|
|
194
|
+
{
|
|
195
|
+
font: "",
|
|
196
|
+
icon: "",
|
|
197
|
+
type: "blank",
|
|
198
|
+
block: false,
|
|
199
|
+
style: {},
|
|
200
|
+
inline: false,
|
|
201
|
+
contents: "Pages",
|
|
202
|
+
labelFor: "sequel_to",
|
|
203
|
+
isFormula: {},
|
|
204
|
+
textStyle: "",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
above: [
|
|
208
|
+
{
|
|
209
|
+
type: "field",
|
|
210
|
+
block: false,
|
|
211
|
+
fieldview: "edit",
|
|
212
|
+
textStyle: "",
|
|
213
|
+
field_name: "pages",
|
|
214
|
+
configuration: {
|
|
215
|
+
where: "publisher == $publisher",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: "field",
|
|
220
|
+
block: false,
|
|
221
|
+
fieldview: "show",
|
|
222
|
+
textStyle: "",
|
|
223
|
+
field_name: "pagesp1",
|
|
224
|
+
configuration: {
|
|
225
|
+
input_type: "text",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
breakpoints: ["", ""],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: "action",
|
|
235
|
+
block: false,
|
|
236
|
+
rndid: "cb94bd",
|
|
237
|
+
nsteps: "",
|
|
238
|
+
minRole: 100,
|
|
239
|
+
isFormula: {},
|
|
240
|
+
action_icon: "",
|
|
241
|
+
action_name: "Save",
|
|
242
|
+
action_size: "",
|
|
243
|
+
action_bgcol: "",
|
|
244
|
+
action_label: "",
|
|
245
|
+
action_style: "btn-primary",
|
|
246
|
+
action_title: "",
|
|
247
|
+
configuration: {},
|
|
248
|
+
step_only_ifs: "",
|
|
249
|
+
action_textcol: "",
|
|
250
|
+
action_bordercol: "",
|
|
251
|
+
step_action_names: "",
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
columns: [
|
|
256
|
+
{
|
|
257
|
+
type: "Field",
|
|
258
|
+
block: false,
|
|
259
|
+
fieldview: "select",
|
|
260
|
+
textStyle: "",
|
|
261
|
+
field_name: "publisher",
|
|
262
|
+
configuration: {},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
type: "JoinField",
|
|
266
|
+
block: false,
|
|
267
|
+
fieldview: "as_text",
|
|
268
|
+
textStyle: "",
|
|
269
|
+
join_field: "publisher.name",
|
|
270
|
+
configuration: {},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
type: "Field",
|
|
274
|
+
block: false,
|
|
275
|
+
fieldview: "select",
|
|
276
|
+
textStyle: "",
|
|
277
|
+
field_name: "sequel_to",
|
|
278
|
+
configuration: {
|
|
279
|
+
where: "publisher == $publisher",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
type: "Field",
|
|
284
|
+
block: false,
|
|
285
|
+
fieldview: "edit",
|
|
286
|
+
textStyle: "",
|
|
287
|
+
field_name: "pages",
|
|
288
|
+
configuration: {
|
|
289
|
+
where: "publisher == $publisher",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: "Field",
|
|
294
|
+
block: false,
|
|
295
|
+
fieldview: "show",
|
|
296
|
+
textStyle: "",
|
|
297
|
+
field_name: "pagesp1",
|
|
298
|
+
configuration: {
|
|
299
|
+
input_type: "text",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: "Action",
|
|
304
|
+
rndid: "cb94bd",
|
|
305
|
+
nsteps: "",
|
|
306
|
+
minRole: 100,
|
|
307
|
+
isFormula: {},
|
|
308
|
+
action_icon: "",
|
|
309
|
+
action_name: "Save",
|
|
310
|
+
action_size: "",
|
|
311
|
+
action_bgcol: "",
|
|
312
|
+
action_label: "",
|
|
313
|
+
action_style: "btn-primary",
|
|
314
|
+
action_title: "",
|
|
315
|
+
configuration: {},
|
|
316
|
+
step_only_ifs: "",
|
|
317
|
+
action_textcol: "",
|
|
318
|
+
action_bordercol: "",
|
|
319
|
+
step_action_names: "",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
viewname: "AuthorEditForTest",
|
|
323
|
+
auto_save: false,
|
|
324
|
+
split_paste: false,
|
|
325
|
+
exttable_name: null,
|
|
326
|
+
page_when_done: null,
|
|
327
|
+
view_when_done: "authorlist",
|
|
328
|
+
dest_url_formula: null,
|
|
329
|
+
destination_type: "View",
|
|
330
|
+
formula_destinations: [],
|
|
331
|
+
page_group_when_done: null,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const newEvent = (dom, type) =>
|
|
337
|
+
new dom.window.CustomEvent(type, {
|
|
338
|
+
bubbles: true,
|
|
339
|
+
cancelable: true,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("JSDOM-E2E edit test", () => {
|
|
343
|
+
it("join select should set dynamic where and show if with joinfield", async () => {
|
|
344
|
+
await makeJoinSelectView({
|
|
345
|
+
name: "AuthorEditForTest",
|
|
346
|
+
showIfFormula: 'publisher?.name == "AK Press"',
|
|
347
|
+
});
|
|
348
|
+
const dom = await load_url_dom("/view/AuthorEditForTest");
|
|
349
|
+
await sleep(1000);
|
|
350
|
+
const pubwarn = dom.window.document.querySelector("div.pubwarn");
|
|
351
|
+
//console.log(dom.serialize());
|
|
352
|
+
expect(pubwarn.style.display).toBe("none");
|
|
353
|
+
|
|
354
|
+
const select_seq = dom.window.document.querySelector(
|
|
355
|
+
"select[name=sequel_to]"
|
|
356
|
+
);
|
|
357
|
+
expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
|
|
358
|
+
"",
|
|
359
|
+
"Herman Melville",
|
|
360
|
+
]);
|
|
361
|
+
const select = dom.window.document.querySelector("select[name=publisher]");
|
|
362
|
+
select.value = "1";
|
|
363
|
+
select.dispatchEvent(newEvent(dom, "change"));
|
|
364
|
+
|
|
365
|
+
await sleep(1000);
|
|
366
|
+
expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
|
|
367
|
+
"",
|
|
368
|
+
"Leo Tolstoy",
|
|
369
|
+
"Peter Kropotkin",
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
expect(pubwarn.style.display).toBe("");
|
|
373
|
+
|
|
374
|
+
const jf = dom.window.document.querySelector(
|
|
375
|
+
"div.pubwarn div[data-source-url]"
|
|
376
|
+
);
|
|
377
|
+
expect(jf.innerHTML).toBe("AK Press");
|
|
378
|
+
});
|
|
379
|
+
it("calculated field", async () => {
|
|
380
|
+
const dom = await load_url_dom("/view/AuthorEditForTest");
|
|
381
|
+
const input = dom.window.document.querySelector("input[name=pages]");
|
|
382
|
+
input.value = "13";
|
|
383
|
+
input.dispatchEvent(newEvent(dom, "change"));
|
|
384
|
+
await sleep(1000);
|
|
385
|
+
const cf = dom.window.document.querySelector(
|
|
386
|
+
`div[data-source-url="/field/show-calculated/books/pagesp1/show?input_type=text"]`
|
|
387
|
+
);
|
|
388
|
+
expect(cf.innerHTML).toBe("14");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("join select should set dynamic where and show if with no joinfield", async () => {
|
|
392
|
+
await makeJoinSelectView({
|
|
393
|
+
name: "AuthorEditForTest1",
|
|
394
|
+
showIfFormula: "publisher == 1",
|
|
395
|
+
});
|
|
396
|
+
const dom = await load_url_dom("/view/AuthorEditForTest1");
|
|
397
|
+
await sleep(1000);
|
|
398
|
+
const pubwarn = dom.window.document.querySelector("div.pubwarn");
|
|
399
|
+
|
|
400
|
+
expect(pubwarn.style.display).toBe("none");
|
|
401
|
+
|
|
402
|
+
const select_seq = dom.window.document.querySelector(
|
|
403
|
+
"select[name=sequel_to]"
|
|
404
|
+
);
|
|
405
|
+
expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
|
|
406
|
+
"",
|
|
407
|
+
"Herman Melville",
|
|
408
|
+
]);
|
|
409
|
+
const select = dom.window.document.querySelector("select[name=publisher]");
|
|
410
|
+
select.value = "1";
|
|
411
|
+
select.dispatchEvent(newEvent(dom, "change"));
|
|
412
|
+
|
|
413
|
+
await sleep(1000);
|
|
414
|
+
expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
|
|
415
|
+
"",
|
|
416
|
+
"Leo Tolstoy",
|
|
417
|
+
"Peter Kropotkin",
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
expect(pubwarn.style.display).toBe("");
|
|
421
|
+
const jf = dom.window.document.querySelector(
|
|
422
|
+
"div.pubwarn div[data-source-url]"
|
|
423
|
+
);
|
|
424
|
+
expect(jf.innerHTML).toBe("AK Press");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const request = require("supertest");
|
|
2
|
+
const getApp = require("../app");
|
|
3
|
+
const { resetToFixtures, load_url_dom } = require("../auth/testhelp");
|
|
4
|
+
const db = require("@saltcorn/data/db");
|
|
5
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
6
|
+
const View = require("@saltcorn/data/models/view");
|
|
7
|
+
const Table = require("@saltcorn/data/models/table");
|
|
8
|
+
const { plugin_with_routes, sleep } = require("@saltcorn/data/tests/mocks");
|
|
9
|
+
|
|
10
|
+
afterAll(db.close);
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
await resetToFixtures();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
jest.setTimeout(30000);
|
|
16
|
+
|
|
17
|
+
describe("JSDOM-E2E filter test", () => {
|
|
18
|
+
it("should load authorlist", async () => {
|
|
19
|
+
const dom = await load_url_dom("/view/authorlist");
|
|
20
|
+
//console.log("dom", dom);
|
|
21
|
+
});
|
|
22
|
+
it("should user filter to change url", async () => {
|
|
23
|
+
await View.create({
|
|
24
|
+
viewtemplate: "Filter",
|
|
25
|
+
description: "",
|
|
26
|
+
min_role: 100,
|
|
27
|
+
name: `authorfilter1`,
|
|
28
|
+
table_id: Table.findOne("books")?.id,
|
|
29
|
+
default_render_page: "",
|
|
30
|
+
slug: {},
|
|
31
|
+
attributes: {},
|
|
32
|
+
configuration: {
|
|
33
|
+
layout: {
|
|
34
|
+
type: "field",
|
|
35
|
+
block: false,
|
|
36
|
+
fieldview: "edit",
|
|
37
|
+
textStyle: "",
|
|
38
|
+
field_name: "author",
|
|
39
|
+
configuration: {},
|
|
40
|
+
},
|
|
41
|
+
columns: [
|
|
42
|
+
{
|
|
43
|
+
type: "Field",
|
|
44
|
+
block: false,
|
|
45
|
+
fieldview: "edit",
|
|
46
|
+
textStyle: "",
|
|
47
|
+
field_name: "author",
|
|
48
|
+
configuration: {},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const dom = await load_url_dom("/view/authorfilter1");
|
|
54
|
+
expect(dom.window.location.href).toBe(
|
|
55
|
+
"http://localhost/view/authorfilter1"
|
|
56
|
+
);
|
|
57
|
+
//console.log(dom.serialize());
|
|
58
|
+
const input = dom.window.document.querySelector("input[name=author]");
|
|
59
|
+
input.value = "Leo";
|
|
60
|
+
input.dispatchEvent(new dom.window.Event("change"));
|
|
61
|
+
await sleep(1000);
|
|
62
|
+
expect(dom.window.location.href).toBe(
|
|
63
|
+
"http://localhost/view/authorfilter1?author=Leo"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
//console.log("dom", dom);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/tests/jsdom.test.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
const request = require("supertest");
|
|
2
|
-
const getApp = require("../app");
|
|
3
|
-
const { resetToFixtures } = require("../auth/testhelp");
|
|
4
|
-
const db = require("@saltcorn/data/db");
|
|
5
|
-
const { getState } = require("@saltcorn/data/db/state");
|
|
6
|
-
const View = require("@saltcorn/data/models/view");
|
|
7
|
-
const Table = require("@saltcorn/data/models/table");
|
|
8
|
-
|
|
9
|
-
const { plugin_with_routes, sleep } = require("@saltcorn/data/tests/mocks");
|
|
10
|
-
const jsdom = require("jsdom");
|
|
11
|
-
const { JSDOM, ResourceLoader } = jsdom;
|
|
12
|
-
afterAll(db.close);
|
|
13
|
-
beforeAll(async () => {
|
|
14
|
-
await resetToFixtures();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
jest.setTimeout(30000);
|
|
18
|
-
|
|
19
|
-
const load_url_dom = async (url) => {
|
|
20
|
-
const app = await getApp({ disableCsrf: true });
|
|
21
|
-
class CustomResourceLoader extends ResourceLoader {
|
|
22
|
-
async fetch(url, options) {
|
|
23
|
-
const url1 = url.replace("http://localhost", "");
|
|
24
|
-
//console.log("fetching", url, url1);
|
|
25
|
-
const res = await request(app).get(url1);
|
|
26
|
-
|
|
27
|
-
return Buffer.from(res.text);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const reqres = await request(app).get(url);
|
|
31
|
-
//console.log("rr1", reqres.text);
|
|
32
|
-
const virtualConsole = new jsdom.VirtualConsole();
|
|
33
|
-
virtualConsole.sendTo(console);
|
|
34
|
-
const dom = new JSDOM(reqres.text, {
|
|
35
|
-
url: "http://localhost" + url,
|
|
36
|
-
runScripts: "dangerously",
|
|
37
|
-
resources: new CustomResourceLoader(),
|
|
38
|
-
pretendToBeVisual: true,
|
|
39
|
-
virtualConsole,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
class FakeXHR {
|
|
43
|
-
constructor() {
|
|
44
|
-
this.readyState = 0;
|
|
45
|
-
this.requestHeaders = [];
|
|
46
|
-
//return traceMethodCalls(this);
|
|
47
|
-
}
|
|
48
|
-
open(method, url) {
|
|
49
|
-
//console.log("open xhr", method, url);
|
|
50
|
-
this.method = method;
|
|
51
|
-
this.url = url;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
addEventListener(ev, reqListener) {
|
|
55
|
-
if (ev === "load") this.reqListener = reqListener;
|
|
56
|
-
}
|
|
57
|
-
setRequestHeader(k, v) {
|
|
58
|
-
this.requestHeaders.push([k, v]);
|
|
59
|
-
}
|
|
60
|
-
overrideMimeType() {}
|
|
61
|
-
async send() {
|
|
62
|
-
//console.log("send1", this.url);
|
|
63
|
-
const url1 = this.url.replace("http://localhost", "");
|
|
64
|
-
//console.log("xhr fetching", url1);
|
|
65
|
-
let req = request(app).get(url1);
|
|
66
|
-
for (const [k, v] of this.requestHeaders) {
|
|
67
|
-
req = req.set(k, v);
|
|
68
|
-
}
|
|
69
|
-
const res = await req;
|
|
70
|
-
this.response = res.text;
|
|
71
|
-
this.responseText = res.text;
|
|
72
|
-
this.status = res.status;
|
|
73
|
-
this.statusText = "OK";
|
|
74
|
-
this.readyState = 4;
|
|
75
|
-
if (this.reqListener) this.reqListener(res.text);
|
|
76
|
-
if (this.onload) this.onload(res.text);
|
|
77
|
-
//console.log("agent res", res);
|
|
78
|
-
//console.log("xhr", this);
|
|
79
|
-
}
|
|
80
|
-
getAllResponseHeaders() {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
dom.window.XMLHttpRequest = FakeXHR;
|
|
85
|
-
await new Promise(function (resolve, reject) {
|
|
86
|
-
dom.window.addEventListener("DOMContentLoaded", (event) => {
|
|
87
|
-
resolve();
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
return dom;
|
|
91
|
-
};
|
|
92
|
-
function traceMethodCalls(obj) {
|
|
93
|
-
let handler = {
|
|
94
|
-
get(target, propKey, receiver) {
|
|
95
|
-
console.log(propKey);
|
|
96
|
-
const origMethod = target[propKey];
|
|
97
|
-
return function (...args) {
|
|
98
|
-
let result = origMethod.apply(this, args);
|
|
99
|
-
console.log(
|
|
100
|
-
propKey + JSON.stringify(args) + " -> " + JSON.stringify(result)
|
|
101
|
-
);
|
|
102
|
-
return result;
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
return new Proxy(obj, handler);
|
|
107
|
-
}
|
|
108
|
-
describe("JSDOM test", () => {
|
|
109
|
-
it("should load authorlist", async () => {
|
|
110
|
-
const dom = await load_url_dom("/view/authorlist");
|
|
111
|
-
//console.log("dom", dom);
|
|
112
|
-
});
|
|
113
|
-
it("should user filter to change url", async () => {
|
|
114
|
-
await View.create({
|
|
115
|
-
viewtemplate: "Filter",
|
|
116
|
-
description: "",
|
|
117
|
-
min_role: 100,
|
|
118
|
-
name: `authorfilter1`,
|
|
119
|
-
table_id: Table.findOne("books")?.id,
|
|
120
|
-
default_render_page: "",
|
|
121
|
-
slug: {},
|
|
122
|
-
attributes: {},
|
|
123
|
-
configuration: {
|
|
124
|
-
layout: {
|
|
125
|
-
type: "field",
|
|
126
|
-
block: false,
|
|
127
|
-
fieldview: "edit",
|
|
128
|
-
textStyle: "",
|
|
129
|
-
field_name: "author",
|
|
130
|
-
configuration: {},
|
|
131
|
-
},
|
|
132
|
-
columns: [
|
|
133
|
-
{
|
|
134
|
-
type: "Field",
|
|
135
|
-
block: false,
|
|
136
|
-
fieldview: "edit",
|
|
137
|
-
textStyle: "",
|
|
138
|
-
field_name: "author",
|
|
139
|
-
configuration: {},
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
const dom = await load_url_dom("/view/authorfilter1");
|
|
145
|
-
expect(dom.window.location.href).toBe(
|
|
146
|
-
"http://localhost/view/authorfilter1"
|
|
147
|
-
);
|
|
148
|
-
//console.log(dom.serialize());
|
|
149
|
-
const input = dom.window.document.querySelector("input[name=author]");
|
|
150
|
-
input.value = "Leo";
|
|
151
|
-
input.dispatchEvent(new dom.window.Event("change"));
|
|
152
|
-
await sleep(2000);
|
|
153
|
-
expect(dom.window.location.href).toBe(
|
|
154
|
-
"http://localhost/view/authorfilter1?author=Leo"
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
//console.log("dom", dom);
|
|
158
|
-
});
|
|
159
|
-
});
|