@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 +11 -6
- package/locales/en.json +3 -1
- package/locales/es.json +5 -4
- package/package.json +8 -8
- package/public/saltcorn-common.js +17 -2
- package/routes/admin.js +68 -23
- package/routes/homepage.js +38 -16
- package/routes/page.js +27 -58
- package/routes/pageedit.js +17 -7
- package/routes/utils.js +74 -1
- package/tests/fields.test.js +2 -1
- package/tests/view.test.js +73 -4
package/auth/testhelp.js
CHANGED
|
@@ -29,24 +29,29 @@ const toRedirect = (loc) => (res) => {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
*
|
|
32
|
-
* @param {
|
|
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
|
-
(
|
|
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 "${
|
|
43
|
+
`Expected status ${expCode} when lookinng for "${exp}", received ${res.statusCode}`
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
if (
|
|
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 ${
|
|
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
|
|
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
|
+
"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.
|
|
11
|
-
"@saltcorn/builder": "0.9.
|
|
12
|
-
"@saltcorn/data": "0.9.
|
|
13
|
-
"@saltcorn/admin-models": "0.9.
|
|
14
|
-
"@saltcorn/filemanager": "0.9.
|
|
15
|
-
"@saltcorn/markup": "0.9.
|
|
16
|
-
"@saltcorn/sbadmin2": "0.9.
|
|
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
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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",
|
package/routes/homepage.js
CHANGED
|
@@ -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 =
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
)
|
|
571
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
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(
|
|
100
|
+
.status(404)
|
|
104
101
|
.sendWrap(
|
|
105
|
-
|
|
106
|
-
req.__("
|
|
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(
|
|
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(
|
|
133
|
+
.sendWrap(
|
|
134
|
+
req.__("Internal Error"),
|
|
135
|
+
req.__("Page %s not found", pagename)
|
|
136
|
+
);
|
|
168
137
|
}
|
|
169
138
|
}
|
|
170
139
|
})
|
package/routes/pageedit.js
CHANGED
|
@@ -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
|
-
*
|
|
246
|
-
* @param {
|
|
247
|
-
* @param {
|
|
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: [
|
|
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 =
|
|
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
|
};
|
package/tests/fields.test.js
CHANGED
|
@@ -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
|
|
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
|
});
|
package/tests/view.test.js
CHANGED
|
@@ -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
|
});
|