@saltcorn/server 0.9.6-beta.15 → 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 CHANGED
@@ -1155,7 +1155,11 @@ router.get(
1155
1155
  } else {
1156
1156
  const auth = getState().auth_methods[method];
1157
1157
  if (auth) {
1158
- passport.authenticate(method, auth.parameters)(req, res, next);
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
- passport.authenticate(method, auth.parameters)(
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
- passport.authenticate(method, { failureRedirect: "/auth/login" })(
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
- const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
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 (!safes.includes(plugin.location)) {
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
@@ -1436,5 +1436,17 @@
1436
1436
  "Older": "Older",
1437
1437
  "Newest": "Newest",
1438
1438
  "Delete all read": "Delete all read",
1439
- "Trigger %s duplicated as %s": "Trigger %s duplicated as %s"
1439
+ "Trigger %s duplicated as %s": "Trigger %s duplicated as %s",
1440
+ "Tooltip": "Tooltip",
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"
1440
1452
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.6-beta.15",
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.15",
11
- "@saltcorn/builder": "0.9.6-beta.15",
12
- "@saltcorn/data": "0.9.6-beta.15",
13
- "@saltcorn/admin-models": "0.9.6-beta.15",
14
- "@saltcorn/filemanager": "0.9.6-beta.15",
15
- "@saltcorn/markup": "0.9.6-beta.15",
16
- "@saltcorn/plugins-loader": "0.9.6-beta.15",
17
- "@saltcorn/sbadmin2": "0.9.6-beta.15",
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",
@@ -1317,10 +1317,12 @@ async function common_done(res, viewnameOrElem, isWeb = true) {
1317
1317
  }
1318
1318
  };
1319
1319
  if (res.notify) await handle(res.notify, notifyAlert);
1320
- if (res.error)
1320
+ if (res.error) {
1321
+ if (window._sc_loglevel > 4) console.trace("error response", res.error);
1321
1322
  await handle(res.error, (text) =>
1322
1323
  notifyAlert({ type: "danger", text: text })
1323
1324
  );
1325
+ }
1324
1326
  if (res.notify_success)
1325
1327
  await handle(res.notify_success, (text) =>
1326
1328
  notifyAlert({ type: "success", text: text })
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
- (isRoot && can_update
1080
- ? post_btn(
1081
- "/admin/upgrade",
1082
- req.__("Upgrade"),
1083
- req.csrfToken(),
1084
- {
1085
- btnClass: "btn-primary btn-sm",
1086
- formClass: "d-inline",
1087
- }
1088
- )
1089
- : isRoot && is_latest
1090
- ? span(
1091
- { class: "badge bg-primary ms-2" },
1092
- req.__("Latest")
1093
- ) +
1094
- post_btn(
1095
- "/admin/check-for-upgrade",
1096
- req.__("Check for updates"),
1097
- req.csrfToken(),
1098
- {
1099
- btnClass: "btn-primary btn-sm px-1 py-0",
1100
- formClass: "d-inline",
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
- if (db.getTenantSchema() !== db.connectObj.default_schema) {
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/menu.js CHANGED
@@ -15,6 +15,7 @@ const { getState } = require("@saltcorn/data/db/state");
15
15
  const User = require("@saltcorn/data/models/user");
16
16
  const View = require("@saltcorn/data/models/view");
17
17
  const Page = require("@saltcorn/data/models/page");
18
+ const PageGroup = require("@saltcorn/data/models/page_group");
18
19
  const { save_menu_items } = require("@saltcorn/data/models/config");
19
20
  const db = require("@saltcorn/data/db");
20
21
 
@@ -43,6 +44,10 @@ module.exports = router;
43
44
  const menuForm = async (req) => {
44
45
  const views = await View.find({}, { orderBy: "name", nocase: true });
45
46
  const pages = await Page.find({}, { orderBy: "name", nocase: true });
47
+ const pageGroups = await PageGroup.find(
48
+ {},
49
+ { orderBy: "name", nocase: true }
50
+ );
46
51
  const roles = await User.get_roles();
47
52
  const tables = await Table.find_with_external({});
48
53
  const dynTableOptions = tables.map((t) => t.name);
@@ -101,6 +106,7 @@ const menuForm = async (req) => {
101
106
  options: [
102
107
  "View",
103
108
  "Page",
109
+ "Page Group",
104
110
  "Link",
105
111
  "Header",
106
112
  "Dynamic",
@@ -141,6 +147,14 @@ const menuForm = async (req) => {
141
147
  attributes: { options: views.map((r) => r.select_option) },
142
148
  showIf: { type: "View" },
143
149
  },
150
+ {
151
+ name: "page_group",
152
+ label: req.__("Page group"),
153
+ input_type: "select",
154
+ class: "item-menu",
155
+ options: pageGroups.map((r) => r.name),
156
+ showIf: { type: "Page Group" },
157
+ },
144
158
  {
145
159
  name: "action_name",
146
160
  label: req.__("Action"),
@@ -194,6 +208,14 @@ const menuForm = async (req) => {
194
208
  required: true,
195
209
  showIf: { type: "Dynamic" },
196
210
  },
211
+ {
212
+ name: "dyn_tooltip_fml",
213
+ label: req.__("Tooltip formula"),
214
+ class: "item-menu",
215
+ type: "String",
216
+ required: false,
217
+ showIf: { type: "Dynamic" },
218
+ },
197
219
  {
198
220
  name: "dyn_url_fml",
199
221
  label: req.__("URL formula"),
@@ -223,6 +245,7 @@ const menuForm = async (req) => {
223
245
  type: [
224
246
  "View",
225
247
  "Page",
248
+ "Page Group",
226
249
  "Link",
227
250
  "Header",
228
251
  "Dynamic",
@@ -238,13 +261,34 @@ const menuForm = async (req) => {
238
261
  attributes: {
239
262
  html: `<button type="button" id="myEditor_icon" class="btn btn-outline-secondary"></button>`,
240
263
  },
241
- showIf: { type: ["View", "Page", "Link", "Header", "Action"] },
264
+ showIf: {
265
+ type: ["View", "Page", "Page Group", "Link", "Header", "Action"],
266
+ },
242
267
  },
243
268
  {
244
269
  name: "icon",
245
270
  class: "item-menu",
246
271
  input_type: "hidden",
247
272
  },
273
+ {
274
+ name: "tooltip",
275
+ label: req.__("Tooltip"),
276
+ class: "item-menu",
277
+ input_type: "text",
278
+ required: false,
279
+ showIf: {
280
+ type: [
281
+ "View",
282
+ "Page",
283
+ "Page Group",
284
+ "Link",
285
+ "Header",
286
+ "Dynamic",
287
+ "Search",
288
+ "Action",
289
+ ],
290
+ },
291
+ },
248
292
  {
249
293
  name: "min_role",
250
294
  label: req.__("Minimum role"),
@@ -277,7 +321,7 @@ const menuForm = async (req) => {
277
321
  type: "Bool",
278
322
  required: false,
279
323
  class: "item-menu",
280
- showIf: { type: ["View", "Page", "Link"] },
324
+ showIf: { type: ["View", "Page", "Page Group", "Link"] },
281
325
  },
282
326
  {
283
327
  name: "in_modal",
@@ -285,7 +329,7 @@ const menuForm = async (req) => {
285
329
  type: "Bool",
286
330
  required: false,
287
331
  class: "item-menu",
288
- showIf: { type: ["View", "Page", "Link"] },
332
+ showIf: { type: ["View", "Page", "Page Group", "Link"] },
289
333
  },
290
334
  {
291
335
  name: "style",
@@ -295,7 +339,15 @@ const menuForm = async (req) => {
295
339
  type: "String",
296
340
  required: true,
297
341
  showIf: {
298
- type: ["View", "Page", "Link", "Header", "Dynamic", "Action"],
342
+ type: [
343
+ "View",
344
+ "Page",
345
+ "Page Group",
346
+ "Link",
347
+ "Header",
348
+ "Dynamic",
349
+ "Action",
350
+ ],
299
351
  },
300
352
  attributes: {
301
353
  options: [
@@ -322,6 +374,7 @@ const menuForm = async (req) => {
322
374
  type: [
323
375
  "View",
324
376
  "Page",
377
+ "Page Group",
325
378
  "Link",
326
379
  "Header",
327
380
  "Dynamic",
package/routes/plugins.js CHANGED
@@ -865,9 +865,13 @@ router.get(
865
865
  if (!module) {
866
866
  module = getState().plugins[getState().plugin_module_names[plugin.name]];
867
867
  }
868
+ const userLayout =
869
+ user._attributes?.layout?.plugin === plugin.name
870
+ ? user._attributes.layout.config || {}
871
+ : {};
868
872
  const form = await module.user_config_form({
869
873
  ...(plugin.configuration || {}),
870
- ...(user._attributes?.layout?.config || {}),
874
+ ...userLayout,
871
875
  });
872
876
  form.action = `/plugins/user_configure/${encodeURIComponent(plugin.name)}`;
873
877
  form.onChange = `applyViewConfig(this, '/plugins/user_saveconfig/${encodeURIComponent(
package/routes/search.js CHANGED
@@ -202,7 +202,8 @@ const runSearch = async ({ q, _page, table }, req, res) => {
202
202
  let tablesWithResults = [];
203
203
  let tablesConfigured = 0;
204
204
  for (const [tableName, viewName] of Object.entries(cfg)) {
205
- if (!viewName || viewName === "") continue;
205
+ if (!viewName || viewName === "" || viewName === "search_table_description")
206
+ continue;
206
207
  tablesConfigured += 1;
207
208
  if (table && tableName !== table) continue;
208
209
  let sectionHeader = tableName;
@@ -232,7 +233,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
232
233
  }
233
234
 
234
235
  if (vresps.length > 0) {
235
- tablesWithResults.push(tableName);
236
+ tablesWithResults.push({ tableName, label: sectionHeader });
236
237
  resp.push({
237
238
  type: "card",
238
239
  title: span({ id: tableName }, sectionHeader),
@@ -273,8 +274,13 @@ const runSearch = async ({ q, _page, table }, req, res) => {
273
274
  req.__("Show only matches in table:"),
274
275
  "&nbsp;",
275
276
  tablesWithResults
276
- .map((t) =>
277
- a({ href: `javascript:set_state_field('table', '${t}')` }, t)
277
+ .map(({ tableName, label }) =>
278
+ a(
279
+ {
280
+ href: `javascript:set_state_field('table', '${tableName}')`,
281
+ },
282
+ label
283
+ )
278
284
  )
279
285
  .join(" | ")
280
286
  )
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 form = tenant_form(req);
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
- if (
81
- req.user &&
82
- req.user.role_id === 1 &&
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
+ });