@saltcorn/server 0.9.3 → 0.9.4-beta.0

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/testhelp.js CHANGED
@@ -29,24 +29,29 @@ const toRedirect = (loc) => (res) => {
29
29
 
30
30
  /**
31
31
  *
32
- * @param {number} txt
32
+ * @param {string|string[]} exp expected string or for arrrays at least one must be present
33
33
  * @param {number} expCode
34
34
  * @returns {void}
35
35
  * @throws {Error}
36
36
  */
37
37
  const toInclude =
38
- (txt, expCode = 200) =>
38
+ (exp, expCode = 200) =>
39
39
  (res) => {
40
40
  if (res.statusCode !== expCode) {
41
41
  console.log(res.text);
42
42
  throw new Error(
43
- `Expected status ${expCode} when lookinng for "${txt}", received ${res.statusCode}`
43
+ `Expected status ${expCode} when lookinng for "${exp}", received ${res.statusCode}`
44
44
  );
45
45
  }
46
-
47
- if (!res.text.includes(txt)) {
46
+ const check = (txt) => res.text.includes(txt);
47
+ if (Array.isArray(exp)) {
48
+ if (!exp.some(check)) {
49
+ console.log(res.text);
50
+ throw new Error(`Expected text from [${exp.join(", ")}] not found`);
51
+ }
52
+ } else if (!check(exp)) {
48
53
  console.log(res.text);
49
- throw new Error(`Expected text ${txt} not found`);
54
+ throw new Error(`Expected text ${exp} not found`);
50
55
  }
51
56
  };
52
57
 
package/locales/en.json CHANGED
@@ -1344,5 +1344,7 @@
1344
1344
  "Time of day": "Time of day",
1345
1345
  "UTC timezone": "UTC timezone",
1346
1346
  "Show if formula": "Show if formula",
1347
- "Show link or embed if true, don't show if false. Based on state variables from URL query string and <code>user</code>. For the full state use <code>row</code>. Example: <code>!!row.createlink</code> to show link if and only if state has <code>createlink</code>.": "Show link or embed if true, don't show if false. Based on state variables from URL query string and <code>user</code>. For the full state use <code>row</code>. Example: <code>!!row.createlink</code> to show link if and only if state has <code>createlink</code>."
1347
+ "Show link or embed if true, don't show if false. Based on state variables from URL query string and <code>user</code>. For the full state use <code>row</code>. Example: <code>!!row.createlink</code> to show link if and only if state has <code>createlink</code>.": "Show link or embed if true, don't show if false. Based on state variables from URL query string and <code>user</code>. For the full state use <code>row</code>. Example: <code>!!row.createlink</code> to show link if and only if state has <code>createlink</code>.",
1348
+ "Pagegroup": "Pagegroup",
1349
+ "Pagegroup %s has no members": "Pagegroup %s has no members"
1348
1350
  }
package/locales/es.json CHANGED
@@ -349,7 +349,7 @@
349
349
  "Omit search form": "Omit search form",
350
350
  "Do not display the search filter form": "No mostrar el formulario de filtro de búsqueda",
351
351
  "Default search form values when first loaded": "Valores del formulario de búsqueda predeterminado cuando se carga por primera vez",
352
- "Next":"Siguiente",
352
+ "Next": "Siguiente",
353
353
  "Save": "Guardar",
354
354
  "Calculated fields cannot have File type": "Los campos calculados no pueden tener tipo de archivo",
355
355
  "Calculated fields cannot have Key type": "Los campos calculados no pueden tener tipo de clave",
@@ -502,7 +502,7 @@
502
502
  "Restart server": "Reiniciar el servidor",
503
503
  "Edit Plugin": "Editar complemento",
504
504
  "Unknown authentication method %ss": "Método de autenticación desconocido %ss",
505
- "Restart required for changes to take effect. Restart server from the <a href=\/admin\">Admin page</a>.": "Es necesario reiniciar para que los cambios surtan efecto. Reinicie el servidor desde la <a href=\/admin\">página de administración</a>.",
505
+ "Restart required for changes to take effect. Restart server from the <a href=/admin\">Admin page</a>.": "Es necesario reiniciar para que los cambios surtan efecto. Reinicie el servidor desde la <a href=/admin\">página de administración</a>.",
506
506
  "Email with password reset link sent": "Correo electrónico con enlace para restablecer contraseña enviado",
507
507
  "Link Style": "Estilo de enlace",
508
508
  "Link size": "Tamaño del enlace",
@@ -1268,5 +1268,6 @@
1268
1268
  "Pack file": "Empaquetar archivo",
1269
1269
  "Upload a pack file": "Subir un archivo de paquete",
1270
1270
  "No menu": "Sin menú",
1271
- "Omit the menu from this page": "Omitir el menú de esta página"
1272
- }
1271
+ "Omit the menu from this page": "Omitir el menú de esta página",
1272
+ "%s has no eligible page": "%s has no eligible page"
1273
+ }
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.3",
3
+ "version": "0.9.4-beta.0",
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.3",
11
- "@saltcorn/builder": "0.9.3",
12
- "@saltcorn/data": "0.9.3",
13
- "@saltcorn/admin-models": "0.9.3",
14
- "@saltcorn/filemanager": "0.9.3",
15
- "@saltcorn/markup": "0.9.3",
16
- "@saltcorn/sbadmin2": "0.9.3",
10
+ "@saltcorn/base-plugin": "0.9.4-beta.0",
11
+ "@saltcorn/builder": "0.9.4-beta.0",
12
+ "@saltcorn/data": "0.9.4-beta.0",
13
+ "@saltcorn/admin-models": "0.9.4-beta.0",
14
+ "@saltcorn/filemanager": "0.9.4-beta.0",
15
+ "@saltcorn/markup": "0.9.4-beta.0",
16
+ "@saltcorn/sbadmin2": "0.9.4-beta.0",
17
17
  "@socket.io/cluster-adapter": "^0.2.1",
18
18
  "@socket.io/sticky": "^1.0.1",
19
19
  "adm-zip": "0.5.10",
@@ -80,6 +80,7 @@ function apply_showif() {
80
80
  $("[data-show-if]").each(function (ix, element) {
81
81
  var e = $(element);
82
82
  try {
83
+ if (e.prop("disabled")) return;
83
84
  let to_show = e.data("data-show-if-fun");
84
85
  if (!to_show) {
85
86
  to_show = new Function(
@@ -92,12 +93,12 @@ function apply_showif() {
92
93
  e.data("data-closest-form-ns", e.closest(".form-namespace"));
93
94
  if (to_show(e))
94
95
  e.show()
95
- .find("input, textarea, button, select")
96
+ .find("input, textarea, button, select, [data-show-if]")
96
97
  .prop("disabled", e.attr("data-disabled") || false);
97
98
  else
98
99
  e.hide()
99
100
  .find(
100
- "input:enabled, textarea:enabled, button:enabled, select:enabled"
101
+ "input:enabled, textarea:enabled, button:enabled, select:enabled, [data-show-if]:not([disabled])"
101
102
  )
102
103
  .prop("disabled", true);
103
104
  } catch (e) {
@@ -1504,3 +1505,17 @@ function disable_inactive_tab_inputs(id) {
1504
1505
  });
1505
1506
  }, 100);
1506
1507
  }
1508
+
1509
+ function set_readonly_select(e) {
1510
+ if (!e.target) return;
1511
+ const $e = $(e.target);
1512
+ if ($e.attr("type") !== "hidden") return;
1513
+ const $disp = $e.prev();
1514
+ const optionsS = decodeURIComponent(
1515
+ $disp.attr("data-readonly-select-options")
1516
+ );
1517
+ if (!optionsS) return;
1518
+ const options = JSON.parse(optionsS);
1519
+ const option = options.find((o) => o.value == e.target.value);
1520
+ if (option) $disp.val(option.label);
1521
+ }
package/routes/admin.js CHANGED
@@ -1476,27 +1476,22 @@ router.get(
1476
1476
  );
1477
1477
  const buildDialogScript = () => {
1478
1478
  return `<script>
1479
- function swapEntryInputs(activeTab, activeInput, disabledTab, disabledInput) {
1480
- activeTab.addClass("active");
1481
- activeInput.removeClass("d-none");
1482
- activeInput.addClass("d-block");
1483
- activeInput.attr("name", "entryPoint");
1484
- disabledTab.removeClass("active");
1485
- disabledInput.removeClass("d-block");
1486
- disabledInput.addClass("d-none");
1487
- disabledInput.removeAttr("name");
1488
- }
1489
-
1490
1479
  function showEntrySelect(type) {
1491
- const viewNavLin = $("#viewNavLinkID");
1492
- const pageNavLink = $("#pageNavLinkID");
1493
- const viewInp = $("#viewInputID");
1494
- const pageInp = $("#pageInputID");
1495
- if (type === "page") {
1496
- swapEntryInputs(pageNavLink, pageInp, viewNavLin, viewInp);
1497
- }
1498
- else if (type === "view") {
1499
- swapEntryInputs(viewNavLin, viewInp, pageNavLink, pageInp);
1480
+ for( const currentType of ["view", "page", "pagegroup"]) {
1481
+ const tab = $('#' + currentType + 'NavLinkID');
1482
+ const input = $('#' + currentType + 'InputID');
1483
+ if (currentType === type) {
1484
+ tab.addClass("active");
1485
+ input.removeClass("d-none");
1486
+ input.addClass("d-block");
1487
+ input.attr("name", "entryPoint");
1488
+ }
1489
+ else {
1490
+ tab.removeClass("active");
1491
+ input.removeClass("d-block");
1492
+ input.addClass("d-none");
1493
+ input.removeAttr("name");
1494
+ }
1500
1495
  }
1501
1496
  $("#entryPointTypeID").attr("value", type);
1502
1497
  }
@@ -1526,6 +1521,7 @@ router.get(
1526
1521
  error_catcher(async (req, res) => {
1527
1522
  const views = await View.find();
1528
1523
  const pages = await Page.find();
1524
+ const pageGroups = await PageGroup.find();
1529
1525
  const images = (await File.find({ mime_super: "image" })).filter((image) =>
1530
1526
  image.filename?.endsWith(".png")
1531
1527
  );
@@ -1622,6 +1618,23 @@ router.get(
1622
1618
  id: "pageNavLinkID",
1623
1619
  },
1624
1620
  req.__("Page")
1621
+ ),
1622
+ li(
1623
+ {
1624
+ class: "nav-item",
1625
+ onClick: "showEntrySelect('pagegroup')",
1626
+ },
1627
+ div(
1628
+ {
1629
+ class: `nav-link ${
1630
+ builderSettings.entryPointType === "pagegroup"
1631
+ ? "active"
1632
+ : ""
1633
+ }`,
1634
+ id: "pagegroupNavLinkID",
1635
+ },
1636
+ req.__("Pagegroup")
1637
+ )
1625
1638
  )
1626
1639
  )
1627
1640
  ),
@@ -1629,7 +1642,8 @@ router.get(
1629
1642
  select(
1630
1643
  {
1631
1644
  class: `form-select ${
1632
- builderSettings.entryPointType === "page"
1645
+ builderSettings.entryPointType === "page" ||
1646
+ builderSettings.entryPointType === "pagegroup"
1633
1647
  ? "d-none"
1634
1648
  : ""
1635
1649
  }`,
@@ -1658,7 +1672,8 @@ router.get(
1658
1672
  {
1659
1673
  class: `form-select ${
1660
1674
  !builderSettings.entryPointType ||
1661
- builderSettings.entryPointType === "view"
1675
+ builderSettings.entryPointType === "view" ||
1676
+ builderSettings.entryPointType === "pagegroup"
1662
1677
  ? "d-none"
1663
1678
  : ""
1664
1679
  }`,
@@ -1680,6 +1695,36 @@ router.get(
1680
1695
  )
1681
1696
  )
1682
1697
  .join("")
1698
+ ),
1699
+ // select entry-pagegroup
1700
+ select(
1701
+ {
1702
+ class: `form-select ${
1703
+ !builderSettings.entryPointType ||
1704
+ builderSettings.entryPointType === "view" ||
1705
+ builderSettings.entryPointType === "page"
1706
+ ? "d-none"
1707
+ : ""
1708
+ }`,
1709
+ ...(builderSettings.entryPointType === "pagegroup"
1710
+ ? { name: "entryPoint" }
1711
+ : {}),
1712
+ id: "pagegroupInputID",
1713
+ },
1714
+ pageGroups
1715
+ .map((group) =>
1716
+ option(
1717
+ {
1718
+ value: group.name,
1719
+ selected:
1720
+ builderSettings.entryPointType ===
1721
+ "pagegroup" &&
1722
+ builderSettings.entryPoint === group.name,
1723
+ },
1724
+ group.name
1725
+ )
1726
+ )
1727
+ .join("")
1683
1728
  )
1684
1729
  ),
1685
1730
  div(
@@ -2252,7 +2297,7 @@ router.post(
2252
2297
  "-e",
2253
2298
  entryPoint,
2254
2299
  "-t",
2255
- entryPointType,
2300
+ entryPointType === "pagegroup" ? "page" : entryPointType,
2256
2301
  "-c",
2257
2302
  buildDir,
2258
2303
  "-b",
@@ -12,6 +12,7 @@ const View = require("@saltcorn/data/models/view");
12
12
  const User = require("@saltcorn/data/models/user");
13
13
  const File = require("@saltcorn/data/models/file");
14
14
  const Page = require("@saltcorn/data/models/page");
15
+ const PageGroup = require("@saltcorn/data/models/page_group");
15
16
  const Plugin = require("@saltcorn/data/models/plugin");
16
17
  const { link, mkTable } = require("@saltcorn/markup");
17
18
  const { div, a, p, i, h5, span } = require("@saltcorn/markup/tags");
@@ -22,7 +23,7 @@ const { get_latest_npm_version } = require("@saltcorn/data/models/config");
22
23
  const packagejson = require("../package.json");
23
24
  const Trigger = require("@saltcorn/data/models/trigger");
24
25
  const { fileUploadForm } = require("../markup/forms");
25
- const { get_base_url, sendHtmlFile } = require("./utils.js");
26
+ const { get_base_url, sendHtmlFile, getEligiblePage } = require("./utils.js");
26
27
 
27
28
  /**
28
29
  * Tables List
@@ -545,6 +546,18 @@ const no_views_logged_in = async (req, res) => {
545
546
  * @returns {Promise<boolean>}
546
547
  */
547
548
  const get_config_response = async (role_id, res, req) => {
549
+ const wrap = async (contents, homeCfg, title, description) => {
550
+ if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
551
+ else
552
+ res.sendWrap(
553
+ {
554
+ title: title || "",
555
+ description: description || "",
556
+ bodyClass: "page_" + db.sqlsanitize(homeCfg),
557
+ },
558
+ contents
559
+ );
560
+ };
548
561
  const modernCfg = getState().getConfig("home_page_by_role", false);
549
562
  // predefined roles
550
563
  const legacy_role = { 100: "public", 80: "user", 40: "staff", 1: "admin" }[
@@ -554,21 +567,30 @@ const get_config_response = async (role_id, res, req) => {
554
567
  if (typeof homeCfg !== "string")
555
568
  homeCfg = getState().getConfig(legacy_role + "_home");
556
569
  if (homeCfg) {
557
- const db_page = await Page.findOne({ name: homeCfg });
558
-
559
- if (db_page) {
560
- const contents = await db_page.run(req.query, { res, req });
561
- if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
562
- else
563
- res.sendWrap(
564
- {
565
- title: db_page.title,
566
- description: db_page.description,
567
- bodyClass: "page_" + db.sqlsanitize(homeCfg),
568
- },
569
- contents
570
- );
571
- } else res.redirect(homeCfg);
570
+ const db_page = Page.findOne({ name: homeCfg });
571
+ if (db_page)
572
+ wrap(
573
+ await db_page.run(req.query, { res, req }),
574
+ homeCfg,
575
+ db_page.title,
576
+ db_page.description
577
+ );
578
+ else {
579
+ const group = PageGroup.findOne({ name: homeCfg });
580
+ if (group) {
581
+ const eligible = await getEligiblePage(group, req, res);
582
+ if (typeof eligible === "string") wrap(eligible);
583
+ else if (eligible) {
584
+ if (!eligible.isReload)
585
+ wrap(
586
+ await eligible.run(req.query, { res, req }),
587
+ homeCfg,
588
+ eligible.title,
589
+ eligible.description
590
+ );
591
+ } else wrap(req.__("%s has no eligible page", group.name), homeCfg);
592
+ } else res.redirect(homeCfg);
593
+ }
572
594
  return true;
573
595
  }
574
596
  };
package/routes/page.js CHANGED
@@ -5,21 +5,20 @@
5
5
  */
6
6
 
7
7
  const Router = require("express-promise-router");
8
- const { UAParser } = require("ua-parser-js");
9
8
 
10
9
  const Page = require("@saltcorn/data/models/page");
11
10
  const PageGroup = require("@saltcorn/data/models/page_group");
12
11
  const Trigger = require("@saltcorn/data/models/trigger");
13
- const { getState, features } = require("@saltcorn/data/db/state");
12
+ const { getState } = require("@saltcorn/data/db/state");
14
13
  const {
15
14
  error_catcher,
16
15
  scan_for_page_title,
17
16
  isAdmin,
18
17
  sendHtmlFile,
18
+ getEligiblePage,
19
19
  } = require("../routes/utils.js");
20
20
  const { isTest } = require("@saltcorn/data/utils");
21
21
  const { add_edit_bar } = require("../markup/admin.js");
22
- const { script, domReady } = require("@saltcorn/markup/tags");
23
22
  const { traverseSync } = require("@saltcorn/data/models/layout");
24
23
  const { run_action_column } = require("@saltcorn/data/plugin-helper");
25
24
  const db = require("@saltcorn/data/db");
@@ -77,74 +76,41 @@ const runPage = async (page, req, res, tic) => {
77
76
  );
78
77
  } else {
79
78
  getState().log(2, `Page ${page.name} not authorized`);
80
- res.status(404).sendWrap(` page`, req.__("Page %s not found", page.name));
79
+ res
80
+ .status(404)
81
+ .sendWrap(
82
+ req.__("Internal Error"),
83
+ req.__("Page %s not found", page.name)
84
+ );
81
85
  }
82
86
  };
83
87
 
84
- const uaDevice = (req) => {
85
- const uaParser = new UAParser(req.headers["user-agent"]);
86
- const device = uaParser.getDevice();
87
- if (!device.type) return "web";
88
- else return device.type;
89
- };
90
-
91
- const screenInfoFromCfg = (req) => {
92
- const device = uaDevice(req);
93
- const uaScreenInfos = getState().getConfig("user_agent_screen_infos", {});
94
- return { device, ...uaScreenInfos[device] };
95
- };
96
-
97
88
  const runPageGroup = async (pageGroup, req, res, tic) => {
98
89
  const role = req.user && req.user.id ? req.user.role_id : 100;
99
90
  if (role <= pageGroup.min_role) {
100
- if (pageGroup.members.length === 0) {
101
- getState().log(2, `Pagegroup ${pageGroup.name} has no members`);
91
+ const eligible = await getEligiblePage(pageGroup, req, res);
92
+ if (typeof eligible === "string") {
93
+ getState().log(2, eligible);
94
+ res.status(400).sendWrap(req.__("Internal Error"), eligible);
95
+ } else if (eligible) {
96
+ if (!eligible.isReload) await runPage(eligible, req, res, tic);
97
+ } else {
98
+ getState().log(2, `Pagegroup ${pageGroup.name} has no eligible page`);
102
99
  res
103
- .status(400)
100
+ .status(404)
104
101
  .sendWrap(
105
- ` page`,
106
- req.__("Pagegroup %s has no members", pageGroup.name)
107
- );
108
- } else {
109
- let screenInfos = null;
110
- if (req.cookies["_sc_screen_info_"]) {
111
- screenInfos = JSON.parse(req.cookies["_sc_screen_info_"]);
112
- screenInfos.device = uaDevice(req);
113
- } else {
114
- const strategy = getState().getConfig(
115
- "missing_screen_info_strategy",
116
- "guess_from_user_agent"
102
+ req.__("Internal Error"),
103
+ req.__("%s has no eligible page", pageGroup.name)
117
104
  );
118
- if (strategy === "guess_from_user_agent")
119
- screenInfos = screenInfoFromCfg(req);
120
- else if (strategy === "reload" && req.query.is_reload !== "true") {
121
- return res.sendWrap(
122
- script(
123
- domReady(`
124
- setScreenInfoCookie();
125
- window.location = updateQueryStringParameter(window.location.href, "is_reload", true);`)
126
- )
127
- );
128
- }
129
- }
130
- const eligiblePage = await pageGroup.getEligiblePage(
131
- screenInfos,
132
- req.user ? req.user : { role_id: features.public_user_role },
133
- req.getLocale()
134
- );
135
- if (eligiblePage) await runPage(eligiblePage, req, res, tic);
136
- else {
137
- getState().log(2, `Pagegroup ${pageGroup.name} has no eligible page`);
138
- res
139
- .status(404)
140
- .sendWrap(` page`, req.__("%s has no eligible page", pageGroup.name));
141
- }
142
105
  }
143
106
  } else {
144
107
  getState().log(2, `Pagegroup ${pageGroup.name} not authorized`);
145
108
  res
146
109
  .status(404)
147
- .sendWrap(` page`, req.__("Pagegroup %s not found", pageGroup.name));
110
+ .sendWrap(
111
+ req.__("Internal Error"),
112
+ req.__("Pagegroup %s not found", pageGroup.name)
113
+ );
148
114
  }
149
115
  };
150
116
 
@@ -164,7 +130,10 @@ router.get(
164
130
  getState().log(2, `Page ${pagename} not found or not authorized`);
165
131
  res
166
132
  .status(404)
167
- .sendWrap(`${pagename} page`, req.__("Page %s not found", pagename));
133
+ .sendWrap(
134
+ req.__("Internal Error"),
135
+ req.__("Page %s not found", pagename)
136
+ );
168
137
  }
169
138
  }
170
139
  })
@@ -242,12 +242,14 @@ const pageBuilderData = async (req, context) => {
242
242
  /**
243
243
  * Root pages configuration Form
244
244
  * Allows to configure root page for each role
245
- * @param {object[]} pages list of pages
246
- * @param {object[]} roles - list of roles
247
- * @param {object} req - request
245
+ * Groups are listed under the pages (perhaps we need something to switch between input-selects)
246
+ * @param {Page[]} pages list of pages
247
+ * @param {PageGroup[]} pageGroups list of page groups
248
+ * @param {Row[]} roles - list of roles
249
+ * @param {any} req - request
248
250
  * @returns {Form} return Form
249
251
  */
250
- const getRootPageForm = (pages, roles, req) => {
252
+ const getRootPageForm = (pages, pageGroups, roles, req) => {
251
253
  const form = new Form({
252
254
  action: "/pageedit/set_root_page",
253
255
  noSubmitButton: true,
@@ -261,7 +263,14 @@ const getRootPageForm = (pages, roles, req) => {
261
263
  name: r.role,
262
264
  label: r.role,
263
265
  input_type: "select",
264
- options: ["", ...pages.map((p) => p.name)],
266
+ options: [
267
+ "",
268
+ ...pages.map((p) => p.name),
269
+ ...pageGroups.map((g) => ({
270
+ label: `${g.name} (group)`,
271
+ value: g.name,
272
+ })),
273
+ ],
265
274
  })
266
275
  ),
267
276
  });
@@ -338,7 +347,7 @@ router.get(
338
347
  title: req.__("Root pages"),
339
348
  titleAjaxIndicator: true,
340
349
  contents: renderForm(
341
- getRootPageForm(pages, roles, req),
350
+ getRootPageForm(pages, pageGroups, roles, req),
342
351
  req.csrfToken()
343
352
  ),
344
353
  },
@@ -704,8 +713,9 @@ router.post(
704
713
  isAdmin,
705
714
  error_catcher(async (req, res) => {
706
715
  const pages = await Page.find({}, { orderBy: "name" });
716
+ const pageGroups = await PageGroup.find({}, { orderBy: "name" });
707
717
  const roles = await User.get_roles();
708
- const form = await getRootPageForm(pages, roles, req);
718
+ const form = getRootPageForm(pages, pageGroups, roles, req);
709
719
  const valres = form.validate(req.body);
710
720
  if (valres.success) {
711
721
  const home_page_by_role =
package/routes/utils.js CHANGED
@@ -10,9 +10,10 @@ const {
10
10
  getState,
11
11
  getTenant,
12
12
  get_other_domain_tenant,
13
+ features,
13
14
  } = require("@saltcorn/data/db/state");
14
15
  const { get_base_url } = require("@saltcorn/data/models/config");
15
- const { input } = require("@saltcorn/markup/tags");
16
+ const { input, script, domReady } = require("@saltcorn/markup/tags");
16
17
  const session = require("express-session");
17
18
  const cookieSession = require("cookie-session");
18
19
  const is = require("contractis/is");
@@ -28,6 +29,7 @@ const {
28
29
  flash_restart,
29
30
  } = require("../markup/admin.js");
30
31
  const path = require("path");
32
+ const { UAParser } = require("ua-parser-js");
31
33
 
32
34
  const get_sys_info = async () => {
33
35
  const disks = await si.fsSize();
@@ -416,6 +418,12 @@ const sendHtmlFile = async (req, res, file) => {
416
418
  }
417
419
  };
418
420
 
421
+ /**
422
+ * set the minimum role for a model (Page, View, ...)
423
+ * @param {any} req
424
+ * @param {any} res
425
+ * @param {any} model
426
+ */
419
427
  const setRole = async (req, res, model) => {
420
428
  const { id } = req.params;
421
429
  const role = req.body.role;
@@ -433,6 +441,70 @@ const setRole = async (req, res, model) => {
433
441
  } else res.json({ okay: true, responseText: message });
434
442
  };
435
443
 
444
+ /**
445
+ * internal helper to get the device type from user agent
446
+ * @param {any} req
447
+ * @returns device type as string
448
+ */
449
+ const uaDevice = (req) => {
450
+ const uaParser = new UAParser(req.headers["user-agent"]);
451
+ const device = uaParser.getDevice();
452
+ if (!device.type) return "web";
453
+ else return device.type;
454
+ };
455
+
456
+ /**
457
+ * internal helper to get the device specific screen info from config
458
+ * @param {any} req
459
+ * @returns object with device type and screen info
460
+ */
461
+ const screenInfoFromCfg = (req) => {
462
+ const device = uaDevice(req);
463
+ const uaScreenInfos = getState().getConfig("user_agent_screen_infos", {});
464
+ return { device, ...uaScreenInfos[device] };
465
+ };
466
+
467
+ /**
468
+ * get the eligible page for pagegroup with respect to the screen infos
469
+ * @param {PageGroup} pageGroup
470
+ * @param {any} req
471
+ * @param {any} res
472
+ * @returns eligible page an error message or an object with reload flag
473
+ */
474
+ const getEligiblePage = async (pageGroup, req, res) => {
475
+ if (pageGroup.members.length === 0)
476
+ return req.__("Pagegroup %s has no members", pageGroup.name);
477
+ else {
478
+ let screenInfos = null;
479
+ if (req.cookies["_sc_screen_info_"]) {
480
+ screenInfos = JSON.parse(req.cookies["_sc_screen_info_"]);
481
+ screenInfos.device = uaDevice(req);
482
+ } else {
483
+ const strategy = getState().getConfig(
484
+ "missing_screen_info_strategy",
485
+ "guess_from_user_agent"
486
+ );
487
+ if (strategy === "guess_from_user_agent")
488
+ screenInfos = screenInfoFromCfg(req);
489
+ else if (strategy === "reload" && req.query.is_reload !== "true") {
490
+ res.sendWrap(
491
+ script(
492
+ domReady(`
493
+ setScreenInfoCookie();
494
+ window.location = updateQueryStringParameter(window.location.href, "is_reload", true);`)
495
+ )
496
+ );
497
+ return { isReload: true };
498
+ }
499
+ }
500
+ return await pageGroup.getEligiblePage(
501
+ screenInfos,
502
+ req.user ? req.user : { role_id: features.public_user_role },
503
+ req.getLocale()
504
+ );
505
+ }
506
+ };
507
+
436
508
  module.exports = {
437
509
  sqlsanitize,
438
510
  csrfField,
@@ -451,4 +523,5 @@ module.exports = {
451
523
  admin_config_route,
452
524
  sendHtmlFile,
453
525
  setRole,
526
+ getEligiblePage,
454
527
  };
@@ -419,9 +419,10 @@ describe("Fieldview config", () => {
419
419
  field_name: "pages",
420
420
  fieldview: "progress_bar",
421
421
  })
422
+ .expect(toInclude(`<div class="form-group"><div class="form-check">`))
422
423
  .expect(
423
424
  toInclude(
424
- `<div class="form-group"><div><label for="inputmax">max</label></div><div><input type="number" class="form-control item-menu" data-fieldname="max" name="max" id="inputmax" step="1" required></div></div><div class="form-group"><div><label for="inputbar_color">Bar color</label></div><div><input type="color" class="form-control item-menu" data-fieldname="bar_color" name="bar_color" id="inputbar_color"></div></div><div class="form-group"><div><label for="inputbg_color">Background color</label></div><div><input type="color" class="form-control item-menu" data-fieldname="bg_color" name="bg_color" id="inputbg_color"></div></div><div class="form-group"><div><label for="inputpx_height">Height in px</label></div><div><input type="number" class="form-control item-menu" data-fieldname="px_height" name="px_height" id="inputpx_height" step="1"></div></div>`
425
+ `<div><label for="inputpx_height">Height in px</label></div><div><input type="number" class="form-control item-menu" data-fieldname="px_height" name="px_height" id="inputpx_height" step="1"></div>`
425
426
  )
426
427
  );
427
428
  });
@@ -135,7 +135,7 @@ describe("view with routes", () => {
135
135
  describe("render view on page", () => {
136
136
  it("should show edit", async () => {
137
137
  const view = await View.findOne({ name: "authorshow" });
138
- View.update({ default_render_page: "a_page" }, view.id);
138
+ await View.update({ default_render_page: "a_page" }, view.id);
139
139
  const app = await getApp({ disableCsrf: true });
140
140
  await request(app)
141
141
  .get("/view/authorshow?id=1")
@@ -151,7 +151,7 @@ describe("render view with slug", () => {
151
151
  const slugOpts = await table.slug_options();
152
152
  const slugOpt = slugOpts.find((so) => so.label === "/:id");
153
153
  expect(!!slugOpt).toBe(true);
154
- View.update({ default_render_page: null, slug: slugOpt }, view.id);
154
+ await View.update({ default_render_page: null, slug: slugOpt }, view.id);
155
155
  const app = await getApp({ disableCsrf: true });
156
156
  await request(app)
157
157
  .get("/view/authorlist")
@@ -171,7 +171,7 @@ describe("render view with slug", () => {
171
171
  const slugOpts = await table.slug_options();
172
172
  const slugOpt = slugOpts.find((so) => so.label === "/slugify-author");
173
173
  expect(!!slugOpt).toBe(true);
174
- View.update({ default_render_page: null, slug: slugOpt }, view.id);
174
+ await View.update({ default_render_page: null, slug: slugOpt }, view.id);
175
175
  const app = await getApp({ disableCsrf: true });
176
176
  await request(app)
177
177
  .get("/view/authorlist")
@@ -868,6 +868,63 @@ describe("relation path to query and state", () => {
868
868
  });
869
869
  });
870
870
 
871
+ describe("edit-in-edit with relation path and legacy", () => {
872
+ it("edit-in-edit with relation path one layer", async () => {
873
+ const app = await getApp({ disableCsrf: true });
874
+ const loginCookie = await getAdminLoginCookie();
875
+ await request(app)
876
+ .get("/view/edit_department_with_edit_in_edit_legacy?id=1")
877
+ .set("Cookie", loginCookie)
878
+ .expect(toInclude("add_repeater"));
879
+
880
+ // TODO post
881
+ });
882
+
883
+ it("edit-in-edit with relation path two layer", async () => {
884
+ const app = await getApp({ disableCsrf: true });
885
+ const loginCookie = await getAdminLoginCookie();
886
+ await request(app)
887
+ .get("/view/edit_cover_with_edit_artist_on_album_rel_path?id=1")
888
+ .set("Cookie", loginCookie)
889
+ .expect(toInclude("add_repeater"));
890
+
891
+ // TODO post
892
+ });
893
+
894
+ it("edit-in-edit legacy one layer", async () => {
895
+ const app = await getApp({ disableCsrf: true });
896
+ const loginCookie = await getAdminLoginCookie();
897
+ await request(app)
898
+ .get("/view/edit_department_with_edit_in_edit_legacy?id=1")
899
+ .set("Cookie", loginCookie)
900
+ .expect(toInclude("add_repeater"));
901
+
902
+ // TODO post
903
+ });
904
+
905
+ it("edit-in-edit with relation path two layer", async () => {
906
+ const app = await getApp({ disableCsrf: true });
907
+ const loginCookie = await getAdminLoginCookie();
908
+ await request(app)
909
+ .get("/view/edit_cover_with_edit_artist_on_album_rel_path?id=1")
910
+ .set("Cookie", loginCookie)
911
+ .expect(toInclude("add_repeater"));
912
+
913
+ // TODO post
914
+ });
915
+
916
+ it("edit-in-edit legacy two layer", async () => {
917
+ const app = await getApp({ disableCsrf: true });
918
+ const loginCookie = await getAdminLoginCookie();
919
+ await request(app)
920
+ .get("/view/edit_cover_with_edit_artist_on_album_legacy?id=1")
921
+ .set("Cookie", loginCookie)
922
+ .expect(toInclude("add_repeater"));
923
+
924
+ // TODO post
925
+ });
926
+ });
927
+
871
928
  describe("legacy relations with relation path", () => {
872
929
  it("Independent feed", async () => {
873
930
  const app = await getApp({ disableCsrf: true });
@@ -905,7 +962,6 @@ describe("legacy relations with relation path", () => {
905
962
  it("Own same table subview", async () => {
906
963
  const app = await getApp({ disableCsrf: true });
907
964
  const loginCookie = await getAdminLoginCookie();
908
-
909
965
  await request(app)
910
966
  .get("/view/show_album_with_subview_new_relation_path?id=1")
911
967
  .set("Cookie", loginCookie)
@@ -915,4 +971,17 @@ describe("legacy relations with relation path", () => {
915
971
  .set("Cookie", loginCookie)
916
972
  .expect(toInclude("album B"));
917
973
  });
974
+
975
+ it("edit-view with show-subview same table", async () => {
976
+ const app = await getApp({ disableCsrf: true });
977
+ const loginCookie = await getAdminLoginCookie();
978
+ await request(app)
979
+ .get("/view/authoredit_with_show")
980
+ .set("Cookie", loginCookie)
981
+ .expect(toSucceed);
982
+ await request(app)
983
+ .get("/view/authoredit_with_show?id=1")
984
+ .set("Cookie", loginCookie)
985
+ .expect(toInclude(["Herman Melville", "agi"]));
986
+ });
918
987
  });