@saltcorn/server 0.9.1-beta.0 → 0.9.1-beta.2
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/help/Event types.tmd +71 -0
- package/locales/en.json +5 -1
- package/package.json +8 -8
- package/public/saltcorn-common.js +55 -32
- package/public/saltcorn.js +1 -10
- package/routes/actions.js +1 -0
- package/routes/common_lists.js +5 -1
- package/routes/homepage.js +11 -10
- package/routes/page.js +19 -15
- package/routes/pageedit.js +50 -3
- package/routes/utils.js +36 -0
- package/tests/page.test.js +94 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
The event type for triggers determines when the chosen action should be run.
|
|
2
|
+
The different event types options come from a variety of the types and sources.
|
|
3
|
+
|
|
4
|
+
These events also form the basis of the event log. Use the log settings to enable or disable
|
|
5
|
+
recording of the occurrence of events.
|
|
6
|
+
|
|
7
|
+
## Database events
|
|
8
|
+
|
|
9
|
+
These conditions are triggered by changes to rows in tables. Together with the event type
|
|
10
|
+
a specific table is chosen. The individual conditions are:
|
|
11
|
+
|
|
12
|
+
**Insert**: run the action when a new row is inserted in the table. This is a good choice
|
|
13
|
+
when a table itself represents actions to be carried out; for instance a table of
|
|
14
|
+
outbound emails would have a trigger with When = Insert and Action = send_email
|
|
15
|
+
|
|
16
|
+
**Update**: run this action when changes are made to an existing row. The old row can
|
|
17
|
+
be accessed with the `old_row` variable.
|
|
18
|
+
|
|
19
|
+
**Delete**: run this action when a row is deleted
|
|
20
|
+
|
|
21
|
+
## Periodic events
|
|
22
|
+
|
|
23
|
+
These triggers are run periodically at different times.
|
|
24
|
+
|
|
25
|
+
**Weekly**: run this once a week.
|
|
26
|
+
|
|
27
|
+
**Daily**: run this once a day.
|
|
28
|
+
|
|
29
|
+
**Hourly**: run this once an hour.
|
|
30
|
+
|
|
31
|
+
**Often**: run this every 5 minutes.
|
|
32
|
+
|
|
33
|
+
## User-based events
|
|
34
|
+
|
|
35
|
+
**PageLoad**: run this whenever a page or view is loaded. If you set up the event log to
|
|
36
|
+
record these events you can use this as a basis for an analytics system.
|
|
37
|
+
|
|
38
|
+
**Login**: run this whenever a user log in successfully
|
|
39
|
+
|
|
40
|
+
**LoginFailed**: run this whenever a user login failed
|
|
41
|
+
|
|
42
|
+
**UserVerified**: run this when a user is verified, if an appropriate module for
|
|
43
|
+
user verification is enabled.
|
|
44
|
+
|
|
45
|
+
## System-based events
|
|
46
|
+
|
|
47
|
+
**Error**: run this whenever an error occurs
|
|
48
|
+
|
|
49
|
+
**Startup**: run this whenever this saltcorn process initializes.
|
|
50
|
+
|
|
51
|
+
## Other events
|
|
52
|
+
|
|
53
|
+
**Never**: this trigger is never run on its own. However triggers that are marked as never
|
|
54
|
+
can be chosen as the target action for a button in the UI. Use this if you have a complex
|
|
55
|
+
configuration for an action that needs to be run in response to a button click, or if you
|
|
56
|
+
have a configuration that needs to be reused between two different buttons in two different
|
|
57
|
+
views. You can also use this to switch off a trigger that is running on a different event
|
|
58
|
+
type without deleting it.
|
|
59
|
+
|
|
60
|
+
**API call**: this trigger can be run in response to an inbound API call. To see the URL
|
|
61
|
+
and further help, click the help icon next to the "API call" label in the trigger list.
|
|
62
|
+
|
|
63
|
+
## Custom events
|
|
64
|
+
|
|
65
|
+
You can create your own event type which can then be triggered with an emit_event action
|
|
66
|
+
or the `emitEvent` call in a run js code action
|
|
67
|
+
|
|
68
|
+
## Events supplied by modules
|
|
69
|
+
|
|
70
|
+
Modules can provide new event types. For instance the mqtt module provides an event
|
|
71
|
+
type based on receiving new messages.
|
package/locales/en.json
CHANGED
|
@@ -1273,5 +1273,9 @@
|
|
|
1273
1273
|
"Body size limit (Kb)": "Body size limit (Kb)",
|
|
1274
1274
|
"Maximum request body size in kilobytes": "Maximum request body size in kilobytes",
|
|
1275
1275
|
"URL encoded size limit (Kb)": "URL encoded size limit (Kb)",
|
|
1276
|
-
"Maximum URL encoded request size in kilobytes": "Maximum URL encoded request size in kilobytes"
|
|
1276
|
+
"Maximum URL encoded request size in kilobytes": "Maximum URL encoded request size in kilobytes",
|
|
1277
|
+
"HTML file": "HTML file",
|
|
1278
|
+
"HTML file to use as page content": "HTML file to use as page content",
|
|
1279
|
+
"Offline mode: cannot load file": "Offline mode: cannot load file",
|
|
1280
|
+
"None - use drag and drop builder": "None - use drag and drop builder"
|
|
1277
1281
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.1-beta.
|
|
3
|
+
"version": "0.9.1-beta.2",
|
|
4
4
|
"description": "Server app for Saltcorn, open-source no-code platform",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@saltcorn/base-plugin": "0.9.1-beta.
|
|
10
|
-
"@saltcorn/builder": "0.9.1-beta.
|
|
11
|
-
"@saltcorn/data": "0.9.1-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.9.1-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.9.1-beta.
|
|
14
|
-
"@saltcorn/markup": "0.9.1-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.9.1-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.9.1-beta.2",
|
|
10
|
+
"@saltcorn/builder": "0.9.1-beta.2",
|
|
11
|
+
"@saltcorn/data": "0.9.1-beta.2",
|
|
12
|
+
"@saltcorn/admin-models": "0.9.1-beta.2",
|
|
13
|
+
"@saltcorn/filemanager": "0.9.1-beta.2",
|
|
14
|
+
"@saltcorn/markup": "0.9.1-beta.2",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.9.1-beta.2",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
|
@@ -381,6 +381,17 @@ function get_form_record(e_in, select_labels) {
|
|
|
381
381
|
? $(`form[data-viewname=${e_in.viewname}]`)
|
|
382
382
|
: e_in.closest(".form-namespace");
|
|
383
383
|
|
|
384
|
+
const form = $(e).closest("form");
|
|
385
|
+
|
|
386
|
+
const rowVals = form.attr("data-row-values");
|
|
387
|
+
if (rowVals)
|
|
388
|
+
try {
|
|
389
|
+
const initRow = JSON.parse(decodeURIComponent(rowVals));
|
|
390
|
+
Object.assign(rec, initRow);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error(error);
|
|
393
|
+
}
|
|
394
|
+
|
|
384
395
|
e.find("input[name],select[name],textarea[name]").each(function () {
|
|
385
396
|
const name = $(this).attr("data-fieldname") || $(this).attr("name");
|
|
386
397
|
if (select_labels && $(this).prop("tagName").toLowerCase() === "select")
|
|
@@ -585,8 +596,11 @@ function initialize_page() {
|
|
|
585
596
|
$(this).find("span.current time").attr("datetime"); // ||
|
|
586
597
|
//$(this).children("span.current").html();
|
|
587
598
|
}
|
|
588
|
-
|
|
599
|
+
if (type === "Bool") {
|
|
600
|
+
current = current === "true";
|
|
601
|
+
}
|
|
589
602
|
var is_key = type?.startsWith("Key:");
|
|
603
|
+
const resetHtml = this.outerHTML;
|
|
590
604
|
const opts = encodeURIComponent(
|
|
591
605
|
JSON.stringify({
|
|
592
606
|
url,
|
|
@@ -597,6 +611,7 @@ function initialize_page() {
|
|
|
597
611
|
type,
|
|
598
612
|
is_key,
|
|
599
613
|
schema,
|
|
614
|
+
resetHtml,
|
|
600
615
|
...(decimalPlaces ? { decimalPlaces } : {}),
|
|
601
616
|
})
|
|
602
617
|
);
|
|
@@ -649,7 +664,11 @@ function initialize_page() {
|
|
|
649
664
|
: ""
|
|
650
665
|
}
|
|
651
666
|
<input type="${
|
|
652
|
-
type === "Integer" || type === "Float"
|
|
667
|
+
type === "Integer" || type === "Float"
|
|
668
|
+
? "number"
|
|
669
|
+
: type === "Bool"
|
|
670
|
+
? "checkbox"
|
|
671
|
+
: "text"
|
|
653
672
|
}" ${
|
|
654
673
|
type === "Float"
|
|
655
674
|
? `step="${
|
|
@@ -660,7 +679,13 @@ function initialize_page() {
|
|
|
660
679
|
: "any"
|
|
661
680
|
}"`
|
|
662
681
|
: ""
|
|
663
|
-
} name="${key}"
|
|
682
|
+
} name="${key}" ${
|
|
683
|
+
type === "Bool"
|
|
684
|
+
? current
|
|
685
|
+
? "checked"
|
|
686
|
+
: ""
|
|
687
|
+
: `value="${escapeHtml(current)}"`
|
|
688
|
+
}>
|
|
664
689
|
<button type="submit" class="btn btn-sm btn-primary">OK</button>
|
|
665
690
|
<button onclick="cancel_inline_edit(event, '${opts}')" type="button" class="btn btn-sm btn-danger"><i class="fas fa-times"></i></button>
|
|
666
691
|
</form>`
|
|
@@ -723,7 +748,26 @@ function initialize_page() {
|
|
|
723
748
|
cm.on(
|
|
724
749
|
"change",
|
|
725
750
|
$.debounce(() => {
|
|
726
|
-
$(el).
|
|
751
|
+
if ($(el).hasClass("validate-statements")) {
|
|
752
|
+
try {
|
|
753
|
+
let AsyncFunction = Object.getPrototypeOf(
|
|
754
|
+
async function () {}
|
|
755
|
+
).constructor;
|
|
756
|
+
AsyncFunction(cm.getValue());
|
|
757
|
+
$(el).closest("form").trigger("change");
|
|
758
|
+
} catch (e) {
|
|
759
|
+
const form = $(el).closest("form");
|
|
760
|
+
const errorArea = form.parent().find(".full-form-error");
|
|
761
|
+
if (errorArea.length) errorArea.text(e.message);
|
|
762
|
+
else
|
|
763
|
+
form
|
|
764
|
+
.parent()
|
|
765
|
+
.append(
|
|
766
|
+
`<p class="text-danger full-form-error">${e.message}</p>`
|
|
767
|
+
);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
} else $(el).closest("form").trigger("change");
|
|
727
771
|
}),
|
|
728
772
|
500,
|
|
729
773
|
null,
|
|
@@ -777,33 +821,7 @@ function cancel_inline_edit(e, opts1) {
|
|
|
777
821
|
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
778
822
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
779
823
|
var form = $(e.target).closest("form");
|
|
780
|
-
|
|
781
|
-
if (opts.schema) {
|
|
782
|
-
json_fk_opt = form.find(`option[value="${opts.current}"]`).text();
|
|
783
|
-
}
|
|
784
|
-
form.replaceWith(`<div
|
|
785
|
-
data-inline-edit-field="${opts.key}"
|
|
786
|
-
${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
|
|
787
|
-
${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
|
|
788
|
-
${opts.current ? `data-inline-edit-current="${opts.current}"` : ""}
|
|
789
|
-
${
|
|
790
|
-
opts.current_label
|
|
791
|
-
? `data-inline-edit-current-label="${opts.current_label}"`
|
|
792
|
-
: ""
|
|
793
|
-
}
|
|
794
|
-
${
|
|
795
|
-
opts.schema
|
|
796
|
-
? `data-inline-edit-schema="${encodeURIComponent(
|
|
797
|
-
JSON.stringify(opts.schema)
|
|
798
|
-
)}"`
|
|
799
|
-
: ""
|
|
800
|
-
}
|
|
801
|
-
data-inline-edit-dest-url="${opts.url}">
|
|
802
|
-
<span class="current">${
|
|
803
|
-
json_fk_opt || opts.current_label || opts.current
|
|
804
|
-
}</span>
|
|
805
|
-
<i class="editicon ${!isNode ? "visible" : ""} fas fa-edit ms-1"></i>
|
|
806
|
-
</div>`);
|
|
824
|
+
form.replaceWith(opts.resetHtml);
|
|
807
825
|
initialize_page();
|
|
808
826
|
}
|
|
809
827
|
|
|
@@ -811,7 +829,8 @@ function inline_submit_success(e, form, opts) {
|
|
|
811
829
|
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
812
830
|
const formDataArray = form.serializeArray();
|
|
813
831
|
if (opts) {
|
|
814
|
-
let
|
|
832
|
+
let fdEntry = formDataArray.find((f) => f.name == opts.key);
|
|
833
|
+
let rawVal = opts.type === "Bool" ? !!fdEntry : fdEntry.value;
|
|
815
834
|
let val =
|
|
816
835
|
opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
|
|
817
836
|
? form.find("select").find("option:selected").text()
|
|
@@ -846,9 +865,13 @@ function inline_submit_success(e, form, opts) {
|
|
|
846
865
|
function inline_ajax_submit(e, opts1) {
|
|
847
866
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
848
867
|
e.preventDefault();
|
|
868
|
+
|
|
849
869
|
var form = $(e.target).closest("form");
|
|
850
870
|
var form_data = form.serialize();
|
|
851
871
|
var url = form.attr("action");
|
|
872
|
+
if (opts.type === "Bool" && !form_data.includes(`${opts.key}=on`)) {
|
|
873
|
+
form_data += `&${opts.key}=off`;
|
|
874
|
+
}
|
|
852
875
|
$.ajax(url, {
|
|
853
876
|
type: "POST",
|
|
854
877
|
headers: {
|
package/public/saltcorn.js
CHANGED
|
@@ -397,16 +397,7 @@ function saveAndContinue(e, k) {
|
|
|
397
397
|
error: function (request) {
|
|
398
398
|
var ct = request.getResponseHeader("content-type") || "";
|
|
399
399
|
if (ct.startsWith && ct.startsWith("application/json")) {
|
|
400
|
-
|
|
401
|
-
if (errorArea.length) {
|
|
402
|
-
errorArea.text(request.responseJSON.error);
|
|
403
|
-
} else {
|
|
404
|
-
form
|
|
405
|
-
.parent()
|
|
406
|
-
.append(
|
|
407
|
-
`<p class="text-danger full-form-error">${request.responseJSON.error}</p>`
|
|
408
|
-
);
|
|
409
|
-
}
|
|
400
|
+
notifyAlert({ type: "danger", text: request.responseJSON.error });
|
|
410
401
|
} else {
|
|
411
402
|
$("#page-inner-content").html(request.responseText);
|
|
412
403
|
initialize_page();
|
package/routes/actions.js
CHANGED
|
@@ -171,6 +171,7 @@ const triggerForm = async (req, trigger) => {
|
|
|
171
171
|
required: true,
|
|
172
172
|
options: Trigger.when_options.map((t) => ({ value: t, label: t })),
|
|
173
173
|
sublabel: req.__("Event type which runs the trigger"),
|
|
174
|
+
help: { topic: "Event types" },
|
|
174
175
|
attributes: {
|
|
175
176
|
explainers: {
|
|
176
177
|
Often: req.__("Every 5 minutes"),
|
package/routes/common_lists.js
CHANGED
|
@@ -344,7 +344,11 @@ const getPageList = (rows, roles, req, { tagId, domId, showList } = {}) => {
|
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
346
|
label: req.__("Edit"),
|
|
347
|
-
key: (r) =>
|
|
347
|
+
key: (r) =>
|
|
348
|
+
link(
|
|
349
|
+
`/pageedit/${!r.html_file ? "edit" : "edit-properties"}/${r.name}`,
|
|
350
|
+
req.__(!r.html_file ? "Edit" : "Edit properties")
|
|
351
|
+
),
|
|
348
352
|
},
|
|
349
353
|
!tagId
|
|
350
354
|
? {
|
package/routes/homepage.js
CHANGED
|
@@ -21,7 +21,7 @@ const { get_latest_npm_version } = require("@saltcorn/data/models/config");
|
|
|
21
21
|
const packagejson = require("../package.json");
|
|
22
22
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
23
23
|
const { fileUploadForm } = require("../markup/forms");
|
|
24
|
-
const { get_base_url } = require("./utils.js");
|
|
24
|
+
const { get_base_url, sendHtmlFile } = require("./utils.js");
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Tables List
|
|
@@ -480,15 +480,16 @@ const get_config_response = async (role_id, res, req) => {
|
|
|
480
480
|
|
|
481
481
|
if (db_page) {
|
|
482
482
|
const contents = await db_page.run(req.query, { res, req });
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
483
|
+
if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
|
|
484
|
+
else
|
|
485
|
+
res.sendWrap(
|
|
486
|
+
{
|
|
487
|
+
title: db_page.title,
|
|
488
|
+
description: db_page.description,
|
|
489
|
+
bodyClass: "page_" + db.sqlsanitize(homeCfg),
|
|
490
|
+
},
|
|
491
|
+
contents
|
|
492
|
+
);
|
|
492
493
|
} else res.redirect(homeCfg);
|
|
493
494
|
return true;
|
|
494
495
|
}
|
package/routes/page.js
CHANGED
|
@@ -8,11 +8,13 @@ const Router = require("express-promise-router");
|
|
|
8
8
|
|
|
9
9
|
const Page = require("@saltcorn/data/models/page");
|
|
10
10
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
11
|
+
const File = require("@saltcorn/data/models/file");
|
|
11
12
|
const { getState } = require("@saltcorn/data/db/state");
|
|
12
13
|
const {
|
|
13
14
|
error_catcher,
|
|
14
15
|
scan_for_page_title,
|
|
15
16
|
isAdmin,
|
|
17
|
+
sendHtmlFile,
|
|
16
18
|
} = require("../routes/utils.js");
|
|
17
19
|
const { add_edit_bar } = require("../markup/admin.js");
|
|
18
20
|
const { traverseSync } = require("@saltcorn/data/models/layout");
|
|
@@ -56,21 +58,23 @@ router.get(
|
|
|
56
58
|
name: pagename,
|
|
57
59
|
render_time: ms,
|
|
58
60
|
});
|
|
59
|
-
res.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
|
|
62
|
+
else
|
|
63
|
+
res.sendWrap(
|
|
64
|
+
{
|
|
65
|
+
title,
|
|
66
|
+
description: db_page.description,
|
|
67
|
+
bodyClass: "page_" + db.sqlsanitize(pagename),
|
|
68
|
+
no_menu: db_page.attributes?.no_menu,
|
|
69
|
+
} || `${pagename} page`,
|
|
70
|
+
add_edit_bar({
|
|
71
|
+
role,
|
|
72
|
+
title: db_page.name,
|
|
73
|
+
what: req.__("Page"),
|
|
74
|
+
url: `/pageedit/edit/${encodeURIComponent(db_page.name)}`,
|
|
75
|
+
contents,
|
|
76
|
+
})
|
|
77
|
+
);
|
|
74
78
|
} else {
|
|
75
79
|
if (db_page && !req.user) {
|
|
76
80
|
res.redirect(`/auth/login?dest=${encodeURIComponent(req.originalUrl)}`);
|
package/routes/pageedit.js
CHANGED
|
@@ -27,6 +27,7 @@ const {
|
|
|
27
27
|
addOnDoneRedirect,
|
|
28
28
|
is_relative_url,
|
|
29
29
|
} = require("./utils.js");
|
|
30
|
+
const { asyncMap } = require("@saltcorn/data/utils");
|
|
30
31
|
const {
|
|
31
32
|
mkTable,
|
|
32
33
|
renderForm,
|
|
@@ -39,6 +40,7 @@ const {
|
|
|
39
40
|
} = require("@saltcorn/markup");
|
|
40
41
|
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
41
42
|
const Library = require("@saltcorn/data/models/library");
|
|
43
|
+
const path = require("path");
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* @type {object}
|
|
@@ -58,6 +60,20 @@ module.exports = router;
|
|
|
58
60
|
const pagePropertiesForm = async (req, isNew) => {
|
|
59
61
|
const roles = await User.get_roles();
|
|
60
62
|
const pages = (await Page.find()).map((p) => p.name);
|
|
63
|
+
const htmlFiles = await File.find(
|
|
64
|
+
{
|
|
65
|
+
mime_super: "text",
|
|
66
|
+
mime_sub: "html",
|
|
67
|
+
},
|
|
68
|
+
{ recursive: true }
|
|
69
|
+
);
|
|
70
|
+
const htmlOptions = await asyncMap(htmlFiles, async (f) => {
|
|
71
|
+
return {
|
|
72
|
+
label: path.join(f.current_folder, f.filename),
|
|
73
|
+
value: File.absPathToServePath(f.location),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
61
77
|
const form = new Form({
|
|
62
78
|
action: addOnDoneRedirect("/pageedit/edit-properties", req),
|
|
63
79
|
fields: [
|
|
@@ -92,6 +108,24 @@ const pagePropertiesForm = async (req, isNew) => {
|
|
|
92
108
|
input_type: "select",
|
|
93
109
|
options: roles.map((r) => ({ value: r.id, label: r.role })),
|
|
94
110
|
},
|
|
111
|
+
...(htmlOptions.length > 0
|
|
112
|
+
? [
|
|
113
|
+
{
|
|
114
|
+
name: "html_file",
|
|
115
|
+
label: req.__("HTML file"),
|
|
116
|
+
sublabel: req.__("HTML file to use as page content"),
|
|
117
|
+
input_type: "select",
|
|
118
|
+
|
|
119
|
+
options: [
|
|
120
|
+
{
|
|
121
|
+
label: req.__("None - use drag and drop builder"),
|
|
122
|
+
value: "",
|
|
123
|
+
},
|
|
124
|
+
...htmlOptions,
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
]
|
|
128
|
+
: []),
|
|
95
129
|
{
|
|
96
130
|
name: "no_menu",
|
|
97
131
|
label: req.__("No menu"),
|
|
@@ -367,17 +401,30 @@ router.post(
|
|
|
367
401
|
wrap(renderForm(form, req.csrfToken()), false, req)
|
|
368
402
|
);
|
|
369
403
|
} else {
|
|
370
|
-
const { id, columns, no_menu, ...pageRow } = form.values;
|
|
404
|
+
const { id, columns, no_menu, html_file, ...pageRow } = form.values;
|
|
371
405
|
pageRow.min_role = +pageRow.min_role;
|
|
372
406
|
pageRow.attributes = { no_menu };
|
|
407
|
+
if (html_file) {
|
|
408
|
+
pageRow.layout = {
|
|
409
|
+
html_file: html_file,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
373
412
|
if (+id) {
|
|
413
|
+
const dbPage = Page.findOne({ id: id });
|
|
414
|
+
if (dbPage.layout?.html_file && !html_file) {
|
|
415
|
+
pageRow.layout = {};
|
|
416
|
+
}
|
|
374
417
|
await Page.update(+id, pageRow);
|
|
375
418
|
res.redirect(`/pageedit/`);
|
|
376
419
|
} else {
|
|
377
|
-
if (!pageRow.fixed_states) pageRow.fixed_states = {};
|
|
378
420
|
if (!pageRow.layout) pageRow.layout = {};
|
|
421
|
+
if (!pageRow.fixed_states) pageRow.fixed_states = {};
|
|
379
422
|
await Page.create(pageRow);
|
|
380
|
-
|
|
423
|
+
if (!html_file)
|
|
424
|
+
res.redirect(
|
|
425
|
+
addOnDoneRedirect(`/pageedit/edit/${pageRow.name}`, req)
|
|
426
|
+
);
|
|
427
|
+
else res.redirect(`/pageedit/`);
|
|
381
428
|
}
|
|
382
429
|
}
|
|
383
430
|
})
|
package/routes/utils.js
CHANGED
|
@@ -18,6 +18,7 @@ const cookieSession = require("cookie-session");
|
|
|
18
18
|
const is = require("contractis/is");
|
|
19
19
|
const { validateHeaderName, validateHeaderValue } = require("http");
|
|
20
20
|
const Crash = require("@saltcorn/data/models/crash");
|
|
21
|
+
const File = require("@saltcorn/data/models/file");
|
|
21
22
|
const si = require("systeminformation");
|
|
22
23
|
const {
|
|
23
24
|
config_fields_form,
|
|
@@ -25,6 +26,8 @@ const {
|
|
|
25
26
|
check_if_restart_required,
|
|
26
27
|
flash_restart,
|
|
27
28
|
} = require("../markup/admin.js");
|
|
29
|
+
const path = require("path");
|
|
30
|
+
|
|
28
31
|
const get_sys_info = async () => {
|
|
29
32
|
const disks = await si.fsSize();
|
|
30
33
|
let size = 0;
|
|
@@ -380,6 +383,38 @@ const admin_config_route = ({
|
|
|
380
383
|
);
|
|
381
384
|
};
|
|
382
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Send HTML file to client without any menu
|
|
388
|
+
* @param {any} req
|
|
389
|
+
* @param {any} res
|
|
390
|
+
* @param {string} file
|
|
391
|
+
* @returns
|
|
392
|
+
*/
|
|
393
|
+
const sendHtmlFile = async (req, res, file) => {
|
|
394
|
+
const fullPath = path.join((await File.rootFolder()).location, file);
|
|
395
|
+
const role = req.user && req.user.id ? req.user.role_id : 100;
|
|
396
|
+
try {
|
|
397
|
+
const scFile = await File.from_file_on_disk(
|
|
398
|
+
path.basename(fullPath),
|
|
399
|
+
path.dirname(fullPath)
|
|
400
|
+
);
|
|
401
|
+
if (scFile && role <= scFile.min_role_read) {
|
|
402
|
+
res.sendFile(fullPath);
|
|
403
|
+
} else {
|
|
404
|
+
return res
|
|
405
|
+
.status(404)
|
|
406
|
+
.sendWrap(req.__("An error occurred"), req.__("File not found"));
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
return res
|
|
410
|
+
.status(404)
|
|
411
|
+
.sendWrap(
|
|
412
|
+
req.__("An error occurred"),
|
|
413
|
+
e.message || req.__("An error occurred")
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
383
418
|
module.exports = {
|
|
384
419
|
sqlsanitize,
|
|
385
420
|
csrfField,
|
|
@@ -396,4 +431,5 @@ module.exports = {
|
|
|
396
431
|
is_relative_url,
|
|
397
432
|
get_sys_info,
|
|
398
433
|
admin_config_route,
|
|
434
|
+
sendHtmlFile,
|
|
399
435
|
};
|
package/tests/page.test.js
CHANGED
|
@@ -13,9 +13,42 @@ const {
|
|
|
13
13
|
} = require("../auth/testhelp");
|
|
14
14
|
const db = require("@saltcorn/data/db");
|
|
15
15
|
const Page = require("@saltcorn/data/models/page");
|
|
16
|
+
const File = require("@saltcorn/data/models/file");
|
|
17
|
+
const { existsSync } = require("fs");
|
|
18
|
+
const { join } = require("path");
|
|
19
|
+
|
|
20
|
+
let htmlFile = null;
|
|
21
|
+
|
|
22
|
+
const prepHtmlFiles = async () => {
|
|
23
|
+
const createFile = async (folder, name, content) => {
|
|
24
|
+
const scFolder = join(
|
|
25
|
+
db.connectObj.file_store,
|
|
26
|
+
db.getTenantSchema(),
|
|
27
|
+
folder
|
|
28
|
+
);
|
|
29
|
+
if (!existsSync(scFolder)) await File.new_folder(folder);
|
|
30
|
+
if (!existsSync(join(scFolder, name))) {
|
|
31
|
+
return await File.from_contents(
|
|
32
|
+
name,
|
|
33
|
+
"text/html",
|
|
34
|
+
`<html><head><title>Landing page</title></head><body><h1>${content}</h1></body></html>`,
|
|
35
|
+
1,
|
|
36
|
+
1,
|
|
37
|
+
folder
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
const file = await File.from_file_on_disk(name, scFolder);
|
|
41
|
+
file.location = File.absPathToServePath(file.location);
|
|
42
|
+
return file;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
htmlFile = await createFile("/", "fixed_page.html", "Land here");
|
|
46
|
+
await createFile("/subfolder", "fixed_page2.html", "Or Land here");
|
|
47
|
+
};
|
|
16
48
|
|
|
17
49
|
beforeAll(async () => {
|
|
18
50
|
await resetToFixtures();
|
|
51
|
+
await prepHtmlFiles();
|
|
19
52
|
});
|
|
20
53
|
afterAll(db.close);
|
|
21
54
|
|
|
@@ -36,9 +69,18 @@ describe("page create", () => {
|
|
|
36
69
|
await request(app)
|
|
37
70
|
.get("/pageedit/new")
|
|
38
71
|
.set("Cookie", loginCookie)
|
|
39
|
-
|
|
40
72
|
.expect(toInclude("A short name that will be in your URL"));
|
|
41
73
|
});
|
|
74
|
+
it("shows new with html file selector", async () => {
|
|
75
|
+
const app = await getApp({ disableCsrf: true });
|
|
76
|
+
const loginCookie = await getAdminLoginCookie();
|
|
77
|
+
await request(app)
|
|
78
|
+
.get("/pageedit/new")
|
|
79
|
+
.set("Cookie", loginCookie)
|
|
80
|
+
.expect(toInclude("HTML file"))
|
|
81
|
+
.expect(toInclude("fixed_page.html"))
|
|
82
|
+
.expect(toInclude(join("subfolder", "fixed_page2.html")));
|
|
83
|
+
});
|
|
42
84
|
it("fills basic details", async () => {
|
|
43
85
|
const app = await getApp({ disableCsrf: true });
|
|
44
86
|
const loginCookie = await getAdminLoginCookie();
|
|
@@ -48,6 +90,19 @@ describe("page create", () => {
|
|
|
48
90
|
.set("Cookie", loginCookie)
|
|
49
91
|
.expect(toRedirect("/pageedit/edit/whales"));
|
|
50
92
|
});
|
|
93
|
+
it("fills details with html-file", async () => {
|
|
94
|
+
const app = await getApp({ disableCsrf: true });
|
|
95
|
+
const loginCookie = await getAdminLoginCookie();
|
|
96
|
+
await request(app)
|
|
97
|
+
.post("/pageedit/edit-properties")
|
|
98
|
+
.send(
|
|
99
|
+
`name=new_page_with_html_file&title=foo&description=bar&min_role=100&html_file=${encodeURIComponent(
|
|
100
|
+
htmlFile.location
|
|
101
|
+
)}`
|
|
102
|
+
)
|
|
103
|
+
.set("Cookie", loginCookie)
|
|
104
|
+
.expect(toRedirect("/pageedit/"));
|
|
105
|
+
});
|
|
51
106
|
it("fills layout", async () => {
|
|
52
107
|
const app = await getApp({ disableCsrf: true });
|
|
53
108
|
const loginCookie = await getAdminLoginCookie();
|
|
@@ -68,6 +123,44 @@ describe("page create", () => {
|
|
|
68
123
|
.set("Cookie", loginCookie)
|
|
69
124
|
.expect(toInclude("Herman"));
|
|
70
125
|
});
|
|
126
|
+
|
|
127
|
+
it("shows page with html file", async () => {
|
|
128
|
+
const app = await getApp({ disableCsrf: true });
|
|
129
|
+
const loginCookie = await getAdminLoginCookie();
|
|
130
|
+
await request(app)
|
|
131
|
+
.get("/page/new_page_with_html_file")
|
|
132
|
+
.set("Cookie", loginCookie)
|
|
133
|
+
.expect(toInclude("Land here"));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("does not find the html file for staff or public", async () => {
|
|
137
|
+
const app = await getApp({ disableCsrf: true });
|
|
138
|
+
const loginCookie = await getStaffLoginCookie();
|
|
139
|
+
await request(app)
|
|
140
|
+
.get("/page/new_page_with_html_file")
|
|
141
|
+
.set("Cookie", loginCookie)
|
|
142
|
+
.expect(toInclude("not found", 404));
|
|
143
|
+
await request(app)
|
|
144
|
+
.get("/page/new_page_with_html_file")
|
|
145
|
+
.expect(toInclude("not found", 404));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("finds the html file for staff (after update)", async () => {
|
|
149
|
+
const app = await getApp({ disableCsrf: true });
|
|
150
|
+
await request(app)
|
|
151
|
+
.post("/files/setrole/fixed_page.html")
|
|
152
|
+
.set("Cookie", await getAdminLoginCookie())
|
|
153
|
+
.send("role=40")
|
|
154
|
+
.expect(toRedirect("/files?dir=."));
|
|
155
|
+
const loginCookie = await getStaffLoginCookie();
|
|
156
|
+
await request(app)
|
|
157
|
+
.get("/page/new_page_with_html_file")
|
|
158
|
+
.set("Cookie", loginCookie)
|
|
159
|
+
.expect(toInclude("Land here"));
|
|
160
|
+
await request(app)
|
|
161
|
+
.get("/page/new_page_with_html_file")
|
|
162
|
+
.expect(toInclude("not found", 404));
|
|
163
|
+
});
|
|
71
164
|
});
|
|
72
165
|
|
|
73
166
|
describe("page action", () => {
|