@saltcorn/server 1.1.2-beta.1 → 1.1.2-beta.11
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/CHANGELOG.md +12 -1
- package/app.js +1 -0
- package/locales/en.json +2 -1
- package/package.json +11 -10
- package/public/saltcorn-common.js +49 -6
- package/public/saltcorn.css +8 -0
- package/public/saltcorn.js +32 -4
- package/routes/actions.js +5 -2
- package/routes/admin.js +10 -5
- package/routes/common_lists.js +4 -4
- package/routes/delete.js +4 -2
- package/routes/fields.js +10 -2
- package/routes/homepage.js +4 -1
- package/routes/list.js +4 -3
- package/routes/pageedit.js +1 -1
- package/routes/plugins.js +1 -0
- package/routes/tables.js +20 -11
- package/routes/tag_entries.js +10 -4
- package/tests/admin.test.js +3 -0
- package/tests/view.test.js +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## 1.1.2 - In beta
|
|
4
4
|
|
|
5
|
-
*
|
|
5
|
+
* Builder:
|
|
6
|
+
- Container background image by file field in Show views
|
|
7
|
+
- Container opacity setting
|
|
8
|
+
- Set custom class on links and actions
|
|
9
|
+
- Fix error toast when saving on Firefox
|
|
10
|
+
- Set action to be submit action - action run on enter keypress.
|
|
11
|
+
|
|
12
|
+
* Restore large backups: stream JSON files to database, use system unzip
|
|
13
|
+
|
|
14
|
+
* Handle multiple fields with same name in Edit.
|
|
15
|
+
|
|
16
|
+
* Upgrade a large number of dependencies (express, typescript, oclif, pg, webpack, typescript, axios, mjml, svelte). Node.js 18+ is require for this release.
|
|
6
17
|
|
|
7
18
|
## 1.1.1 - Released 2 February 2025
|
|
8
19
|
|
package/app.js
CHANGED
package/locales/en.json
CHANGED
|
@@ -1552,5 +1552,6 @@
|
|
|
1552
1552
|
"Show results from each table in this type of element": "Show results from each table in this type of element",
|
|
1553
1553
|
"Search syntax help": "Search syntax help",
|
|
1554
1554
|
"Search syntax": "Search syntax",
|
|
1555
|
-
"Maximum role": "Maximum role"
|
|
1555
|
+
"Maximum role": "Maximum role",
|
|
1556
|
+
"Module dependencies": "Module dependencies"
|
|
1556
1557
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "1.1.2-beta.
|
|
3
|
+
"version": "1.1.2-beta.11",
|
|
4
4
|
"description": "Server app for Saltcorn, open-source no-code platform",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@aws-sdk/client-s3": "^3.735.0",
|
|
10
10
|
"@dr.pogodin/csurf": "^1.14.1",
|
|
11
|
-
"@saltcorn/base-plugin": "1.1.2-beta.
|
|
12
|
-
"@saltcorn/builder": "1.1.2-beta.
|
|
13
|
-
"@saltcorn/data": "1.1.2-beta.
|
|
14
|
-
"@saltcorn/admin-models": "1.1.2-beta.
|
|
15
|
-
"@saltcorn/filemanager": "1.1.2-beta.
|
|
16
|
-
"@saltcorn/markup": "1.1.2-beta.
|
|
17
|
-
"@saltcorn/plugins-loader": "1.1.2-beta.
|
|
18
|
-
"@saltcorn/sbadmin2": "1.1.2-beta.
|
|
11
|
+
"@saltcorn/base-plugin": "1.1.2-beta.11",
|
|
12
|
+
"@saltcorn/builder": "1.1.2-beta.11",
|
|
13
|
+
"@saltcorn/data": "1.1.2-beta.11",
|
|
14
|
+
"@saltcorn/admin-models": "1.1.2-beta.11",
|
|
15
|
+
"@saltcorn/filemanager": "1.1.2-beta.11",
|
|
16
|
+
"@saltcorn/markup": "1.1.2-beta.11",
|
|
17
|
+
"@saltcorn/plugins-loader": "1.1.2-beta.11",
|
|
18
|
+
"@saltcorn/sbadmin2": "1.1.2-beta.11",
|
|
19
19
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
20
20
|
"@socket.io/sticky": "^1.0.1",
|
|
21
21
|
"adm-zip": "0.5.16",
|
|
@@ -77,11 +77,12 @@
|
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"dev": "nodemon index.js",
|
|
80
|
-
"test": "jest --runInBand",
|
|
80
|
+
"test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js ./tests --runInBand",
|
|
81
81
|
"tsc": "echo \"Error: no TypeScript support yet\"",
|
|
82
82
|
"clean": "echo \"Error: no TypeScript support yet\""
|
|
83
83
|
},
|
|
84
84
|
"jest": {
|
|
85
|
+
"transform": {},
|
|
85
86
|
"testEnvironment": "node",
|
|
86
87
|
"testPathIgnorePatterns": [
|
|
87
88
|
"/node_modules/",
|
|
@@ -103,15 +103,26 @@ function rep_del(e) {
|
|
|
103
103
|
var myrep = $(e).closest(".form-repeat");
|
|
104
104
|
var ix = myrep.index();
|
|
105
105
|
var parent = myrep.parent();
|
|
106
|
-
myrep.remove();
|
|
107
106
|
parent.children().each(function (childix, element) {
|
|
108
107
|
if (childix > ix) {
|
|
109
108
|
reindex(element, childix, childix - 1);
|
|
110
109
|
}
|
|
111
110
|
});
|
|
111
|
+
myrep.remove();
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function reindex(element, oldix, newix) {
|
|
115
|
+
$(element)
|
|
116
|
+
.find("input,textarea")
|
|
117
|
+
.each(function () {
|
|
118
|
+
$(this).attr("value", $(this).val());
|
|
119
|
+
});
|
|
120
|
+
$(element)
|
|
121
|
+
.find("select")
|
|
122
|
+
.each(function () {
|
|
123
|
+
$(this).find(":selected").attr("selected", "selected");
|
|
124
|
+
});
|
|
125
|
+
|
|
115
126
|
$(element).html(
|
|
116
127
|
$(element)
|
|
117
128
|
.html()
|
|
@@ -909,6 +920,27 @@ function initialize_page() {
|
|
|
909
920
|
if (e.keyCode === 13) e.target.blur();
|
|
910
921
|
});
|
|
911
922
|
|
|
923
|
+
const validate_identifier_elem = (target) => {
|
|
924
|
+
const next = target.next();
|
|
925
|
+
if (next.hasClass("expr-error")) next.remove();
|
|
926
|
+
const val = target.val();
|
|
927
|
+
if (!val) return;
|
|
928
|
+
try {
|
|
929
|
+
Function(val, "return 1");
|
|
930
|
+
} catch (error) {
|
|
931
|
+
target.after(`<small class="text-danger font-monospace d-block expr-error">
|
|
932
|
+
Invalid identifier
|
|
933
|
+
</small>`);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
$(".validate-identifier").attr("spellcheck", false);
|
|
937
|
+
$(".validate-expression").attr("spellcheck", false);
|
|
938
|
+
|
|
939
|
+
$(".validate-identifier").bind("input", function (e) {
|
|
940
|
+
const target = $(e.target);
|
|
941
|
+
validate_identifier_elem(target);
|
|
942
|
+
});
|
|
943
|
+
|
|
912
944
|
const validate_expression_elem = (target) => {
|
|
913
945
|
const next = target.next();
|
|
914
946
|
if (next.hasClass("expr-error")) next.remove();
|
|
@@ -921,7 +953,10 @@ function initialize_page() {
|
|
|
921
953
|
}
|
|
922
954
|
if (!val) return;
|
|
923
955
|
try {
|
|
924
|
-
|
|
956
|
+
const AsyncFunction = Object.getPrototypeOf(
|
|
957
|
+
async function () {}
|
|
958
|
+
).constructor;
|
|
959
|
+
AsyncFunction("return " + val);
|
|
925
960
|
} catch (error) {
|
|
926
961
|
target.after(`<small class="text-danger font-monospace d-block expr-error">
|
|
927
962
|
${error.message}
|
|
@@ -932,6 +967,7 @@ function initialize_page() {
|
|
|
932
967
|
const target = $(e.target);
|
|
933
968
|
validate_expression_elem(target);
|
|
934
969
|
});
|
|
970
|
+
|
|
935
971
|
$(".validate-expression-conditional").each(function () {
|
|
936
972
|
const theInput = $(this);
|
|
937
973
|
theInput
|
|
@@ -1543,7 +1579,7 @@ function emptyAlerts() {
|
|
|
1543
1579
|
$("#toasts-area").html("");
|
|
1544
1580
|
}
|
|
1545
1581
|
|
|
1546
|
-
function press_store_button(clicked, keepOld) {
|
|
1582
|
+
function press_store_button(clicked, keepOld, disable) {
|
|
1547
1583
|
let btn = clicked;
|
|
1548
1584
|
if ($(clicked).is("form")) btn = $(clicked).find("button[type=submit]");
|
|
1549
1585
|
if (keepOld) {
|
|
@@ -1552,17 +1588,24 @@ function press_store_button(clicked, keepOld) {
|
|
|
1552
1588
|
}
|
|
1553
1589
|
const width = $(btn).width();
|
|
1554
1590
|
$(btn).html('<i class="fas fa-spinner fa-spin"></i>').width(width);
|
|
1591
|
+
setTimeout(() => {
|
|
1592
|
+
$(btn).prop("disabled", true);
|
|
1593
|
+
}, 50);
|
|
1555
1594
|
}
|
|
1556
1595
|
|
|
1557
1596
|
function restore_old_button(btnId) {
|
|
1558
|
-
const btn = $(`#${btnId}`);
|
|
1597
|
+
const btn = btnId instanceof jQuery ? btnId : $(`#${btnId}`);
|
|
1559
1598
|
const oldText = $(btn).data("old-text");
|
|
1560
1599
|
btn.html(oldText);
|
|
1561
|
-
btn.css({ width: "" });
|
|
1600
|
+
btn.css({ width: "" }).prop("disabled", false);
|
|
1562
1601
|
btn.removeData("old-text");
|
|
1563
1602
|
}
|
|
1564
1603
|
|
|
1565
|
-
async function common_done(res,
|
|
1604
|
+
async function common_done(res, viewnameOrElem0, isWeb = true) {
|
|
1605
|
+
const viewnameOrElem =
|
|
1606
|
+
viewnameOrElem0 === "undefined"
|
|
1607
|
+
? last_route_viewname
|
|
1608
|
+
: viewnameOrElem0 || last_route_viewname;
|
|
1566
1609
|
const viewname =
|
|
1567
1610
|
typeof viewnameOrElem === "string"
|
|
1568
1611
|
? viewnameOrElem
|
package/public/saltcorn.css
CHANGED
|
@@ -812,3 +812,11 @@ tr span.add-tag {
|
|
|
812
812
|
tr:hover span.add-tag {
|
|
813
813
|
opacity: 1;
|
|
814
814
|
}
|
|
815
|
+
|
|
816
|
+
#saltcorn-file-manager .filelist tr {
|
|
817
|
+
cursor: pointer;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
#saltcorn-file-manager .filelist tr.selected td {
|
|
821
|
+
background-color: var(--bs-secondary-bg-subtle, var(--tblr-secondary-bg-subtle, gray));;
|
|
822
|
+
}
|
package/public/saltcorn.js
CHANGED
|
@@ -34,10 +34,7 @@ function updateQueryStringParameter(uri1, key, value) {
|
|
|
34
34
|
uri = uris[0];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
var re = new RegExp(
|
|
38
|
-
"([?&])" + escapeRegExp(key) + "=.*?(&|$)",
|
|
39
|
-
"i"
|
|
40
|
-
);
|
|
37
|
+
var re = new RegExp("([?&])" + escapeRegExp(key) + "=.*?(&|$)", "i");
|
|
41
38
|
var separator = uri.indexOf("?") !== -1 ? "&" : "?";
|
|
42
39
|
if (uri.match(re)) {
|
|
43
40
|
if (Array.isArray(value)) {
|
|
@@ -267,6 +264,8 @@ function reset_spinners() {
|
|
|
267
264
|
});
|
|
268
265
|
}
|
|
269
266
|
|
|
267
|
+
let last_route_viewname;
|
|
268
|
+
|
|
270
269
|
function view_post(viewnameOrElem, route, data, onDone, sendState) {
|
|
271
270
|
const viewname =
|
|
272
271
|
typeof viewnameOrElem === "string"
|
|
@@ -274,6 +273,7 @@ function view_post(viewnameOrElem, route, data, onDone, sendState) {
|
|
|
274
273
|
: $(viewnameOrElem)
|
|
275
274
|
.closest("[data-sc-embed-viewname]")
|
|
276
275
|
.attr("data-sc-embed-viewname");
|
|
276
|
+
last_route_viewname = viewname;
|
|
277
277
|
const query = sendState
|
|
278
278
|
? `?${new URL(get_current_state_url()).searchParams.toString()}`
|
|
279
279
|
: "";
|
|
@@ -1322,6 +1322,34 @@ function check_delete_unsaved(tablename, script_tag) {
|
|
|
1322
1322
|
}
|
|
1323
1323
|
}
|
|
1324
1324
|
|
|
1325
|
+
function handle_identical_fields(event) {
|
|
1326
|
+
let form = null;
|
|
1327
|
+
if (event.currentTarget.tagName === "FORM") form = event.currentTarget;
|
|
1328
|
+
else form = $(event.currentTarget).closest("form")[0];
|
|
1329
|
+
if (!form) {
|
|
1330
|
+
console.warn("No form found");
|
|
1331
|
+
} else {
|
|
1332
|
+
const name = event.target.name;
|
|
1333
|
+
const newValue = event.target.value;
|
|
1334
|
+
const tagName = event.target.tagName;
|
|
1335
|
+
const isRadio = event.target.type === "radio";
|
|
1336
|
+
if (tagName === "SELECT" || isRadio) {
|
|
1337
|
+
form.querySelectorAll(`select[name="${name}"]`).forEach((select) => {
|
|
1338
|
+
$(select).val(newValue); //.trigger("change");
|
|
1339
|
+
});
|
|
1340
|
+
form
|
|
1341
|
+
.querySelectorAll(`input[type="radio"][name="${name}"]`)
|
|
1342
|
+
.forEach((input) => {
|
|
1343
|
+
input.checked = input.value === newValue;
|
|
1344
|
+
});
|
|
1345
|
+
} else if (tagName === "INPUT") {
|
|
1346
|
+
form.querySelectorAll(`input[name="${name}"]`).forEach((input) => {
|
|
1347
|
+
input.value = newValue;
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1325
1353
|
(() => {
|
|
1326
1354
|
const e = document.querySelector("[data-sidebar-toggler]");
|
|
1327
1355
|
let closed = localStorage.getItem("sidebarClosed") === "true";
|
package/routes/actions.js
CHANGED
|
@@ -730,6 +730,7 @@ const getWorkflowStepForm = async (
|
|
|
730
730
|
label: req.__("Step name"),
|
|
731
731
|
type: "String",
|
|
732
732
|
required: true,
|
|
733
|
+
class: "validate-identifier",
|
|
733
734
|
sublabel: "An identifier by which this step can be referred to.",
|
|
734
735
|
validator: jsIdentifierValidator,
|
|
735
736
|
},
|
|
@@ -742,6 +743,7 @@ const getWorkflowStepForm = async (
|
|
|
742
743
|
{
|
|
743
744
|
name: "wf_only_if",
|
|
744
745
|
label: req.__("Only if..."),
|
|
746
|
+
class: "validate-expression",
|
|
745
747
|
sublabel:
|
|
746
748
|
"Optional JavaScript expression based on the run context. If given, the chosen action will only be executed if evaluates to true",
|
|
747
749
|
type: "String",
|
|
@@ -1716,6 +1718,7 @@ const getWorkflowStepUserForm = async (run, trigger, step, req) => {
|
|
|
1716
1718
|
const form = new Form({
|
|
1717
1719
|
action: `/actions/fill-workflow-form/${run.id}`,
|
|
1718
1720
|
submitLabel: run.wait_info.output ? req.__("OK") : req.__("Submit"),
|
|
1721
|
+
onSubmit: "press_store_button(this)",
|
|
1719
1722
|
blurb,
|
|
1720
1723
|
formStyle: run.wait_info.output || req.xhr ? "vert" : undefined,
|
|
1721
1724
|
fields: await run.userFormFields(step),
|
|
@@ -1788,8 +1791,8 @@ router.post(
|
|
|
1788
1791
|
if (req.xhr) {
|
|
1789
1792
|
const retDirs = await run.popReturnDirectives();
|
|
1790
1793
|
|
|
1791
|
-
if (runres?.popup) retDirs.popup = runres.popup;
|
|
1792
|
-
res.json({ success: "ok", ...retDirs });
|
|
1794
|
+
//if (runres?.popup) retDirs.popup = runres.popup;
|
|
1795
|
+
res.json({ success: "ok", ...runres, ...retDirs });
|
|
1793
1796
|
} else {
|
|
1794
1797
|
if (run.context.goto) res.redirect(run.context.goto);
|
|
1795
1798
|
else res.redirect("/");
|
package/routes/admin.js
CHANGED
|
@@ -657,9 +657,11 @@ router.get(
|
|
|
657
657
|
{
|
|
658
658
|
label: req.__("When"),
|
|
659
659
|
key: (r) =>
|
|
660
|
-
`${moment(
|
|
661
|
-
r.created
|
|
662
|
-
|
|
660
|
+
`${moment(r.created).fromNow()}<br><small>${localeDateTime(
|
|
661
|
+
r.created,
|
|
662
|
+
{},
|
|
663
|
+
locale
|
|
664
|
+
)}</small>`,
|
|
663
665
|
},
|
|
664
666
|
{
|
|
665
667
|
label: req.__("Name"),
|
|
@@ -1193,10 +1195,10 @@ router.get(
|
|
|
1193
1195
|
th({ valign: "top" }, req.__("Saltcorn version")),
|
|
1194
1196
|
td(
|
|
1195
1197
|
packagejson.version,
|
|
1196
|
-
isRoot
|
|
1198
|
+
isRoot
|
|
1197
1199
|
? post_btn(
|
|
1198
1200
|
"/admin/upgrade",
|
|
1199
|
-
req.__("Upgrade"),
|
|
1201
|
+
req.__("Upgrade") + " (latest)",
|
|
1200
1202
|
req.csrfToken(),
|
|
1201
1203
|
{
|
|
1202
1204
|
btnClass: "btn-primary btn-sm",
|
|
@@ -3784,6 +3786,9 @@ router.post(
|
|
|
3784
3786
|
}
|
|
3785
3787
|
if (form.values.triggers) {
|
|
3786
3788
|
await db.deleteWhere("_sc_tag_entries", { not: { trigger_id: null } });
|
|
3789
|
+
await db.deleteWhere("_sc_workflow_trace");
|
|
3790
|
+
await db.deleteWhere("_sc_workflow_runs");
|
|
3791
|
+
await db.deleteWhere("_sc_workflow_steps");
|
|
3787
3792
|
await db.deleteWhere("_sc_triggers");
|
|
3788
3793
|
await getState().refresh_triggers();
|
|
3789
3794
|
}
|
package/routes/common_lists.js
CHANGED
|
@@ -290,7 +290,7 @@ const tagsDropdown = (tags, altHeader) =>
|
|
|
290
290
|
)
|
|
291
291
|
);
|
|
292
292
|
|
|
293
|
-
const mkAddBtn = (tags, entityType, id, req, myTagIds) =>
|
|
293
|
+
const mkAddBtn = (tags, entityType, id, req, myTagIds, on_done_redirect_str) =>
|
|
294
294
|
div(
|
|
295
295
|
{ class: "dropdown d-inline ms-1" },
|
|
296
296
|
span(
|
|
@@ -314,7 +314,7 @@ const mkAddBtn = (tags, entityType, id, req, myTagIds) =>
|
|
|
314
314
|
post_dropdown_item(
|
|
315
315
|
`/tag-entries/add-tag-entity/${encodeURIComponent(
|
|
316
316
|
t.name
|
|
317
|
-
)}/${entityType}/${id}`,
|
|
317
|
+
)}/${entityType}/${id}${on_done_redirect_str||""}`,
|
|
318
318
|
t.name,
|
|
319
319
|
req
|
|
320
320
|
)
|
|
@@ -345,7 +345,7 @@ const viewsList = async (
|
|
|
345
345
|
const tagBadges = (view) => {
|
|
346
346
|
const myTags = tag_entries.filter((te) => te.view_id === view.id);
|
|
347
347
|
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
348
|
-
const addBtn = mkAddBtn(tags, "views", view.id, req, myTagIds);
|
|
348
|
+
const addBtn = mkAddBtn(tags, "views", view.id, req, myTagIds, on_done_redirect_str);
|
|
349
349
|
return (
|
|
350
350
|
myTags.map((te) => tagBadge(tagsById[te.tag_id], "views")).join(nbsp) +
|
|
351
351
|
addBtn
|
|
@@ -709,7 +709,7 @@ const getTriggerList = async (
|
|
|
709
709
|
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
710
710
|
return (
|
|
711
711
|
myTags.map((te) => tagBadge(tagsById[te.tag_id], "triggers")).join(nbsp) +
|
|
712
|
-
mkAddBtn(tags, "triggers", trigger.id, req, myTagIds)
|
|
712
|
+
mkAddBtn(tags, "triggers", trigger.id, req, myTagIds, on_done_redirect_str)
|
|
713
713
|
);
|
|
714
714
|
};
|
|
715
715
|
return mkTable(
|
package/routes/delete.js
CHANGED
|
@@ -35,9 +35,11 @@ router.post(
|
|
|
35
35
|
// todo check that works after where change
|
|
36
36
|
const table = Table.findOne({ name: tableName });
|
|
37
37
|
const role = req.user && req.user.id ? req.user.role_id : 100;
|
|
38
|
+
const where = { [table.pk_name]: id };
|
|
39
|
+
|
|
38
40
|
try {
|
|
39
41
|
if (role <= table.min_role_write)
|
|
40
|
-
await table.deleteRows(
|
|
42
|
+
await table.deleteRows(where, req.user || { role_id: 100 });
|
|
41
43
|
else if (
|
|
42
44
|
(table.ownership_field_id || table.ownership_formula) &&
|
|
43
45
|
req.user
|
|
@@ -47,7 +49,7 @@ router.post(
|
|
|
47
49
|
{ forUser: req.user, forPublic: !req.user }
|
|
48
50
|
);
|
|
49
51
|
if (row && table.is_owner(req.user, row))
|
|
50
|
-
await table.deleteRows(
|
|
52
|
+
await table.deleteRows(where, req.user || { role_id: 100 });
|
|
51
53
|
else req.flash("error", req.__("Not authorized"));
|
|
52
54
|
} else
|
|
53
55
|
req.flash(
|
package/routes/fields.js
CHANGED
|
@@ -1378,6 +1378,7 @@ router.post(
|
|
|
1378
1378
|
agg_outcome_type,
|
|
1379
1379
|
agg_fieldview,
|
|
1380
1380
|
agg_field,
|
|
1381
|
+
mode,
|
|
1381
1382
|
_columndef,
|
|
1382
1383
|
} = req.body || {};
|
|
1383
1384
|
const table = Table.findOne({ name: tableName });
|
|
@@ -1389,7 +1390,9 @@ router.post(
|
|
|
1389
1390
|
return;
|
|
1390
1391
|
}
|
|
1391
1392
|
const field = table.getField(agg_field);
|
|
1392
|
-
const cfgfields = await applyAsync(fv.configFields, field || { table }
|
|
1393
|
+
const cfgfields = await applyAsync(fv.configFields, field || { table }, {
|
|
1394
|
+
mode,
|
|
1395
|
+
});
|
|
1393
1396
|
res.json(cfgfields);
|
|
1394
1397
|
return;
|
|
1395
1398
|
}
|
|
@@ -1412,7 +1415,12 @@ router.post(
|
|
|
1412
1415
|
res.send(req.query?.accept == "json" ? "[]" : "");
|
|
1413
1416
|
return;
|
|
1414
1417
|
}
|
|
1415
|
-
const fieldViewConfigForms = await calcfldViewConfig(
|
|
1418
|
+
const fieldViewConfigForms = await calcfldViewConfig(
|
|
1419
|
+
[field],
|
|
1420
|
+
false,
|
|
1421
|
+
0,
|
|
1422
|
+
mode
|
|
1423
|
+
);
|
|
1416
1424
|
const formFields = fieldViewConfigForms[field.name][fv_name];
|
|
1417
1425
|
if (!formFields) {
|
|
1418
1426
|
res.send(req.query?.accept == "json" ? "[]" : "");
|
package/routes/homepage.js
CHANGED
|
@@ -262,7 +262,10 @@ const actionsTab = async (req, triggers) => {
|
|
|
262
262
|
? p(req.__("No triggers"))
|
|
263
263
|
: mkTable(
|
|
264
264
|
[
|
|
265
|
-
{
|
|
265
|
+
{
|
|
266
|
+
label: req.__("Name"),
|
|
267
|
+
key: (tr) => a({ href: `actions/configure/${tr.id}` }, tr.name),
|
|
268
|
+
},
|
|
266
269
|
{ label: req.__("Action"), key: "action" },
|
|
267
270
|
{
|
|
268
271
|
label: req.__("Table or Channel"),
|
package/routes/list.js
CHANGED
|
@@ -282,6 +282,7 @@ router.get(
|
|
|
282
282
|
cellClick: "__delete_tabulator_row",
|
|
283
283
|
});
|
|
284
284
|
const isDark = getState().getLightDarkMode(req.user) === "dark";
|
|
285
|
+
const pkNm = table.pk_name
|
|
285
286
|
res.sendWrap(
|
|
286
287
|
{
|
|
287
288
|
title: req.__(`%s data table`, table.name),
|
|
@@ -428,7 +429,7 @@ router.get(
|
|
|
428
429
|
ajax_indicator(true);
|
|
429
430
|
$.ajax({
|
|
430
431
|
type: "POST",
|
|
431
|
-
url: "/api/${table.name}/" + (row
|
|
432
|
+
url: "/api/${table.name}/" + (row.${pkNm}||""),
|
|
432
433
|
data: row,
|
|
433
434
|
headers: {
|
|
434
435
|
"CSRF-Token": _sc_globalCsrf,
|
|
@@ -438,8 +439,8 @@ router.get(
|
|
|
438
439
|
ajax_indicator(false);
|
|
439
440
|
//if (item._versions) item._versions = +item._versions + 1;
|
|
440
441
|
//data.resolve(fixKeys(item));
|
|
441
|
-
if(resp.success &&(typeof resp.success ==="number" || typeof resp.success ==="string") && !row
|
|
442
|
-
window.tabulator_table.updateRow(cell.getRow(), {
|
|
442
|
+
if(resp.success &&(typeof resp.success ==="number" || typeof resp.success ==="string") && !row.${pkNm}) {
|
|
443
|
+
window.tabulator_table.updateRow(cell.getRow(), {${pkNm}: resp.success});
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
}).fail(function (resp) {
|
package/routes/pageedit.js
CHANGED
|
@@ -289,7 +289,7 @@ const getRootPageForm = (pages, pageGroups, roles, req) => {
|
|
|
289
289
|
input_type: "select",
|
|
290
290
|
options: [
|
|
291
291
|
"",
|
|
292
|
-
...pages.map((p) => p.name),
|
|
292
|
+
...pages.filter((p) => p.min_role >= r.id).map((p) => p.name),
|
|
293
293
|
...pageGroups.map((g) => ({
|
|
294
294
|
label: `${g.name} (group)`,
|
|
295
295
|
value: g.name,
|
package/routes/plugins.js
CHANGED
package/routes/tables.js
CHANGED
|
@@ -50,6 +50,7 @@ const {
|
|
|
50
50
|
code,
|
|
51
51
|
pre,
|
|
52
52
|
button,
|
|
53
|
+
text_attr,
|
|
53
54
|
} = require("@saltcorn/markup/tags");
|
|
54
55
|
const { stringify } = require("csv-stringify");
|
|
55
56
|
const TableConstraint = require("@saltcorn/data/models/table_constraints");
|
|
@@ -696,7 +697,14 @@ const typeBadges = (f, req) => {
|
|
|
696
697
|
if (f.primary_key) s += badge("warning", req.__("Primary key"));
|
|
697
698
|
if (f.required) s += badge("primary", req.__("Required"));
|
|
698
699
|
if (f.is_unique) s += badge("success", req.__("Unique"));
|
|
699
|
-
if (f.calculated)
|
|
700
|
+
if (f.calculated)
|
|
701
|
+
s += badge(
|
|
702
|
+
"info",
|
|
703
|
+
req.__("Calculated"),
|
|
704
|
+
f.expression && f.expression !== "__aggregation"
|
|
705
|
+
? text_attr(f.expression)
|
|
706
|
+
: undefined
|
|
707
|
+
);
|
|
700
708
|
if (f.stored) s += badge("warning", req.__("Stored"));
|
|
701
709
|
return s;
|
|
702
710
|
};
|
|
@@ -978,8 +986,8 @@ router.get(
|
|
|
978
986
|
table.name === "users"
|
|
979
987
|
? `/useradmin/`
|
|
980
988
|
: fields.length === 1
|
|
981
|
-
|
|
982
|
-
|
|
989
|
+
? `javascript:;` // Fix problem with edition of table with only one column ID / Primary Key
|
|
990
|
+
: `/list/${encodeURIComponent(table.name)}`,
|
|
983
991
|
},
|
|
984
992
|
i({ class: "fas fa-2x fa-edit" }),
|
|
985
993
|
"<br/>",
|
|
@@ -1535,12 +1543,12 @@ router.get(
|
|
|
1535
1543
|
r.type === "Unique"
|
|
1536
1544
|
? r.configuration.fields.join(", ")
|
|
1537
1545
|
: r.type === "Index" && r.configuration?.field === "_fts"
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1546
|
+
? "Full text search"
|
|
1547
|
+
: r.type === "Index"
|
|
1548
|
+
? r.configuration.field
|
|
1549
|
+
: r.type === "Formula"
|
|
1550
|
+
? r.configuration.formula
|
|
1551
|
+
: "",
|
|
1544
1552
|
},
|
|
1545
1553
|
{
|
|
1546
1554
|
label: req.__("Delete"),
|
|
@@ -1582,7 +1590,7 @@ const constraintForm = (req, table, fields, type) => {
|
|
|
1582
1590
|
case "Formula":
|
|
1583
1591
|
return new Form({
|
|
1584
1592
|
action: `/table/add-constraint/${table.id}/${type}`,
|
|
1585
|
-
|
|
1593
|
+
onSubmit: "press_store_button(this)",
|
|
1586
1594
|
fields: [
|
|
1587
1595
|
{
|
|
1588
1596
|
name: "formula",
|
|
@@ -1617,6 +1625,7 @@ const constraintForm = (req, table, fields, type) => {
|
|
|
1617
1625
|
blurb: req.__(
|
|
1618
1626
|
"Tick the boxes for the fields that should be jointly unique"
|
|
1619
1627
|
),
|
|
1628
|
+
onSubmit: "press_store_button(this)",
|
|
1620
1629
|
fields: [
|
|
1621
1630
|
...fields.map((f) => ({
|
|
1622
1631
|
name: f.name,
|
|
@@ -1641,7 +1650,7 @@ const constraintForm = (req, table, fields, type) => {
|
|
|
1641
1650
|
blurb: req.__(
|
|
1642
1651
|
"Choose the field to be indexed. This make searching the table faster."
|
|
1643
1652
|
),
|
|
1644
|
-
|
|
1653
|
+
onSubmit: "press_store_button(this)",
|
|
1645
1654
|
fields: [
|
|
1646
1655
|
{
|
|
1647
1656
|
type: "String",
|
package/routes/tag_entries.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
csrfField,
|
|
19
19
|
isAdminOrHasConfigMinRole,
|
|
20
20
|
checkEditPermission,
|
|
21
|
+
is_relative_url,
|
|
21
22
|
} = require("./utils");
|
|
22
23
|
|
|
23
24
|
const Table = require("@saltcorn/data/models/table");
|
|
@@ -192,18 +193,23 @@ router.post(
|
|
|
192
193
|
const auth = checkEditPermission(entitytype, req.user);
|
|
193
194
|
if (!auth) req.flash("error", "Not authorized");
|
|
194
195
|
else await tag.addEntry({ [fieldName]: +entityid });
|
|
196
|
+
let redirectTarget =
|
|
197
|
+
req.query.on_done_redirect &&
|
|
198
|
+
is_relative_url("/" + req.query.on_done_redirect)
|
|
199
|
+
? `/${req.query.on_done_redirect}`
|
|
200
|
+
: null;
|
|
195
201
|
switch (entitytype) {
|
|
196
202
|
case "views":
|
|
197
|
-
res.redirect(`/viewedit`);
|
|
203
|
+
res.redirect(redirectTarget || `/viewedit`);
|
|
198
204
|
break;
|
|
199
205
|
case "pages":
|
|
200
|
-
res.redirect(`/pageedit`);
|
|
206
|
+
res.redirect(redirectTarget || `/pageedit`);
|
|
201
207
|
break;
|
|
202
208
|
case "tables":
|
|
203
|
-
res.redirect(`/table`);
|
|
209
|
+
res.redirect(redirectTarget || `/table`);
|
|
204
210
|
break;
|
|
205
211
|
case "triggers":
|
|
206
|
-
res.redirect(`/actions`);
|
|
212
|
+
res.redirect(redirectTarget || `/actions`);
|
|
207
213
|
break;
|
|
208
214
|
|
|
209
215
|
default:
|
package/tests/admin.test.js
CHANGED
|
@@ -647,6 +647,9 @@ describe("clear all page", () => {
|
|
|
647
647
|
.send("users=on")
|
|
648
648
|
.send("config=on")
|
|
649
649
|
.send("plugins=on")
|
|
650
|
+
.send("triggers=on")
|
|
651
|
+
.send("library=on")
|
|
652
|
+
.send("eventlog=on")
|
|
650
653
|
.expect(toRedirect("/auth/create_first_user"));
|
|
651
654
|
});
|
|
652
655
|
it("restores backup after clear all", async () => {
|
package/tests/view.test.js
CHANGED
|
@@ -1134,3 +1134,41 @@ describe("legacy relations with relation path", () => {
|
|
|
1134
1134
|
.expect(toInclude("Delete"));
|
|
1135
1135
|
});
|
|
1136
1136
|
});
|
|
1137
|
+
|
|
1138
|
+
describe("identical fields", () => {
|
|
1139
|
+
it("runs a post with an author array ", async () => {
|
|
1140
|
+
const app = await getApp({ disableCsrf: true });
|
|
1141
|
+
const loginCookie = await getAdminLoginCookie();
|
|
1142
|
+
await request(app)
|
|
1143
|
+
.post("/view/authoredit_identicals")
|
|
1144
|
+
.set("Cookie", loginCookie)
|
|
1145
|
+
.send("author=Charles&author=Charles")
|
|
1146
|
+
.expect(toRedirect("/view/authorlist"));
|
|
1147
|
+
const table = Table.findOne({ name: "books" });
|
|
1148
|
+
const rows = await table.getRows();
|
|
1149
|
+
expect(rows).toContainEqual({
|
|
1150
|
+
id: 3,
|
|
1151
|
+
author: "Charles",
|
|
1152
|
+
pages: 678,
|
|
1153
|
+
publisher: null,
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it("runs a post with only one author", async () => {
|
|
1158
|
+
const app = await getApp({ disableCsrf: true });
|
|
1159
|
+
const loginCookie = await getAdminLoginCookie();
|
|
1160
|
+
await request(app)
|
|
1161
|
+
.post("/view/authoredit_identicals")
|
|
1162
|
+
.set("Cookie", loginCookie)
|
|
1163
|
+
.send("author=Fjodor")
|
|
1164
|
+
.expect(toRedirect("/view/authorlist"));
|
|
1165
|
+
const table = Table.findOne({ name: "books" });
|
|
1166
|
+
const rows = await table.getRows();
|
|
1167
|
+
expect(rows).toContainEqual({
|
|
1168
|
+
id: 4,
|
|
1169
|
+
author: "Fjodor",
|
|
1170
|
+
pages: 678,
|
|
1171
|
+
publisher: null,
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
});
|