@saltcorn/server 0.8.5-beta.1 → 0.8.5-beta.3
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/locales/en.json +15 -1
- package/locales/pl.json +1110 -0
- package/package.json +9 -8
- package/public/saltcorn-common.js +44 -8
- package/public/saltcorn.css +2 -3
- package/public/saltcorn.js +17 -2
- package/routes/actions.js +25 -12
- package/routes/admin.js +70 -30
- package/routes/api.js +8 -8
- package/routes/tables.js +103 -33
- package/routes/utils.js +19 -0
- package/tests/table.test.js +2 -2
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.8.5-beta.
|
|
3
|
+
"version": "0.8.5-beta.3",
|
|
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.8.5-beta.
|
|
10
|
-
"@saltcorn/builder": "0.8.5-beta.
|
|
11
|
-
"@saltcorn/data": "0.8.5-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.8.5-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.8.5-beta.
|
|
14
|
-
"@saltcorn/markup": "0.8.5-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.8.5-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.8.5-beta.3",
|
|
10
|
+
"@saltcorn/builder": "0.8.5-beta.3",
|
|
11
|
+
"@saltcorn/data": "0.8.5-beta.3",
|
|
12
|
+
"@saltcorn/admin-models": "0.8.5-beta.3",
|
|
13
|
+
"@saltcorn/filemanager": "0.8.5-beta.3",
|
|
14
|
+
"@saltcorn/markup": "0.8.5-beta.3",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.8.5-beta.3",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"qrcode": "1.5.1",
|
|
53
53
|
"resize-with-sharp-or-jimp": "0.1.6",
|
|
54
54
|
"socket.io": "4.6.0",
|
|
55
|
+
"systeminformation": "^5.11.12",
|
|
55
56
|
"thirty-two": "1.0.2",
|
|
56
57
|
"tmp-promise": "^3.0.2",
|
|
57
58
|
"uuid": "^8.2.0",
|
|
@@ -145,7 +145,7 @@ function apply_showif() {
|
|
|
145
145
|
});
|
|
146
146
|
element.dispatchEvent(new Event("RefreshSelectOptions"));
|
|
147
147
|
if (e.hasClass("selectized") && $().selectize) {
|
|
148
|
-
e.selectize()[0].selectize.clearOptions();
|
|
148
|
+
e.selectize()[0].selectize.clearOptions(true);
|
|
149
149
|
e.selectize()[0].selectize.addOption(dataOptions);
|
|
150
150
|
if (typeof currentDataOption !== "undefined")
|
|
151
151
|
e.selectize()[0].selectize.setValue(currentDataOption);
|
|
@@ -428,6 +428,10 @@ function initialize_page() {
|
|
|
428
428
|
var key = $(this).attr("data-inline-edit-field") || "value";
|
|
429
429
|
var ajax = !!$(this).attr("data-inline-edit-ajax");
|
|
430
430
|
var type = $(this).attr("data-inline-edit-type");
|
|
431
|
+
var schema = $(this).attr("data-inline-edit-schema");
|
|
432
|
+
if (schema) {
|
|
433
|
+
schema = JSON.parse(decodeURIComponent(schema));
|
|
434
|
+
}
|
|
431
435
|
var is_key = type?.startsWith("Key:");
|
|
432
436
|
const opts = encodeURIComponent(
|
|
433
437
|
JSON.stringify({
|
|
@@ -438,12 +442,16 @@ function initialize_page() {
|
|
|
438
442
|
current_label: $(this).attr("data-inline-edit-current-label"),
|
|
439
443
|
type,
|
|
440
444
|
is_key,
|
|
445
|
+
schema,
|
|
441
446
|
})
|
|
442
447
|
);
|
|
443
|
-
|
|
444
|
-
const [tblName, target] = type.replace("Key:", "").split(".");
|
|
448
|
+
const doAjaxOptionsFetch = (tblName, target) => {
|
|
445
449
|
$.ajax(`/api/${tblName}`).then((resp) => {
|
|
446
450
|
if (resp.success) {
|
|
451
|
+
resp.success.sort((a, b) =>
|
|
452
|
+
a[target]?.toLowerCase?.() > b[target]?.toLowerCase?.() ? 1 : -1
|
|
453
|
+
);
|
|
454
|
+
|
|
447
455
|
const selopts = resp.success.map(
|
|
448
456
|
(r) =>
|
|
449
457
|
`<option ${current == r.id ? `selected ` : ``}value="${
|
|
@@ -463,6 +471,14 @@ function initialize_page() {
|
|
|
463
471
|
);
|
|
464
472
|
}
|
|
465
473
|
});
|
|
474
|
+
};
|
|
475
|
+
if (type === "JSON" && schema && schema.type.startsWith("Key to ")) {
|
|
476
|
+
const tblName = schema.type.replace("Key to ", "");
|
|
477
|
+
const target = schema.summary_field || "id";
|
|
478
|
+
doAjaxOptionsFetch(tblName, target);
|
|
479
|
+
} else if (is_key) {
|
|
480
|
+
const [tblName, target] = type.replace("Key:", "").split(".");
|
|
481
|
+
doAjaxOptionsFetch(tblName, target);
|
|
466
482
|
} else
|
|
467
483
|
$(this).replaceWith(
|
|
468
484
|
`<form method="post" action="${url}" ${
|
|
@@ -590,7 +606,10 @@ $(initialize_page);
|
|
|
590
606
|
function cancel_inline_edit(e, opts1) {
|
|
591
607
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
592
608
|
var form = $(e.target).closest("form");
|
|
593
|
-
|
|
609
|
+
var json_fk_opt;
|
|
610
|
+
if (opts.schema) {
|
|
611
|
+
json_fk_opt = form.find(`option[value="${opts.current}"]`).text();
|
|
612
|
+
}
|
|
594
613
|
form.replaceWith(`<div
|
|
595
614
|
data-inline-edit-field="${opts.key}"
|
|
596
615
|
${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
|
|
@@ -601,8 +620,17 @@ function cancel_inline_edit(e, opts1) {
|
|
|
601
620
|
? `data-inline-edit-current-label="${opts.current_label}"`
|
|
602
621
|
: ""
|
|
603
622
|
}
|
|
623
|
+
${
|
|
624
|
+
opts.schema
|
|
625
|
+
? `data-inline-edit-schema="${encodeURIComponent(
|
|
626
|
+
JSON.stringify(opts.schema)
|
|
627
|
+
)}"`
|
|
628
|
+
: ""
|
|
629
|
+
}
|
|
604
630
|
data-inline-edit-dest-url="${opts.url}">
|
|
605
|
-
<span class="current">${
|
|
631
|
+
<span class="current">${
|
|
632
|
+
json_fk_opt || opts.current_label || opts.current
|
|
633
|
+
}</span>
|
|
606
634
|
<i class="editicon fas fa-edit ms-1"></i>
|
|
607
635
|
</div>`);
|
|
608
636
|
initialize_page();
|
|
@@ -624,15 +652,23 @@ function inline_ajax_submit(e, opts1) {
|
|
|
624
652
|
success: function (res) {
|
|
625
653
|
if (opts) {
|
|
626
654
|
let rawVal = formDataArray.find((f) => f.name == opts.key).value;
|
|
627
|
-
let val =
|
|
628
|
-
|
|
629
|
-
|
|
655
|
+
let val =
|
|
656
|
+
opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
|
|
657
|
+
? form.find("select").find("option:selected").text()
|
|
658
|
+
: rawVal;
|
|
630
659
|
|
|
631
660
|
$(e.target).replaceWith(`<div
|
|
632
661
|
data-inline-edit-field="${opts.key}"
|
|
633
662
|
${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
|
|
634
663
|
${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
|
|
635
664
|
${opts.current ? `data-inline-edit-current="${rawVal}"` : ""}
|
|
665
|
+
${
|
|
666
|
+
opts.schema
|
|
667
|
+
? `data-inline-edit-schema="${encodeURIComponent(
|
|
668
|
+
JSON.stringify(opts.schema)
|
|
669
|
+
)}"`
|
|
670
|
+
: ""
|
|
671
|
+
}
|
|
636
672
|
${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
|
|
637
673
|
data-inline-edit-dest-url="${opts.url}">
|
|
638
674
|
<span class="current">${val}</span>
|
package/public/saltcorn.css
CHANGED
|
@@ -227,7 +227,6 @@ footer.bs-mobile-nav-footer {
|
|
|
227
227
|
|
|
228
228
|
.containerbgimage {
|
|
229
229
|
position: absolute;
|
|
230
|
-
top: 0;
|
|
231
230
|
left: 0;
|
|
232
231
|
width: 100%;
|
|
233
232
|
height: 100%;
|
|
@@ -372,7 +371,7 @@ table.table-inner-grid td {
|
|
|
372
371
|
}
|
|
373
372
|
|
|
374
373
|
.join-table-header {
|
|
375
|
-
padding: 0.25rem 1rem;
|
|
374
|
+
padding: 0.25rem 1rem;
|
|
376
375
|
margin-bottom: 0 !important;
|
|
377
376
|
text-decoration: underline;
|
|
378
|
-
}
|
|
377
|
+
}
|
package/public/saltcorn.js
CHANGED
|
@@ -320,6 +320,7 @@ function saveAndContinue(e, k) {
|
|
|
320
320
|
data: form_data,
|
|
321
321
|
success: function (res) {
|
|
322
322
|
ajax_indicator(false);
|
|
323
|
+
form.parent().find(".full-form-error").text("");
|
|
323
324
|
if (res.id && form.find("input[name=id")) {
|
|
324
325
|
form.append(
|
|
325
326
|
`<input type="hidden" class="form-control " name="id" value="${res.id}">`
|
|
@@ -327,9 +328,23 @@ function saveAndContinue(e, k) {
|
|
|
327
328
|
}
|
|
328
329
|
},
|
|
329
330
|
error: function (request) {
|
|
330
|
-
|
|
331
|
+
var ct = request.getResponseHeader("content-type") || "";
|
|
332
|
+
if (ct.startsWith && ct.startsWith("application/json")) {
|
|
333
|
+
var errorArea = form.parent().find(".full-form-error");
|
|
334
|
+
if (errorArea.length) {
|
|
335
|
+
errorArea.text(request.responseJSON.error);
|
|
336
|
+
} else {
|
|
337
|
+
form
|
|
338
|
+
.parent()
|
|
339
|
+
.append(
|
|
340
|
+
`<p class="text-danger full-form-error">${request.responseJSON.error}</p>`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
$("#page-inner-content").html(request.responseText);
|
|
345
|
+
initialize_page();
|
|
346
|
+
}
|
|
331
347
|
ajax_indicate_error(e, request);
|
|
332
|
-
initialize_page();
|
|
333
348
|
},
|
|
334
349
|
complete: function () {
|
|
335
350
|
if (k) k();
|
package/routes/actions.js
CHANGED
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
h6,
|
|
42
42
|
pre,
|
|
43
43
|
text,
|
|
44
|
+
i,
|
|
44
45
|
} = require("@saltcorn/markup/tags");
|
|
45
46
|
const Table = require("@saltcorn/data/models/table");
|
|
46
47
|
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
@@ -510,17 +511,20 @@ router.post(
|
|
|
510
511
|
});
|
|
511
512
|
form.validate(req.body);
|
|
512
513
|
if (form.hasErrors) {
|
|
513
|
-
|
|
514
|
-
res
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
contents:
|
|
522
|
-
|
|
523
|
-
|
|
514
|
+
if (req.xhr) {
|
|
515
|
+
res.status(400).json({ error: form.errorSummary });
|
|
516
|
+
} else
|
|
517
|
+
send_events_page({
|
|
518
|
+
res,
|
|
519
|
+
req,
|
|
520
|
+
active_sub: "Triggers",
|
|
521
|
+
sub2_page: "Configure",
|
|
522
|
+
contents: {
|
|
523
|
+
type: "card",
|
|
524
|
+
title: req.__("Configure trigger"),
|
|
525
|
+
contents: renderForm(form, req.csrfToken()),
|
|
526
|
+
},
|
|
527
|
+
});
|
|
524
528
|
} else {
|
|
525
529
|
await Trigger.update(trigger.id, { configuration: form.values });
|
|
526
530
|
if (req.xhr) {
|
|
@@ -598,6 +602,7 @@ router.get(
|
|
|
598
602
|
user: req.user,
|
|
599
603
|
});
|
|
600
604
|
} catch (e) {
|
|
605
|
+
console.error(e);
|
|
601
606
|
fakeConsole.error(e.message);
|
|
602
607
|
}
|
|
603
608
|
if (output.length === 0) {
|
|
@@ -622,8 +627,16 @@ router.get(
|
|
|
622
627
|
div({ class: "testrunoutput" }, output),
|
|
623
628
|
|
|
624
629
|
a(
|
|
625
|
-
{ href: `/actions`, class: "mt-4 btn btn-primary" },
|
|
630
|
+
{ href: `/actions`, class: "mt-4 btn btn-primary me-1" },
|
|
626
631
|
"« " + req.__("back to actions")
|
|
632
|
+
),
|
|
633
|
+
a(
|
|
634
|
+
{
|
|
635
|
+
href: `/actions/testrun/${id}`,
|
|
636
|
+
class: "ms-1 mt-4 btn btn-primary",
|
|
637
|
+
},
|
|
638
|
+
i({ class: "fas fa-redo me-1" }),
|
|
639
|
+
req.__("Re-run")
|
|
627
640
|
)
|
|
628
641
|
),
|
|
629
642
|
},
|
package/routes/admin.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
error_catcher,
|
|
11
11
|
getGitRevision,
|
|
12
12
|
setTenant,
|
|
13
|
+
get_sys_info,
|
|
13
14
|
} = require("./utils.js");
|
|
14
15
|
const Table = require("@saltcorn/data/models/table");
|
|
15
16
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
@@ -40,6 +41,7 @@ const {
|
|
|
40
41
|
p,
|
|
41
42
|
code,
|
|
42
43
|
h5,
|
|
44
|
+
h3,
|
|
43
45
|
pre,
|
|
44
46
|
button,
|
|
45
47
|
form,
|
|
@@ -100,6 +102,7 @@ const os = require("os");
|
|
|
100
102
|
const Page = require("@saltcorn/data/models/page");
|
|
101
103
|
const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
|
|
102
104
|
const stream = require("stream");
|
|
105
|
+
const Crash = require("@saltcorn/data/models/crash");
|
|
103
106
|
|
|
104
107
|
const router = new Router();
|
|
105
108
|
module.exports = router;
|
|
@@ -862,7 +865,7 @@ router.get(
|
|
|
862
865
|
const can_update =
|
|
863
866
|
!is_latest && !process.env.SALTCORN_DISABLE_UPGRADE && !git_commit;
|
|
864
867
|
const dbversion = await db.getVersion(true);
|
|
865
|
-
|
|
868
|
+
const { memUsage, diskUsage, cpuUsage } = await get_sys_info();
|
|
866
869
|
send_admin_page({
|
|
867
870
|
res,
|
|
868
871
|
req,
|
|
@@ -892,7 +895,6 @@ router.get(
|
|
|
892
895
|
{
|
|
893
896
|
href: "/admin/configuration-check",
|
|
894
897
|
class: "btn btn-info",
|
|
895
|
-
onClick: "press_store_button(this)",
|
|
896
898
|
},
|
|
897
899
|
i({ class: "fas fa-stethoscope" }),
|
|
898
900
|
" ",
|
|
@@ -985,7 +987,20 @@ router.get(
|
|
|
985
987
|
tr(
|
|
986
988
|
th(req.__("Process uptime")),
|
|
987
989
|
td(moment(get_process_init_time()).fromNow(true))
|
|
988
|
-
)
|
|
990
|
+
),
|
|
991
|
+
tr(
|
|
992
|
+
th(req.__("Disk usage")),
|
|
993
|
+
diskUsage > 95
|
|
994
|
+
? td(
|
|
995
|
+
{ class: "text-danger fw-bold" },
|
|
996
|
+
diskUsage,
|
|
997
|
+
"%",
|
|
998
|
+
i({ class: "fas fa-exclamation-triangle ms-1" })
|
|
999
|
+
)
|
|
1000
|
+
: td(diskUsage, "%")
|
|
1001
|
+
),
|
|
1002
|
+
tr(th(req.__("CPU usage")), td(cpuUsage, "%")),
|
|
1003
|
+
tr(th(req.__("Mem usage")), td(memUsage, "%"))
|
|
989
1004
|
)
|
|
990
1005
|
),
|
|
991
1006
|
p(
|
|
@@ -1306,49 +1321,74 @@ router.get(
|
|
|
1306
1321
|
"/configuration-check",
|
|
1307
1322
|
isAdmin,
|
|
1308
1323
|
error_catcher(async (req, res) => {
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1324
|
+
const filename = `${moment().format("YYYYMMDDHHmm")}.html`;
|
|
1325
|
+
await File.new_folder("configuration_checks");
|
|
1326
|
+
const go = async () => {
|
|
1327
|
+
const { passes, errors, pass, warnings } = await runConfigurationCheck(
|
|
1328
|
+
req
|
|
1314
1329
|
);
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1330
|
+
const mkError = (err) =>
|
|
1331
|
+
div(
|
|
1332
|
+
{ class: "alert alert-danger", role: "alert" },
|
|
1333
|
+
pre({ class: "mb-0" }, code(err))
|
|
1334
|
+
);
|
|
1335
|
+
const mkWarning = (err) =>
|
|
1336
|
+
div(
|
|
1337
|
+
{ class: "alert alert-warning", role: "alert" },
|
|
1338
|
+
pre({ class: "mb-0" }, code(err))
|
|
1339
|
+
);
|
|
1340
|
+
|
|
1341
|
+
const report =
|
|
1342
|
+
div(
|
|
1343
|
+
h3("Errors"),
|
|
1344
|
+
pass
|
|
1345
|
+
? div(req.__("No errors detected during configuration check"))
|
|
1346
|
+
: errors.map(mkError)
|
|
1347
|
+
) +
|
|
1348
|
+
div(
|
|
1349
|
+
h3("Warnings"),
|
|
1350
|
+
(warnings || []).length
|
|
1351
|
+
? (warnings || []).map(mkWarning)
|
|
1352
|
+
: "No warnings"
|
|
1353
|
+
) +
|
|
1354
|
+
div(
|
|
1355
|
+
h3("Passes"),
|
|
1356
|
+
|
|
1357
|
+
pre(code(passes.join("\n")))
|
|
1358
|
+
);
|
|
1359
|
+
await File.from_contents(
|
|
1360
|
+
filename,
|
|
1361
|
+
"text/html",
|
|
1362
|
+
report,
|
|
1363
|
+
req.user.id,
|
|
1364
|
+
1,
|
|
1365
|
+
"/configuration_checks"
|
|
1319
1366
|
);
|
|
1367
|
+
};
|
|
1368
|
+
go().catch((err) => Crash.create(err, req));
|
|
1320
1369
|
res.sendWrap(req.__(`Admin`), {
|
|
1321
1370
|
above: [
|
|
1322
1371
|
{
|
|
1323
1372
|
type: "breadcrumbs",
|
|
1324
1373
|
crumbs: [
|
|
1325
1374
|
{ text: req.__("Settings") },
|
|
1326
|
-
{ text: req.__("
|
|
1375
|
+
{ text: req.__("About application"), href: "/admin" },
|
|
1376
|
+
{ text: req.__("System"), href: "/admin/system" },
|
|
1327
1377
|
{ text: req.__("Configuration check") },
|
|
1328
1378
|
],
|
|
1329
1379
|
},
|
|
1380
|
+
|
|
1330
1381
|
{
|
|
1331
1382
|
type: "card",
|
|
1332
|
-
title: req.__("Configuration
|
|
1383
|
+
title: req.__("Configuration check report"),
|
|
1333
1384
|
contents: div(
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
{ class: "d-inline" },
|
|
1340
|
-
req.__("No errors detected during configuration check")
|
|
1341
|
-
)
|
|
1342
|
-
)
|
|
1343
|
-
: errors.map(mkError),
|
|
1344
|
-
(warnings || []).map(mkWarning)
|
|
1385
|
+
"When completed, the report will be ready here: ",
|
|
1386
|
+
a(
|
|
1387
|
+
{ href: `/files/serve/configuration_checks/${filename}` },
|
|
1388
|
+
"/configuration_checks/" + filename
|
|
1389
|
+
)
|
|
1345
1390
|
),
|
|
1346
1391
|
},
|
|
1347
|
-
{
|
|
1348
|
-
type: "card",
|
|
1349
|
-
title: req.__("Configuration checks passed"),
|
|
1350
|
-
contents: div(pre(code(passes.join("\n")))),
|
|
1351
|
-
},
|
|
1352
1392
|
],
|
|
1353
1393
|
});
|
|
1354
1394
|
})
|
package/routes/api.js
CHANGED
|
@@ -421,29 +421,29 @@ router.post(
|
|
|
421
421
|
readState(row, fields);
|
|
422
422
|
let errors = [];
|
|
423
423
|
let hasErrors = false;
|
|
424
|
-
Object.keys(row)
|
|
424
|
+
for (const k of Object.keys(row)) {
|
|
425
425
|
const field = fields.find((f) => f.name === k);
|
|
426
426
|
if (!field && k.includes(".")) {
|
|
427
427
|
const [fnm, jkey] = k.split(".");
|
|
428
428
|
const jfield = fields.find((f) => f.name === fnm);
|
|
429
429
|
if (jfield?.type?.name === "JSON") {
|
|
430
|
-
if (
|
|
431
|
-
|
|
430
|
+
if (typeof row[fnm] === "undefined") {
|
|
431
|
+
const dbrow = await table.getRow({ [table.pk_name]: id });
|
|
432
|
+
row[fnm] = dbrow[fnm] || {};
|
|
433
|
+
}
|
|
434
|
+
row[fnm][jkey] = row[k];
|
|
432
435
|
delete row[k];
|
|
433
436
|
}
|
|
434
437
|
} else if (!field || field.calculated) {
|
|
435
438
|
delete row[k];
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (field?.type && field.type.validate) {
|
|
439
|
+
} else if (field?.type && field.type.validate) {
|
|
440
440
|
const vres = field.type.validate(field.attributes || {})(row[k]);
|
|
441
441
|
if (vres.error) {
|
|
442
442
|
hasErrors = true;
|
|
443
443
|
errors.push(`${k}: ${vres.error}`);
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
-
}
|
|
446
|
+
}
|
|
447
447
|
if (hasErrors) {
|
|
448
448
|
res.status(400).json({ error: errors.join(", ") });
|
|
449
449
|
return;
|
package/routes/tables.js
CHANGED
|
@@ -762,18 +762,22 @@ router.get(
|
|
|
762
762
|
})
|
|
763
763
|
)
|
|
764
764
|
),
|
|
765
|
+
!table.external &&
|
|
766
|
+
div(
|
|
767
|
+
{ class: "mx-auto" },
|
|
768
|
+
a(
|
|
769
|
+
{ href: `/table/constraints/${table.id}` },
|
|
770
|
+
i({ class: "fas fa-2x fa-tasks" }),
|
|
771
|
+
"<br/>",
|
|
772
|
+
req.__("Constraints")
|
|
773
|
+
)
|
|
774
|
+
),
|
|
775
|
+
|
|
765
776
|
// only if table is not external
|
|
766
777
|
!table.external &&
|
|
767
778
|
div(
|
|
768
779
|
{ class: "mx-auto" },
|
|
769
780
|
settingsDropdown(`dataMenuButton`, [
|
|
770
|
-
a(
|
|
771
|
-
{
|
|
772
|
-
class: "dropdown-item",
|
|
773
|
-
href: `/table/constraints/${table.id}`,
|
|
774
|
-
},
|
|
775
|
-
'<i class="fas fa-ban"></i> ' + req.__("Constraints")
|
|
776
|
-
),
|
|
777
781
|
// rename table doesnt supported for sqlite
|
|
778
782
|
!db.isSQLite &&
|
|
779
783
|
table.name !== "users" &&
|
|
@@ -1154,8 +1158,15 @@ router.get(
|
|
|
1154
1158
|
[
|
|
1155
1159
|
{ label: req.__("Type"), key: "type" },
|
|
1156
1160
|
{
|
|
1157
|
-
label: req.__("
|
|
1158
|
-
key: (r) =>
|
|
1161
|
+
label: req.__("What"),
|
|
1162
|
+
key: (r) =>
|
|
1163
|
+
r.type === "Unique"
|
|
1164
|
+
? r.configuration.fields.join(", ")
|
|
1165
|
+
: r.type === "Index"
|
|
1166
|
+
? r.configuration.field
|
|
1167
|
+
: r.type === "Formula"
|
|
1168
|
+
? r.configuration.formula
|
|
1169
|
+
: "",
|
|
1159
1170
|
},
|
|
1160
1171
|
{
|
|
1161
1172
|
label: req.__("Delete"),
|
|
@@ -1166,7 +1177,12 @@ router.get(
|
|
|
1166
1177
|
cons,
|
|
1167
1178
|
{ hover: true }
|
|
1168
1179
|
),
|
|
1169
|
-
|
|
1180
|
+
req.__("Add constraint: "),
|
|
1181
|
+
link(`/table/add-constraint/${id}/Unique`, req.__("Unique")),
|
|
1182
|
+
" | ",
|
|
1183
|
+
link(`/table/add-constraint/${id}/Formula`, req.__("Formula")),
|
|
1184
|
+
" | ",
|
|
1185
|
+
link(`/table/add-constraint/${id}/Index`, req.__("Index")),
|
|
1170
1186
|
],
|
|
1171
1187
|
},
|
|
1172
1188
|
],
|
|
@@ -1181,18 +1197,68 @@ router.get(
|
|
|
1181
1197
|
* @param {object[]} fields
|
|
1182
1198
|
* @returns {Form}
|
|
1183
1199
|
*/
|
|
1184
|
-
const constraintForm = (req, table_id, fields) =>
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1200
|
+
const constraintForm = (req, table_id, fields, type) => {
|
|
1201
|
+
switch (type) {
|
|
1202
|
+
case "Formula":
|
|
1203
|
+
return new Form({
|
|
1204
|
+
action: `/table/add-constraint/${table_id}/${type}`,
|
|
1205
|
+
|
|
1206
|
+
fields: [
|
|
1207
|
+
{
|
|
1208
|
+
name: "formula",
|
|
1209
|
+
label: req.__("Constraint formula"),
|
|
1210
|
+
validator: expressionValidator,
|
|
1211
|
+
type: "String",
|
|
1212
|
+
class: "validate-expression",
|
|
1213
|
+
sublabel:
|
|
1214
|
+
req.__(
|
|
1215
|
+
"Formula must evaluate to true for valid rows. In scope: "
|
|
1216
|
+
) +
|
|
1217
|
+
fields
|
|
1218
|
+
.map((f) => f.name)
|
|
1219
|
+
.map((fn) => code(fn))
|
|
1220
|
+
.join(", "),
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
name: "errormsg",
|
|
1224
|
+
label: "Error message",
|
|
1225
|
+
sublabel: "Shown the user if formula is false",
|
|
1226
|
+
type: "String",
|
|
1227
|
+
},
|
|
1228
|
+
],
|
|
1229
|
+
});
|
|
1230
|
+
case "Unique":
|
|
1231
|
+
return new Form({
|
|
1232
|
+
action: `/table/add-constraint/${table_id}/${type}`,
|
|
1233
|
+
blurb: req.__(
|
|
1234
|
+
"Tick the boxes for the fields that should be jointly unique"
|
|
1235
|
+
),
|
|
1236
|
+
fields: fields.map((f) => ({
|
|
1237
|
+
name: f.name,
|
|
1238
|
+
label: f.label,
|
|
1239
|
+
type: "Bool",
|
|
1240
|
+
})),
|
|
1241
|
+
});
|
|
1242
|
+
case "Index":
|
|
1243
|
+
return new Form({
|
|
1244
|
+
action: `/table/add-constraint/${table_id}/${type}`,
|
|
1245
|
+
blurb: req.__(
|
|
1246
|
+
"Choose the field to be indexed. This make searching the table faster."
|
|
1247
|
+
),
|
|
1248
|
+
fields: [
|
|
1249
|
+
{
|
|
1250
|
+
type: "String",
|
|
1251
|
+
name: "field",
|
|
1252
|
+
label: "Field",
|
|
1253
|
+
required: true,
|
|
1254
|
+
attributes: {
|
|
1255
|
+
options: fields.map((f) => ({ label: f.label, name: f.name })),
|
|
1256
|
+
},
|
|
1257
|
+
},
|
|
1258
|
+
],
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1196
1262
|
|
|
1197
1263
|
/**
|
|
1198
1264
|
* Add constraint GET handler
|
|
@@ -1203,10 +1269,10 @@ const constraintForm = (req, table_id, fields) =>
|
|
|
1203
1269
|
* @function
|
|
1204
1270
|
*/
|
|
1205
1271
|
router.get(
|
|
1206
|
-
"/add-constraint/:id",
|
|
1272
|
+
"/add-constraint/:id/:type",
|
|
1207
1273
|
isAdmin,
|
|
1208
1274
|
error_catcher(async (req, res) => {
|
|
1209
|
-
const { id } = req.params;
|
|
1275
|
+
const { id, type } = req.params;
|
|
1210
1276
|
const table = await Table.findOne({ id });
|
|
1211
1277
|
if (!table) {
|
|
1212
1278
|
req.flash("error", `Table not found`);
|
|
@@ -1214,7 +1280,7 @@ router.get(
|
|
|
1214
1280
|
return;
|
|
1215
1281
|
}
|
|
1216
1282
|
const fields = await table.getFields();
|
|
1217
|
-
const form = constraintForm(req, table.id, fields);
|
|
1283
|
+
const form = constraintForm(req, table.id, fields, type);
|
|
1218
1284
|
res.sendWrap(req.__(`Add constraint to %s`, table.name), {
|
|
1219
1285
|
above: [
|
|
1220
1286
|
{
|
|
@@ -1231,7 +1297,7 @@ router.get(
|
|
|
1231
1297
|
},
|
|
1232
1298
|
{
|
|
1233
1299
|
type: "card",
|
|
1234
|
-
title: req.__(`Add constraint to %s`, table.name),
|
|
1300
|
+
title: req.__(`Add %s constraint to %s`, type, table.name),
|
|
1235
1301
|
contents: renderForm(form, req.csrfToken()),
|
|
1236
1302
|
},
|
|
1237
1303
|
],
|
|
@@ -1247,10 +1313,10 @@ router.get(
|
|
|
1247
1313
|
* @function
|
|
1248
1314
|
*/
|
|
1249
1315
|
router.post(
|
|
1250
|
-
"/add-constraint/:id",
|
|
1316
|
+
"/add-constraint/:id/:type",
|
|
1251
1317
|
isAdmin,
|
|
1252
1318
|
error_catcher(async (req, res) => {
|
|
1253
|
-
const { id } = req.params;
|
|
1319
|
+
const { id, type } = req.params;
|
|
1254
1320
|
const table = await Table.findOne({ id });
|
|
1255
1321
|
if (!table) {
|
|
1256
1322
|
req.flash("error", `Table not found`);
|
|
@@ -1258,16 +1324,20 @@ router.post(
|
|
|
1258
1324
|
return;
|
|
1259
1325
|
}
|
|
1260
1326
|
const fields = await table.getFields();
|
|
1261
|
-
const form = constraintForm(req, table.id, fields);
|
|
1327
|
+
const form = constraintForm(req, table.id, fields, type);
|
|
1262
1328
|
form.validate(req.body);
|
|
1263
1329
|
if (form.hasErrors) req.flash("error", req.__("An error occurred"));
|
|
1264
1330
|
else {
|
|
1331
|
+
let configuration = {};
|
|
1332
|
+
if (type === "Unique")
|
|
1333
|
+
configuration.fields = fields
|
|
1334
|
+
.map((f) => f.name)
|
|
1335
|
+
.filter((f) => form.values[f]);
|
|
1336
|
+
else configuration = form.values;
|
|
1265
1337
|
await TableConstraint.create({
|
|
1266
1338
|
table_id: table.id,
|
|
1267
|
-
type
|
|
1268
|
-
configuration
|
|
1269
|
-
fields: fields.map((f) => f.name).filter((f) => form.values[f]),
|
|
1270
|
-
},
|
|
1339
|
+
type,
|
|
1340
|
+
configuration,
|
|
1271
1341
|
});
|
|
1272
1342
|
}
|
|
1273
1343
|
res.redirect(`/table/constraints/${table.id}`);
|