@saltcorn/server 0.7.1-beta.3 → 0.7.2-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/app.js +64 -9
- package/auth/routes.js +37 -13
- package/load_plugins.js +29 -24
- package/locales/da.json +1 -1
- package/locales/de.json +155 -155
- package/locales/en.json +12 -5
- package/locales/it.json +1 -1
- package/locales/ru.json +52 -18
- package/locales/zh.json +3 -3
- package/package.json +14 -9
- package/public/jquery-menu-editor.min.js +5 -0
- package/public/saltcorn-common.js +105 -0
- package/public/saltcorn.css +70 -0
- package/public/saltcorn.js +101 -84
- package/routes/admin.js +1 -1
- package/routes/api.js +36 -1
- package/routes/edit.js +2 -1
- package/routes/fields.js +12 -0
- package/routes/files.js +51 -0
- package/routes/plugins.js +4 -4
- package/routes/viewedit.js +54 -0
- package/tests/admin.test.js +72 -1
- package/tests/clientjs.test.js +1 -0
- package/tests/plugins.test.js +2 -2
- package/tests/view.test.js +1 -1
- package/tests/viewedit.test.js +94 -0
- package/wrapper.js +1 -0
package/public/saltcorn.js
CHANGED
|
@@ -70,10 +70,12 @@ function apply_showif() {
|
|
|
70
70
|
|
|
71
71
|
var options = data[1][val];
|
|
72
72
|
var current = e.attr("data-selected");
|
|
73
|
-
//console.log(val, options, current,data)
|
|
73
|
+
//console.log({ val, options, current, data });
|
|
74
74
|
e.empty();
|
|
75
75
|
(options || []).forEach((o) => {
|
|
76
|
-
if (
|
|
76
|
+
if (
|
|
77
|
+
!(o && typeof o.label !== "undefined" && typeof o.value !== "undefined")
|
|
78
|
+
) {
|
|
77
79
|
if (`${current}` === `${o}`)
|
|
78
80
|
e.append($("<option selected>" + o + "</option>"));
|
|
79
81
|
else e.append($("<option>" + o + "</option>"));
|
|
@@ -219,6 +221,9 @@ if (localStorage.getItem("reload_on_init")) {
|
|
|
219
221
|
location.reload();
|
|
220
222
|
}
|
|
221
223
|
function initialize_page() {
|
|
224
|
+
$(".blur-on-enter-keypress").bind("keyup", function (e) {
|
|
225
|
+
if (e.keyCode === 13) e.target.blur();
|
|
226
|
+
});
|
|
222
227
|
$("form").change(apply_showif);
|
|
223
228
|
apply_showif();
|
|
224
229
|
apply_showif();
|
|
@@ -310,6 +315,34 @@ function initialize_page() {
|
|
|
310
315
|
});
|
|
311
316
|
$('a[data-bs-toggle="tab"].deeplink').historyTabs();
|
|
312
317
|
init_bs5_dropdowns();
|
|
318
|
+
|
|
319
|
+
// Initialize Sliders - https://stackoverflow.com/a/31083391
|
|
320
|
+
var sliderSections = document.getElementsByClassName("range-slider");
|
|
321
|
+
for (var x = 0; x < sliderSections.length; x++) {
|
|
322
|
+
var sliders = sliderSections[x].getElementsByTagName("input");
|
|
323
|
+
for (var y = 0; y < sliders.length; y++) {
|
|
324
|
+
if (sliders[y].type === "range") {
|
|
325
|
+
sliders[y].oninput = function () {
|
|
326
|
+
// Get slider values
|
|
327
|
+
var parent = this.parentNode;
|
|
328
|
+
var slides = parent.getElementsByTagName("input");
|
|
329
|
+
var slide1 = parseFloat(slides[0].value);
|
|
330
|
+
var slide2 = parseFloat(slides[1].value);
|
|
331
|
+
// Neither slider will clip the other, so make sure we determine which is larger
|
|
332
|
+
if (slide1 > slide2) {
|
|
333
|
+
var tmp = slide2;
|
|
334
|
+
slide2 = slide1;
|
|
335
|
+
slide1 = tmp;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var displayElement = parent.getElementsByClassName("rangeValues")[0];
|
|
339
|
+
displayElement.innerHTML = slide1 + " - " + slide2;
|
|
340
|
+
};
|
|
341
|
+
// Manually trigger event first time to display values
|
|
342
|
+
sliders[y].oninput();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
313
346
|
}
|
|
314
347
|
|
|
315
348
|
$(initialize_page);
|
|
@@ -459,57 +492,8 @@ function tristateClick(nm) {
|
|
|
459
492
|
}
|
|
460
493
|
}
|
|
461
494
|
|
|
462
|
-
function notifyAlert(note, spin) {
|
|
463
|
-
if (Array.isArray(note)) {
|
|
464
|
-
note.forEach(notifyAlert);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
var txt, type;
|
|
468
|
-
if (typeof note == "string") {
|
|
469
|
-
txt = note;
|
|
470
|
-
type = "info";
|
|
471
|
-
} else {
|
|
472
|
-
txt = note.text;
|
|
473
|
-
type = note.type;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
$("#alerts-area")
|
|
477
|
-
.append(`<div class="alert alert-${type} alert-dismissible fade show ${
|
|
478
|
-
spin ? "d-flex align-items-center" : ""
|
|
479
|
-
}" role="alert">
|
|
480
|
-
${txt}
|
|
481
|
-
${
|
|
482
|
-
spin
|
|
483
|
-
? `<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>`
|
|
484
|
-
: `<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
|
|
485
|
-
</button>`
|
|
486
|
-
}
|
|
487
|
-
</div>`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
495
|
function ajax_done(res) {
|
|
491
|
-
|
|
492
|
-
if (res.error) notifyAlert({ type: "danger", text: res.error });
|
|
493
|
-
if (res.eval_js) eval(res.eval_js);
|
|
494
|
-
if (res.reload_page) location.reload(); //TODO notify to cookie if reload or goto
|
|
495
|
-
if (res.download) {
|
|
496
|
-
const dataurl = `data:${
|
|
497
|
-
res.download.mimetype || "application/octet-stream"
|
|
498
|
-
};base64,${res.download.blob}`;
|
|
499
|
-
fetch(dataurl)
|
|
500
|
-
.then((res) => res.blob())
|
|
501
|
-
.then((blob) => {
|
|
502
|
-
const link = document.createElement("a");
|
|
503
|
-
link.href = window.URL.createObjectURL(blob);
|
|
504
|
-
if (res.download.filename) link.download = res.download.filename;
|
|
505
|
-
else link.target = "_blank";
|
|
506
|
-
link.click();
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
if (res.goto) {
|
|
510
|
-
if (res.target === "_blank") window.open(res.goto, "_blank").focus();
|
|
511
|
-
else window.location.href = res.goto;
|
|
512
|
-
}
|
|
496
|
+
common_done(res);
|
|
513
497
|
}
|
|
514
498
|
|
|
515
499
|
function view_post(viewname, route, data, onDone) {
|
|
@@ -546,11 +530,6 @@ function globalErrorCatcher(message, source, lineno, colno, error) {
|
|
|
546
530
|
});
|
|
547
531
|
}
|
|
548
532
|
|
|
549
|
-
function press_store_button(clicked) {
|
|
550
|
-
const width = $(clicked).width();
|
|
551
|
-
$(clicked).html('<i class="fas fa-spinner fa-spin"></i>').width(width);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
533
|
function ajax_modal(url, opts = {}) {
|
|
555
534
|
if ($("#scmodal").length === 0) {
|
|
556
535
|
$("body").append(`<div id="scmodal", class="modal">
|
|
@@ -567,6 +546,10 @@ function ajax_modal(url, opts = {}) {
|
|
|
567
546
|
</div>
|
|
568
547
|
</div>
|
|
569
548
|
</div>`);
|
|
549
|
+
} else if ($("#scmodal").hasClass("show")) {
|
|
550
|
+
var myModalEl = document.getElementById("scmodal");
|
|
551
|
+
var modal = bootstrap.Modal.getInstance(myModalEl);
|
|
552
|
+
modal.dispose();
|
|
570
553
|
}
|
|
571
554
|
if (opts.submitReload === false) $("#scmodal").addClass("no-submit-reload");
|
|
572
555
|
else $("#scmodal").removeClass("no-submit-reload");
|
|
@@ -587,6 +570,7 @@ function ajax_modal(url, opts = {}) {
|
|
|
587
570
|
|
|
588
571
|
function saveAndContinue(e, k) {
|
|
589
572
|
var form = $(e).closest("form");
|
|
573
|
+
submitWithEmptyAction(form[0]);
|
|
590
574
|
var url = form.attr("action");
|
|
591
575
|
var form_data = form.serialize();
|
|
592
576
|
$.ajax(url, {
|
|
@@ -614,6 +598,54 @@ function saveAndContinue(e, k) {
|
|
|
614
598
|
return false;
|
|
615
599
|
}
|
|
616
600
|
|
|
601
|
+
function applyViewConfig(e, url) {
|
|
602
|
+
var form = $(e).closest("form");
|
|
603
|
+
var form_data = form.serializeArray();
|
|
604
|
+
const cfg = {};
|
|
605
|
+
form_data.forEach((item) => {
|
|
606
|
+
cfg[item.name] = item.value;
|
|
607
|
+
});
|
|
608
|
+
$.ajax(url, {
|
|
609
|
+
type: "POST",
|
|
610
|
+
dataType: "json",
|
|
611
|
+
contentType: "application/json",
|
|
612
|
+
headers: {
|
|
613
|
+
"CSRF-Token": _sc_globalCsrf,
|
|
614
|
+
},
|
|
615
|
+
data: JSON.stringify(cfg),
|
|
616
|
+
error: function (request) {},
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const repeaterCopyValuesToForm = (form, editor) => {
|
|
623
|
+
const vs = JSON.parse(editor.getString());
|
|
624
|
+
const allNames = new Set([]);
|
|
625
|
+
const setVal = (k, ix, v) => {
|
|
626
|
+
const $e = form.find(`input[name="${k}_${ix}"]`);
|
|
627
|
+
if ($e.length) $e.val(v);
|
|
628
|
+
else
|
|
629
|
+
form.append(
|
|
630
|
+
`<input type="hidden" name="${k}_${ix}" value="${v}"></input>`
|
|
631
|
+
);
|
|
632
|
+
};
|
|
633
|
+
vs.forEach((v, ix) => {
|
|
634
|
+
Object.entries(v).forEach(([k, v]) => {
|
|
635
|
+
//console.log(ix, k, typeof v, v)
|
|
636
|
+
allNames.add(k);
|
|
637
|
+
if (typeof v === "boolean") setVal(k, ix, v ? "on" : "");
|
|
638
|
+
else setVal(k, ix, v);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
//delete
|
|
642
|
+
[...allNames].forEach((k) => {
|
|
643
|
+
for (let ix = vs.length; ix < vs.length + 20; ix++) {
|
|
644
|
+
$(`input[name=${k}_${ix}]`).remove();
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
};
|
|
648
|
+
|
|
617
649
|
function ajaxSubmitForm(e) {
|
|
618
650
|
var form = $(e).closest("form");
|
|
619
651
|
var url = form.attr("action");
|
|
@@ -698,31 +730,16 @@ function make_unique_field(
|
|
|
698
730
|
type: "GET",
|
|
699
731
|
success: function (res) {
|
|
700
732
|
if (res.success) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
break;
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
const vals = res.success
|
|
715
|
-
.map((o) => o[field_name])
|
|
716
|
-
.filter((s) => s.startsWith(value));
|
|
717
|
-
if (vals.includes(value) || always_append) {
|
|
718
|
-
for (let i = start || 0; i < vals.length + (start || 0) + 2; i++) {
|
|
719
|
-
const newname = `${value}${space ? " " : ""}${gen_char(i)}`;
|
|
720
|
-
if (!vals.includes(newname)) {
|
|
721
|
-
$("#" + id).val(newname);
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
733
|
+
unique_field_from_rows(
|
|
734
|
+
res.success,
|
|
735
|
+
id,
|
|
736
|
+
field_name,
|
|
737
|
+
space,
|
|
738
|
+
start,
|
|
739
|
+
always_append,
|
|
740
|
+
char_type,
|
|
741
|
+
value
|
|
742
|
+
);
|
|
726
743
|
}
|
|
727
744
|
},
|
|
728
745
|
}
|
|
@@ -830,13 +847,13 @@ const columnSummary = (col) => {
|
|
|
830
847
|
if (!col) return "Unknown";
|
|
831
848
|
switch (col.type) {
|
|
832
849
|
case "Field":
|
|
833
|
-
return `Field ${col.field_name} ${col.fieldview}`;
|
|
850
|
+
return `Field ${col.field_name} ${col.fieldview || ""}`;
|
|
834
851
|
case "Link":
|
|
835
852
|
return `Link ${col.link_text}`;
|
|
836
853
|
case "JoinField":
|
|
837
854
|
return `Join ${col.join_field}`;
|
|
838
855
|
case "ViewLink":
|
|
839
|
-
return `View ${col.view_label || col.view.split(":")[1] || ""}`;
|
|
856
|
+
return `View link ${col.view_label || col.view.split(":")[1] || ""}`;
|
|
840
857
|
case "Action":
|
|
841
858
|
return `Action ${col.action_label || col.action_name}`;
|
|
842
859
|
case "Aggregation":
|
package/routes/admin.js
CHANGED
|
@@ -787,7 +787,7 @@ router.get(
|
|
|
787
787
|
? div(
|
|
788
788
|
{ class: "alert alert-success", role: "alert" },
|
|
789
789
|
i({ class: "fas fa-check-circle fa-lg me-2" }),
|
|
790
|
-
h5({ class: "d-inline" }, "No errors detected")
|
|
790
|
+
h5({ class: "d-inline" }, req.__("No errors detected during configuration check"))
|
|
791
791
|
)
|
|
792
792
|
: errors.map(mkError)
|
|
793
793
|
),
|
package/routes/api.js
CHANGED
|
@@ -20,6 +20,7 @@ const { error_catcher } = require("./utils.js");
|
|
|
20
20
|
//const { mkTable, renderForm, link, post_btn } = require("@saltcorn/markup");
|
|
21
21
|
const { getState } = require("@saltcorn/data/db/state");
|
|
22
22
|
const Table = require("@saltcorn/data/models/table");
|
|
23
|
+
const View = require("@saltcorn/data/models/view");
|
|
23
24
|
//const Field = require("@saltcorn/data/models/field");
|
|
24
25
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
25
26
|
//const load_plugins = require("../load_plugins");
|
|
@@ -111,6 +112,40 @@ function accessAllowed(req, user, trigger) {
|
|
|
111
112
|
return role <= trigger.min_role;
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
router.post(
|
|
116
|
+
"/viewQuery/:viewName/:queryName",
|
|
117
|
+
error_catcher(async (req, res, next) => {
|
|
118
|
+
let { viewName, queryName } = req.params;
|
|
119
|
+
const view = await View.findOne({ name: viewName });
|
|
120
|
+
if (!view) {
|
|
121
|
+
res.status(404).json({ error: req.__("Not found") });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await passport.authenticate(
|
|
125
|
+
"jwt",
|
|
126
|
+
{ session: false },
|
|
127
|
+
async function (err, user, info) {
|
|
128
|
+
const role = user && user.id ? user.role_id : 10;
|
|
129
|
+
if (
|
|
130
|
+
role <= view.min_role ||
|
|
131
|
+
(await view.authorise_get({ req, ...view })) // TODO set query to state
|
|
132
|
+
) {
|
|
133
|
+
const queries = view.queries(false, req);
|
|
134
|
+
if (queries[queryName]) {
|
|
135
|
+
const { args } = req.body;
|
|
136
|
+
const resp = await queries[queryName](...args, true);
|
|
137
|
+
res.json({ success: resp });
|
|
138
|
+
} else {
|
|
139
|
+
res.status(404).json({ error: req.__("Not found") });
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
res.status(401).json({ error: req.__("Not authorized") });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
)(req, res, next);
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
|
|
114
149
|
router.get(
|
|
115
150
|
"/:tableName/distinct/:fieldName",
|
|
116
151
|
//passport.authenticate("api-bearer", { session: false }),
|
|
@@ -180,7 +215,7 @@ router.get(
|
|
|
180
215
|
}
|
|
181
216
|
|
|
182
217
|
await passport.authenticate(
|
|
183
|
-
"api-bearer",
|
|
218
|
+
["api-bearer", "jwt"],
|
|
184
219
|
{ session: false },
|
|
185
220
|
async function (err, user, info) {
|
|
186
221
|
if (accessAllowedRead(req, user, table)) {
|
package/routes/edit.js
CHANGED
|
@@ -44,7 +44,8 @@ router.post(
|
|
|
44
44
|
"error",
|
|
45
45
|
req.__("Not allowed to write to table %s", table.name)
|
|
46
46
|
);
|
|
47
|
-
if (req.
|
|
47
|
+
if (req.xhr) res.send("OK");
|
|
48
|
+
else if (req.get("referer")) res.redirect(req.get("referer"));
|
|
48
49
|
else res.redirect(redirect || `/list/${table.name}`);
|
|
49
50
|
})
|
|
50
51
|
);
|
package/routes/fields.js
CHANGED
|
@@ -181,6 +181,7 @@ const fieldFlow = (req) =>
|
|
|
181
181
|
var attributes = context.attributes || {};
|
|
182
182
|
attributes.default = context.default;
|
|
183
183
|
attributes.summary_field = context.summary_field;
|
|
184
|
+
attributes.include_fts = context.include_fts;
|
|
184
185
|
attributes.on_delete_cascade = context.on_delete_cascade;
|
|
185
186
|
const {
|
|
186
187
|
table_id,
|
|
@@ -370,6 +371,11 @@ const fieldFlow = (req) =>
|
|
|
370
371
|
value: f.name,
|
|
371
372
|
label: f.label,
|
|
372
373
|
}));
|
|
374
|
+
const textfields = orderedFields
|
|
375
|
+
.filter(
|
|
376
|
+
(f) => (!f.calculated || f.stored) && f.type?.sql_name === "text"
|
|
377
|
+
)
|
|
378
|
+
.map((f) => f.name);
|
|
373
379
|
return new Form({
|
|
374
380
|
fields: [
|
|
375
381
|
new Field({
|
|
@@ -378,6 +384,12 @@ const fieldFlow = (req) =>
|
|
|
378
384
|
input_type: "select",
|
|
379
385
|
options: keyfields,
|
|
380
386
|
}),
|
|
387
|
+
new Field({
|
|
388
|
+
name: "include_fts",
|
|
389
|
+
label: req.__("Include in full-text search"),
|
|
390
|
+
type: "Bool",
|
|
391
|
+
showIf: { summary_field: textfields },
|
|
392
|
+
}),
|
|
381
393
|
new Field({
|
|
382
394
|
name: "on_delete_cascade",
|
|
383
395
|
label: req.__("On delete cascade"),
|
package/routes/files.js
CHANGED
|
@@ -9,6 +9,7 @@ const File = require("@saltcorn/data/models/file");
|
|
|
9
9
|
const User = require("@saltcorn/data/models/user");
|
|
10
10
|
const { getState } = require("@saltcorn/data/db/state");
|
|
11
11
|
const s3storage = require("../s3storage");
|
|
12
|
+
const sharp = require("sharp");
|
|
12
13
|
|
|
13
14
|
const {
|
|
14
15
|
mkTable,
|
|
@@ -43,6 +44,8 @@ const {
|
|
|
43
44
|
config_fields_form,
|
|
44
45
|
save_config_from_form,
|
|
45
46
|
} = require("../markup/admin");
|
|
47
|
+
const fsp = require("fs").promises;
|
|
48
|
+
const fs = require("fs");
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
51
|
* @type {object}
|
|
@@ -187,6 +190,54 @@ router.get(
|
|
|
187
190
|
})
|
|
188
191
|
);
|
|
189
192
|
|
|
193
|
+
/**
|
|
194
|
+
* @name get/resize/:id
|
|
195
|
+
* @function
|
|
196
|
+
* @memberof module:routes/files~filesRouter
|
|
197
|
+
* @function
|
|
198
|
+
*/
|
|
199
|
+
router.get(
|
|
200
|
+
"/resize/:id/:width_str",
|
|
201
|
+
error_catcher(async (req, res) => {
|
|
202
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
203
|
+
const user_id = req.user && req.user.id;
|
|
204
|
+
const { id, width_str } = req.params;
|
|
205
|
+
let file;
|
|
206
|
+
if (typeof strictParseInt(id) !== "undefined")
|
|
207
|
+
file = await File.findOne({ id });
|
|
208
|
+
else file = await File.findOne({ filename: id });
|
|
209
|
+
|
|
210
|
+
if (!file) {
|
|
211
|
+
res
|
|
212
|
+
.status(404)
|
|
213
|
+
.sendWrap(req.__("Not found"), h1(req.__("File not found")));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (role <= file.min_role_read || (user_id && user_id === file.user_id)) {
|
|
217
|
+
res.type(file.mimetype);
|
|
218
|
+
const cacheability = file.min_role_read === 10 ? "public" : "private";
|
|
219
|
+
res.set("Cache-Control", `${cacheability}, max-age=86400`);
|
|
220
|
+
//TODO s3
|
|
221
|
+
if (file.s3_store) s3storage.serveObject(file, res, false);
|
|
222
|
+
else {
|
|
223
|
+
const width = strictParseInt(width_str);
|
|
224
|
+
if (!width) {
|
|
225
|
+
res.sendFile(file.location);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const fnm = `${file.location}_w${width}`;
|
|
229
|
+
if (!fs.existsSync(fnm)) {
|
|
230
|
+
await sharp(file.location).resize({ width }).toFile(fnm);
|
|
231
|
+
}
|
|
232
|
+
res.sendFile(fnm);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
req.flash("warning", req.__("Not authorized"));
|
|
236
|
+
res.redirect("/");
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
190
241
|
/**
|
|
191
242
|
* @name post/setrole/:id
|
|
192
243
|
* @function
|
package/routes/plugins.js
CHANGED
|
@@ -652,16 +652,16 @@ router.get(
|
|
|
652
652
|
error_catcher(async (req, res) => {
|
|
653
653
|
const { plugin } = req.params;
|
|
654
654
|
const filepath = req.params[0];
|
|
655
|
+
const hasVersion = plugin.includes("@");
|
|
655
656
|
const location =
|
|
656
|
-
getState().plugin_locations[
|
|
657
|
-
plugin.includes("@") ? plugin.split("@")[0] : plugin
|
|
658
|
-
];
|
|
657
|
+
getState().plugin_locations[hasVersion ? plugin.split("@")[0] : plugin];
|
|
659
658
|
if (location) {
|
|
660
659
|
const safeFile = path
|
|
661
660
|
.normalize(filepath)
|
|
662
661
|
.replace(/^(\.\.(\/|\\|$))+/, "");
|
|
663
662
|
const fullpath = path.join(location, "public", safeFile);
|
|
664
|
-
if (fs.existsSync(fullpath))
|
|
663
|
+
if (fs.existsSync(fullpath))
|
|
664
|
+
res.sendFile(fullpath, { maxAge: hasVersion ? "100d" : "1d" });
|
|
665
665
|
else res.status(404).send(req.__("Not found"));
|
|
666
666
|
} else {
|
|
667
667
|
res.status(404).send(req.__("Not found"));
|
package/routes/viewedit.js
CHANGED
|
@@ -582,8 +582,11 @@ router.get(
|
|
|
582
582
|
return;
|
|
583
583
|
}
|
|
584
584
|
const configFlow = await view.get_config_flow(req);
|
|
585
|
+
const hasConfig =
|
|
586
|
+
view.configuration && Object.keys(view.configuration).length > 0;
|
|
585
587
|
const wfres = await configFlow.run(
|
|
586
588
|
{
|
|
589
|
+
id: hasConfig ? view.id : undefined,
|
|
587
590
|
table_id: view.table_id,
|
|
588
591
|
exttable_name: view.exttable_name,
|
|
589
592
|
viewname: name,
|
|
@@ -704,6 +707,48 @@ router.post(
|
|
|
704
707
|
})
|
|
705
708
|
);
|
|
706
709
|
|
|
710
|
+
/**
|
|
711
|
+
* @name post/saveconfig/:id
|
|
712
|
+
* @function
|
|
713
|
+
* @memberof module:routes/viewedit~vieweditRouter
|
|
714
|
+
* @function
|
|
715
|
+
*/
|
|
716
|
+
router.post(
|
|
717
|
+
"/saveconfig/:viewname",
|
|
718
|
+
isAdmin,
|
|
719
|
+
error_catcher(async (req, res) => {
|
|
720
|
+
const { viewname } = req.params;
|
|
721
|
+
|
|
722
|
+
if (viewname && req.body) {
|
|
723
|
+
const view = await View.findOne({ name: viewname });
|
|
724
|
+
const configFlow = await view.get_config_flow(req);
|
|
725
|
+
const step = await configFlow.singleStepForm(req.body, req);
|
|
726
|
+
if (step?.renderForm) {
|
|
727
|
+
if (!step.renderForm.hasErrors) {
|
|
728
|
+
let newcfg;
|
|
729
|
+
if (step.contextField)
|
|
730
|
+
newcfg = {
|
|
731
|
+
...view.configuration,
|
|
732
|
+
[step.contextField]: {
|
|
733
|
+
...view.configuration?.[step.contextField],
|
|
734
|
+
...step.renderForm.values,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
else newcfg = { ...view.configuration, ...step.renderForm.values };
|
|
738
|
+
await View.update({ configuration: newcfg }, view.id);
|
|
739
|
+
res.json({ success: "ok" });
|
|
740
|
+
} else {
|
|
741
|
+
res.json({ error: step.renderForm.errorSummary });
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
res.json({ error: "no form" });
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
res.json({ error: "no view" });
|
|
748
|
+
}
|
|
749
|
+
})
|
|
750
|
+
);
|
|
751
|
+
|
|
707
752
|
/**
|
|
708
753
|
* @name post/setrole/:id
|
|
709
754
|
* @function
|
|
@@ -730,3 +775,12 @@ router.post(
|
|
|
730
775
|
res.redirect("/viewedit");
|
|
731
776
|
})
|
|
732
777
|
);
|
|
778
|
+
|
|
779
|
+
router.post(
|
|
780
|
+
"/test/inserter",
|
|
781
|
+
isAdmin,
|
|
782
|
+
error_catcher(async (req, res) => {
|
|
783
|
+
const view = await View.create(req.body);
|
|
784
|
+
res.json({ view });
|
|
785
|
+
})
|
|
786
|
+
);
|
package/tests/admin.test.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
respondJsonWith,
|
|
13
13
|
} = require("../auth/testhelp");
|
|
14
14
|
const db = require("@saltcorn/data/db");
|
|
15
|
+
const { sleep } = require("@saltcorn/data/tests/mocks");
|
|
15
16
|
const fs = require("fs").promises;
|
|
16
17
|
const File = require("@saltcorn/data/models/file");
|
|
17
18
|
const User = require("@saltcorn/data/models/user");
|
|
@@ -30,7 +31,12 @@ beforeAll(async () => {
|
|
|
30
31
|
4
|
|
31
32
|
);
|
|
32
33
|
});
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
await sleep(200);
|
|
37
|
+
db.close();
|
|
38
|
+
});
|
|
39
|
+
|
|
34
40
|
const adminPageContains = (specs) =>
|
|
35
41
|
it("adminPageContains " + specs.map((s) => s[1]).join(","), async () => {
|
|
36
42
|
const app = await getApp({ disableCsrf: true });
|
|
@@ -456,6 +462,71 @@ describe("actions", () => {
|
|
|
456
462
|
.expect(toRedirect("/actions/"));
|
|
457
463
|
});
|
|
458
464
|
});
|
|
465
|
+
describe("localizer", () => {
|
|
466
|
+
itShouldRedirectUnauthToLogin("/site-structure/localizer");
|
|
467
|
+
itShouldRedirectUnauthToLogin("/site-structure/localizer/add-lang");
|
|
468
|
+
it("redirects site struct to menu", async () => {
|
|
469
|
+
const app = await getApp({ disableCsrf: true });
|
|
470
|
+
const loginCookie = await getAdminLoginCookie();
|
|
471
|
+
await request(app)
|
|
472
|
+
.get("/site-structure")
|
|
473
|
+
.set("Cookie", loginCookie)
|
|
474
|
+
.expect(toRedirect("/menu"));
|
|
475
|
+
});
|
|
476
|
+
it("shows languages", async () => {
|
|
477
|
+
const app = await getApp({ disableCsrf: true });
|
|
478
|
+
const loginCookie = await getAdminLoginCookie();
|
|
479
|
+
await request(app)
|
|
480
|
+
.get("/site-structure/localizer")
|
|
481
|
+
.set("Cookie", loginCookie)
|
|
482
|
+
.expect(toInclude("Languages"));
|
|
483
|
+
});
|
|
484
|
+
it("shows add language form", async () => {
|
|
485
|
+
const app = await getApp({ disableCsrf: true });
|
|
486
|
+
const loginCookie = await getAdminLoginCookie();
|
|
487
|
+
await request(app)
|
|
488
|
+
.get("/site-structure/localizer/add-lang")
|
|
489
|
+
.set("Cookie", loginCookie)
|
|
490
|
+
.expect(toInclude("Locale identifier short code"));
|
|
491
|
+
});
|
|
492
|
+
it("add language", async () => {
|
|
493
|
+
const app = await getApp({ disableCsrf: true });
|
|
494
|
+
const loginCookie = await getAdminLoginCookie();
|
|
495
|
+
await request(app)
|
|
496
|
+
.post("/site-structure/localizer/save-lang")
|
|
497
|
+
.set("Cookie", loginCookie)
|
|
498
|
+
.send("name=dansk")
|
|
499
|
+
.send("locale=da")
|
|
500
|
+
.expect(toRedirect("/site-structure/localizer/edit/da"));
|
|
501
|
+
});
|
|
502
|
+
it("shows new in languages", async () => {
|
|
503
|
+
const app = await getApp({ disableCsrf: true });
|
|
504
|
+
const loginCookie = await getAdminLoginCookie();
|
|
505
|
+
await request(app)
|
|
506
|
+
.get("/site-structure/localizer")
|
|
507
|
+
.set("Cookie", loginCookie)
|
|
508
|
+
.expect(toInclude("dansk"));
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("shows edit language form", async () => {
|
|
512
|
+
const app = await getApp({ disableCsrf: true });
|
|
513
|
+
const loginCookie = await getAdminLoginCookie();
|
|
514
|
+
await request(app)
|
|
515
|
+
.get("/site-structure/localizer/edit/da")
|
|
516
|
+
.set("Cookie", loginCookie)
|
|
517
|
+
.expect(toInclude("Hello world"));
|
|
518
|
+
});
|
|
519
|
+
it("set string language", async () => {
|
|
520
|
+
const app = await getApp({ disableCsrf: true });
|
|
521
|
+
const loginCookie = await getAdminLoginCookie();
|
|
522
|
+
await request(app)
|
|
523
|
+
.post("/site-structure/localizer/save-string/da/Hello%20world")
|
|
524
|
+
.set("Cookie", loginCookie)
|
|
525
|
+
.send("value=Hej+verden")
|
|
526
|
+
.expect(toRedirect("/site-structure/localizer/edit/da"));
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
459
530
|
/**
|
|
460
531
|
* Pages tests
|
|
461
532
|
*/
|
package/tests/clientjs.test.js
CHANGED
package/tests/plugins.test.js
CHANGED
|
@@ -90,7 +90,7 @@ describe("Plugin Endpoints", () => {
|
|
|
90
90
|
.expect(toInclude("testfilecontents"));
|
|
91
91
|
await request(app)
|
|
92
92
|
.get(
|
|
93
|
-
"/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.
|
|
93
|
+
"/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.4/css/sb-admin-2.min.css"
|
|
94
94
|
)
|
|
95
95
|
.expect(toInclude("Start Bootstrap"));
|
|
96
96
|
|
|
@@ -100,7 +100,7 @@ describe("Plugin Endpoints", () => {
|
|
|
100
100
|
.expect(toRedirect("/plugins"));
|
|
101
101
|
await request(app)
|
|
102
102
|
.get(
|
|
103
|
-
"/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.
|
|
103
|
+
"/plugins/pubdeps/sbadmin2/startbootstrap-sb-admin-2-bs5/4.1.5-beta.4/css/sb-admin-2.min.css"
|
|
104
104
|
)
|
|
105
105
|
.expect(toInclude("Start Bootstrap"));
|
|
106
106
|
});
|