@saltcorn/server 1.0.0-beta.14 → 1.0.0-beta.16
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/Date format.tmd +55 -0
- package/help/index.js +2 -0
- package/load_plugins.js +1 -1
- package/locales/en.json +3 -2
- package/package.json +9 -9
- package/public/saltcorn-common.js +7 -3
- package/public/saltcorn.js +22 -5
- package/routes/actions.js +4 -2
- package/routes/admin.js +55 -4
- package/routes/eventlog.js +2 -1
- package/routes/fields.js +12 -5
- package/routes/infoarch.js +32 -1
- package/routes/menu.js +3 -0
- package/routes/pageedit.js +7 -1
- package/routes/sync.js +2 -1
- package/routes/tables.js +9 -3
- package/tests/fields.test.js +32 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
You can define how a date value should be shown by specifying a date format string
|
|
2
|
+
which supports a range of formatting options. The formatting is provided by the
|
|
3
|
+
[Day.js](https://day.js.org/docs/en/display/format) library with the
|
|
4
|
+
[AdvancedFormat](https://day.js.org/docs/en/plugin/advanced-format) extension. Those links
|
|
5
|
+
give the full definition for the available format options.
|
|
6
|
+
|
|
7
|
+
When a date is rendered with a format string, certain specific character sequences are
|
|
8
|
+
substituted for parts of the date value. Any unrecognized characters are repeated verbatim
|
|
9
|
+
in the output and you can used that for separator charcters such as hyphens (-) or slashes (/).
|
|
10
|
+
|
|
11
|
+
For example, the timepoint 11.15am on Friday, 4 October 2024 is rendered, according to the
|
|
12
|
+
format string:
|
|
13
|
+
|
|
14
|
+
{{# const d = new Date('2024-10-04T11:15:00.000')}}
|
|
15
|
+
|
|
16
|
+
| Format string | Output |
|
|
17
|
+
| ------------------- | ------------ |
|
|
18
|
+
| YYYY-MM-DD | {{ moment(d).format("YYYY-MM-DD")}} |
|
|
19
|
+
| H:mm on dddd, D MMMM YYYY | {{ moment(d).format("H:mm on dddd, D MMMM YYYY")}} |
|
|
20
|
+
|
|
21
|
+
The full list of supported format sequences are:
|
|
22
|
+
|
|
23
|
+
| Format | Output | Description |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `YY` | 18 | Two-digit year |
|
|
26
|
+
| `YYYY` | 2018 | Four-digit year |
|
|
27
|
+
| `M` | 1-12 | The month, beginning at 1 |
|
|
28
|
+
| `MM` | 01-12 | The month, 2-digits |
|
|
29
|
+
| `MMM` | Jan-Dec | The abbreviated month name |
|
|
30
|
+
| `MMMM` | January-December | The full month name |
|
|
31
|
+
| `D` | 1-31 | The day of the month |
|
|
32
|
+
| `DD` | 01-31 | The day of the month, 2-digits |
|
|
33
|
+
| `d` | 0-6 | The day of the week, with Sunday as 0 |
|
|
34
|
+
| `dd` | Su-Sa | The min name of the day of the week |
|
|
35
|
+
| `ddd` | Sun-Sat | The short name of the day of the week |
|
|
36
|
+
| `dddd` | Sunday-Saturday | The name of the day of the week |
|
|
37
|
+
| `H` | 0-23 | The hour |
|
|
38
|
+
| `HH` | 00-23 | The hour, 2-digits |
|
|
39
|
+
| `h` | 1-12 | The hour, 12-hour clock |
|
|
40
|
+
| `hh` | 01-12 | The hour, 12-hour clock, 2-digits |
|
|
41
|
+
| `m` | 0-59 | The minute |
|
|
42
|
+
| `mm` | 00-59 | The minute, 2-digits |
|
|
43
|
+
| `s` | 0-59 | The second |
|
|
44
|
+
| `ss` | 00-59 | The second, 2-digits |
|
|
45
|
+
| `SSS` | 000-999 | The millisecond, 3-digits |
|
|
46
|
+
| `Z` | +05:00 | The offset from UTC, ±HH:mm |
|
|
47
|
+
| `ZZ` | +0500 | The offset from UTC, ±HHmm |
|
|
48
|
+
| `A` | AM PM | |
|
|
49
|
+
| `a` | am pm | |
|
|
50
|
+
| `Q` | 1-4 | Quarter |
|
|
51
|
+
| `Do` | 1st 2nd ... 31st | Day of Month with ordinal |
|
|
52
|
+
| `k` | 1-24 | The hour, beginning at 1 |
|
|
53
|
+
| `kk` | 01-24 | The hour, 2-digits, beginning at 1 |
|
|
54
|
+
| `X` | 1360013296 | Unix Timestamp in second |
|
|
55
|
+
| `x` | 1360013296123 | Unix Timestamp in millisecond |
|
package/help/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const _ = require("underscore");
|
|
|
5
5
|
const fs = require("fs").promises;
|
|
6
6
|
const MarkdownIt = require("markdown-it"),
|
|
7
7
|
md = new MarkdownIt();
|
|
8
|
+
const moment = require("moment");
|
|
8
9
|
|
|
9
10
|
const { pre } = require("@saltcorn/markup/tags");
|
|
10
11
|
const path = require("path");
|
|
@@ -33,6 +34,7 @@ const get_help_markup = async (topic, query, req) => {
|
|
|
33
34
|
scState: getState(),
|
|
34
35
|
query,
|
|
35
36
|
oneOf,
|
|
37
|
+
moment,
|
|
36
38
|
};
|
|
37
39
|
const mdTemplate = await get_md_file(topic);
|
|
38
40
|
if (!mdTemplate) return { markup: "Topic not found" };
|
package/load_plugins.js
CHANGED
package/locales/en.json
CHANGED
|
@@ -1472,5 +1472,6 @@
|
|
|
1472
1472
|
"Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>": "Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>",
|
|
1473
1473
|
"Select tag": "Select tag",
|
|
1474
1474
|
"Invalid build directory path": "Invalid build directory path",
|
|
1475
|
-
"Invalid build directory name": "Invalid build directory name"
|
|
1476
|
-
|
|
1475
|
+
"Invalid build directory name": "Invalid build directory name",
|
|
1476
|
+
"clean node_modules": "clean node_modules"
|
|
1477
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.16",
|
|
4
4
|
"description": "Server app for Saltcorn, open-source no-code platform",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@aws-sdk/client-s3": "^3.451.0",
|
|
10
|
-
"@saltcorn/base-plugin": "1.0.0-beta.
|
|
11
|
-
"@saltcorn/builder": "1.0.0-beta.
|
|
12
|
-
"@saltcorn/data": "1.0.0-beta.
|
|
13
|
-
"@saltcorn/admin-models": "1.0.0-beta.
|
|
14
|
-
"@saltcorn/filemanager": "1.0.0-beta.
|
|
15
|
-
"@saltcorn/markup": "1.0.0-beta.
|
|
16
|
-
"@saltcorn/plugins-loader": "1.0.0-beta.
|
|
17
|
-
"@saltcorn/sbadmin2": "1.0.0-beta.
|
|
10
|
+
"@saltcorn/base-plugin": "1.0.0-beta.16",
|
|
11
|
+
"@saltcorn/builder": "1.0.0-beta.16",
|
|
12
|
+
"@saltcorn/data": "1.0.0-beta.16",
|
|
13
|
+
"@saltcorn/admin-models": "1.0.0-beta.16",
|
|
14
|
+
"@saltcorn/filemanager": "1.0.0-beta.16",
|
|
15
|
+
"@saltcorn/markup": "1.0.0-beta.16",
|
|
16
|
+
"@saltcorn/plugins-loader": "1.0.0-beta.16",
|
|
17
|
+
"@saltcorn/sbadmin2": "1.0.0-beta.16",
|
|
18
18
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
19
19
|
"@socket.io/sticky": "^1.0.1",
|
|
20
20
|
"adm-zip": "0.5.10",
|
|
@@ -18,7 +18,7 @@ function monospace_block_click(e) {
|
|
|
18
18
|
function copy_monospace_block(e) {
|
|
19
19
|
let e1 = $(e).next("pre");
|
|
20
20
|
let e2 = $(e1).next("pre");
|
|
21
|
-
if (!e2.length) return navigator.clipboard.writeText($(
|
|
21
|
+
if (!e2.length) return navigator.clipboard.writeText($(e1).text());
|
|
22
22
|
const e1t = e1.text();
|
|
23
23
|
const e2t = e2.text();
|
|
24
24
|
if (e1t.length > e2t.length) return navigator.clipboard.writeText(e1t);
|
|
@@ -1365,10 +1365,14 @@ function notifyAlert(note, spin) {
|
|
|
1365
1365
|
if (typeof note == "string") {
|
|
1366
1366
|
txt = note;
|
|
1367
1367
|
type = "info";
|
|
1368
|
-
} else {
|
|
1368
|
+
} else if (note.text) {
|
|
1369
1369
|
txt = note.text;
|
|
1370
|
-
type = note.type;
|
|
1370
|
+
type = note.type || "info";
|
|
1371
|
+
} else {
|
|
1372
|
+
type = "info";
|
|
1373
|
+
txt = JSON.stringify(note, null, 2);
|
|
1371
1374
|
}
|
|
1375
|
+
|
|
1372
1376
|
const { id, html } = buildToast(txt, type, spin);
|
|
1373
1377
|
let $modal = $("#scmodal");
|
|
1374
1378
|
if ($modal.length && $modal.hasClass("show"))
|
package/public/saltcorn.js
CHANGED
|
@@ -225,8 +225,10 @@ function ajax_done(res, viewname) {
|
|
|
225
225
|
function spin_action_link(e) {
|
|
226
226
|
const $e = $(e);
|
|
227
227
|
const width = $e.width();
|
|
228
|
+
const height = $e.height();
|
|
229
|
+
|
|
228
230
|
$e.attr("data-innerhtml-prespin", $e.html());
|
|
229
|
-
$e.html('<i class="fas fa-spinner fa-spin"></i>').width(width);
|
|
231
|
+
$e.html('<i class="fas fa-spinner fa-spin"></i>').width(width).height(height);
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
function reset_spinners() {
|
|
@@ -870,7 +872,7 @@ function build_mobile_app(button) {
|
|
|
870
872
|
|
|
871
873
|
if (
|
|
872
874
|
params.useDocker &&
|
|
873
|
-
!cordovaBuilderAvailable &&
|
|
875
|
+
!window.cordovaBuilderAvailable &&
|
|
874
876
|
!confirm(
|
|
875
877
|
"Docker is selected but the Cordova builder seems not to be installed. " +
|
|
876
878
|
"Do you really want to continue?"
|
|
@@ -878,6 +880,21 @@ function build_mobile_app(button) {
|
|
|
878
880
|
) {
|
|
879
881
|
return;
|
|
880
882
|
}
|
|
883
|
+
|
|
884
|
+
const notSupportedPlugins = params.includedPlugins.filter(
|
|
885
|
+
(plugin) => !window.pluginsReadyForMobile.includes(plugin)
|
|
886
|
+
);
|
|
887
|
+
if (
|
|
888
|
+
notSupportedPlugins.length > 0 &&
|
|
889
|
+
!confirm(
|
|
890
|
+
`It seems that the plugins '${notSupportedPlugins.join(
|
|
891
|
+
", "
|
|
892
|
+
)}' are not ready for mobile. Do you really want to continue?`
|
|
893
|
+
)
|
|
894
|
+
) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
881
898
|
ajax_post("/admin/build-mobile-app", {
|
|
882
899
|
data: params,
|
|
883
900
|
success: (data) => {
|
|
@@ -949,8 +966,8 @@ function check_cordova_builder() {
|
|
|
949
966
|
$.ajax("/admin/mobile-app/check-cordova-builder", {
|
|
950
967
|
type: "GET",
|
|
951
968
|
success: function (res) {
|
|
952
|
-
cordovaBuilderAvailable = !!res.installed;
|
|
953
|
-
if (cordovaBuilderAvailable) {
|
|
969
|
+
window.cordovaBuilderAvailable = !!res.installed;
|
|
970
|
+
if (window.cordovaBuilderAvailable) {
|
|
954
971
|
$("#dockerBuilderStatusId").html(
|
|
955
972
|
`<span>
|
|
956
973
|
installed<i class="ps-2 fas fa-check text-success"></i>
|
|
@@ -1044,7 +1061,7 @@ function toggle_tbl_sync() {
|
|
|
1044
1061
|
function toggle_android_platform() {
|
|
1045
1062
|
if ($("#androidCheckboxId")[0].checked === true) {
|
|
1046
1063
|
$("#dockerCheckboxId").attr("hidden", false);
|
|
1047
|
-
$("#dockerCheckboxId").attr("checked", cordovaBuilderAvailable);
|
|
1064
|
+
$("#dockerCheckboxId").attr("checked", window.cordovaBuilderAvailable);
|
|
1048
1065
|
$("#dockerLabelId").removeClass("d-none");
|
|
1049
1066
|
} else {
|
|
1050
1067
|
$("#dockerCheckboxId").attr("hidden", true);
|
package/routes/actions.js
CHANGED
|
@@ -154,10 +154,12 @@ const triggerForm = async (req, trigger) => {
|
|
|
154
154
|
.filter(([k, v]) => v.hasChannel)
|
|
155
155
|
.map(([k, v]) => k);
|
|
156
156
|
|
|
157
|
-
const allActions = Trigger.action_options(false);
|
|
157
|
+
const allActions = Trigger.action_options({ notRequireRow: false });
|
|
158
158
|
const table_triggers = ["Insert", "Update", "Delete", "Validate"];
|
|
159
159
|
const action_options = {};
|
|
160
|
-
const actionsNotRequiringRow = Trigger.action_options(
|
|
160
|
+
const actionsNotRequiringRow = Trigger.action_options({
|
|
161
|
+
notRequireRow: true,
|
|
162
|
+
});
|
|
161
163
|
|
|
162
164
|
Trigger.when_options.forEach((t) => {
|
|
163
165
|
if (table_triggers.includes(t)) action_options[t] = allActions;
|
package/routes/admin.js
CHANGED
|
@@ -1281,6 +1281,7 @@ router.get(
|
|
|
1281
1281
|
throw new Error(req.__("Unable to fetch versions"));
|
|
1282
1282
|
const versions = Object.keys(pkgInfo.versions);
|
|
1283
1283
|
if (versions.length === 0) throw new Error(req.__("No versions found"));
|
|
1284
|
+
const tags = pkgInfo["dist-tags"] || {};
|
|
1284
1285
|
res.set("Page-Title", req.__("%s versions", "Saltcorn"));
|
|
1285
1286
|
let selected = packagejson.version;
|
|
1286
1287
|
res.send(
|
|
@@ -1290,6 +1291,7 @@ router.get(
|
|
|
1290
1291
|
method: "post",
|
|
1291
1292
|
},
|
|
1292
1293
|
input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
|
|
1294
|
+
// version select
|
|
1293
1295
|
div(
|
|
1294
1296
|
{ class: "form-group" },
|
|
1295
1297
|
label(
|
|
@@ -1315,6 +1317,37 @@ router.get(
|
|
|
1315
1317
|
)
|
|
1316
1318
|
)
|
|
1317
1319
|
),
|
|
1320
|
+
// tag select
|
|
1321
|
+
div(
|
|
1322
|
+
{ class: "form-group" },
|
|
1323
|
+
label(
|
|
1324
|
+
{
|
|
1325
|
+
for: "tag_select",
|
|
1326
|
+
class: "form-label fw-bold",
|
|
1327
|
+
},
|
|
1328
|
+
req.__("Tags")
|
|
1329
|
+
),
|
|
1330
|
+
select(
|
|
1331
|
+
{
|
|
1332
|
+
id: "tag_select",
|
|
1333
|
+
class: "form-control form-select",
|
|
1334
|
+
},
|
|
1335
|
+
option({
|
|
1336
|
+
id: "empty_opt",
|
|
1337
|
+
value: "",
|
|
1338
|
+
label: req.__("Select tag"),
|
|
1339
|
+
selected: true,
|
|
1340
|
+
}),
|
|
1341
|
+
Object.keys(tags).map((tag) =>
|
|
1342
|
+
option({
|
|
1343
|
+
id: `${tag}_opt`,
|
|
1344
|
+
value: tags[tag],
|
|
1345
|
+
label: `${tag} (${tags[tag]})`,
|
|
1346
|
+
})
|
|
1347
|
+
)
|
|
1348
|
+
)
|
|
1349
|
+
),
|
|
1350
|
+
// deep clean checkbox
|
|
1318
1351
|
div(
|
|
1319
1352
|
{ class: "form-group" },
|
|
1320
1353
|
input({
|
|
@@ -1351,7 +1384,18 @@ router.get(
|
|
|
1351
1384
|
req.__("Install")
|
|
1352
1385
|
)
|
|
1353
1386
|
)
|
|
1354
|
-
)
|
|
1387
|
+
) +
|
|
1388
|
+
script(
|
|
1389
|
+
domReady(`
|
|
1390
|
+
document.getElementById('tag_select').addEventListener('change', () => {
|
|
1391
|
+
const version = document.getElementById('tag_select').value;
|
|
1392
|
+
if (version) document.getElementById('version_select').value = version;
|
|
1393
|
+
});
|
|
1394
|
+
document.getElementById('version_select').addEventListener('change', () => {
|
|
1395
|
+
document.getElementById('tag_select').value = '';
|
|
1396
|
+
});
|
|
1397
|
+
`)
|
|
1398
|
+
)
|
|
1355
1399
|
);
|
|
1356
1400
|
} catch (error) {
|
|
1357
1401
|
getState().log(
|
|
@@ -1780,8 +1824,8 @@ router.get(
|
|
|
1780
1824
|
});
|
|
1781
1825
|
})
|
|
1782
1826
|
);
|
|
1783
|
-
const buildDialogScript = (cordovaBuilderAvailable) =>
|
|
1784
|
-
|
|
1827
|
+
const buildDialogScript = (cordovaBuilderAvailable) =>
|
|
1828
|
+
`<script>
|
|
1785
1829
|
var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
|
|
1786
1830
|
function showEntrySelect(type) {
|
|
1787
1831
|
for( const currentType of ["view", "page", "pagegroup"]) {
|
|
@@ -1807,7 +1851,6 @@ const buildDialogScript = (cordovaBuilderAvailable) => {
|
|
|
1807
1851
|
notifyAlert("Building the app, please wait.", true)
|
|
1808
1852
|
}
|
|
1809
1853
|
</script>`;
|
|
1810
|
-
};
|
|
1811
1854
|
|
|
1812
1855
|
const imageAvailable = async () => {
|
|
1813
1856
|
try {
|
|
@@ -1865,6 +1908,9 @@ router.get(
|
|
|
1865
1908
|
const plugins = (await Plugin.find()).filter(
|
|
1866
1909
|
(plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
|
|
1867
1910
|
);
|
|
1911
|
+
const pluginsReadyForMobile = plugins
|
|
1912
|
+
.filter((plugin) => plugin.ready_for_mobile())
|
|
1913
|
+
.map((plugin) => plugin.name);
|
|
1868
1914
|
const builderSettings =
|
|
1869
1915
|
getState().getConfig("mobile_builder_settings") || {};
|
|
1870
1916
|
const dockerAvailable = await imageAvailable();
|
|
@@ -1879,6 +1925,11 @@ router.get(
|
|
|
1879
1925
|
{
|
|
1880
1926
|
headerTag: buildDialogScript(dockerAvailable),
|
|
1881
1927
|
},
|
|
1928
|
+
{
|
|
1929
|
+
headerTag: `<script>var pluginsReadyForMobile = ${JSON.stringify(
|
|
1930
|
+
pluginsReadyForMobile
|
|
1931
|
+
)}</script>`,
|
|
1932
|
+
},
|
|
1882
1933
|
],
|
|
1883
1934
|
contents: {
|
|
1884
1935
|
above: [
|
package/routes/eventlog.js
CHANGED
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
i,
|
|
42
42
|
th,
|
|
43
43
|
pre,
|
|
44
|
+
text,
|
|
44
45
|
} = require("@saltcorn/markup/tags");
|
|
45
46
|
const Table = require("@saltcorn/data/models/table");
|
|
46
47
|
const { send_events_page } = require("../markup/admin.js");
|
|
@@ -442,7 +443,7 @@ router.get(
|
|
|
442
443
|
) +
|
|
443
444
|
div(
|
|
444
445
|
{ class: "eventpayload" },
|
|
445
|
-
ev.payload ? pre(JSON.stringify(ev.payload, null, 2)) : ""
|
|
446
|
+
ev.payload ? pre(text(JSON.stringify(ev.payload, null, 2))) : ""
|
|
446
447
|
),
|
|
447
448
|
},
|
|
448
449
|
});
|
package/routes/fields.js
CHANGED
|
@@ -919,12 +919,19 @@ router.post(
|
|
|
919
919
|
"/test-formula",
|
|
920
920
|
isAdmin,
|
|
921
921
|
error_catcher(async (req, res) => {
|
|
922
|
-
|
|
922
|
+
let { formula, tablename, stored } = req.body;
|
|
923
|
+
if (stored === "false") stored = false;
|
|
924
|
+
|
|
923
925
|
const table = Table.findOne({ name: tablename });
|
|
924
926
|
const fields = table.getFields();
|
|
925
927
|
const freeVars = freeVariables(formula);
|
|
926
928
|
const joinFields = {};
|
|
927
|
-
|
|
929
|
+
add_free_variables_to_joinfields(freeVars, joinFields, fields);
|
|
930
|
+
if (!stored && Object.keys(joinFields).length > 0) {
|
|
931
|
+
return res
|
|
932
|
+
.status(400)
|
|
933
|
+
.send(`Joinfields only permitted in stored calculated fields`);
|
|
934
|
+
}
|
|
928
935
|
const rows = await table.getJoinedRows({
|
|
929
936
|
joinFields,
|
|
930
937
|
orderBy: "RANDOM()",
|
|
@@ -950,9 +957,9 @@ router.post(
|
|
|
950
957
|
);
|
|
951
958
|
} catch (e) {
|
|
952
959
|
console.error(e);
|
|
953
|
-
return res
|
|
954
|
-
|
|
955
|
-
|
|
960
|
+
return res
|
|
961
|
+
.status(400)
|
|
962
|
+
.send(`Error on running on row with id=${rows[0].id}: ${e.message}`);
|
|
956
963
|
}
|
|
957
964
|
})
|
|
958
965
|
);
|
package/routes/infoarch.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
} = require("../markup/admin.js");
|
|
15
15
|
const { getState } = require("@saltcorn/data/db/state");
|
|
16
16
|
const { div, a, i, text } = require("@saltcorn/markup/tags");
|
|
17
|
-
const { mkTable, renderForm } = require("@saltcorn/markup");
|
|
17
|
+
const { mkTable, renderForm, post_delete_btn } = require("@saltcorn/markup");
|
|
18
18
|
const Form = require("@saltcorn/data/models/form");
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -119,6 +119,15 @@ router.get(
|
|
|
119
119
|
})
|
|
120
120
|
: "",
|
|
121
121
|
},
|
|
122
|
+
{
|
|
123
|
+
label: req.__("Delete"),
|
|
124
|
+
key: (r) =>
|
|
125
|
+
post_delete_btn(
|
|
126
|
+
`/site-structure/localizer/delete-lang/${r.locale}`,
|
|
127
|
+
req,
|
|
128
|
+
r.name
|
|
129
|
+
),
|
|
130
|
+
},
|
|
122
131
|
],
|
|
123
132
|
Object.values(cfgLangs)
|
|
124
133
|
),
|
|
@@ -287,3 +296,25 @@ router.post(
|
|
|
287
296
|
}
|
|
288
297
|
})
|
|
289
298
|
);
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @name post/localizer/save-lang
|
|
302
|
+
* @function
|
|
303
|
+
* @memberof module:routes/infoarch~infoarchRouter
|
|
304
|
+
* @function
|
|
305
|
+
*/
|
|
306
|
+
router.post(
|
|
307
|
+
"/localizer/delete-lang/:lang",
|
|
308
|
+
isAdmin,
|
|
309
|
+
error_catcher(async (req, res) => {
|
|
310
|
+
const { lang } = req.params;
|
|
311
|
+
|
|
312
|
+
const cfgLangs = getState().getConfig("localizer_languages");
|
|
313
|
+
if (cfgLangs[lang]) {
|
|
314
|
+
delete cfgLangs[lang];
|
|
315
|
+
await getState().setConfig("localizer_languages", cfgLangs);
|
|
316
|
+
}
|
|
317
|
+
if (!req.xhr) res.redirect(`/site-structure/localizer`);
|
|
318
|
+
else res.json({ success: "ok" });
|
|
319
|
+
})
|
|
320
|
+
);
|
package/routes/menu.js
CHANGED
package/routes/pageedit.js
CHANGED
|
@@ -183,6 +183,12 @@ const pageBuilderData = async (req, context) => {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
const actionsNotRequiringRow = Trigger.action_options({
|
|
187
|
+
notRequireRow: true,
|
|
188
|
+
apiNeverTriggers: true,
|
|
189
|
+
builtInLabel: "Page Actions",
|
|
190
|
+
builtIns: ["GoBack"],
|
|
191
|
+
});
|
|
186
192
|
const library = (await Library.find({})).filter((l) => l.suitableFor("page"));
|
|
187
193
|
const fixed_state_fields = {};
|
|
188
194
|
for (const view of views) {
|
|
@@ -228,7 +234,7 @@ const pageBuilderData = async (req, context) => {
|
|
|
228
234
|
images,
|
|
229
235
|
pages,
|
|
230
236
|
page_groups,
|
|
231
|
-
actions,
|
|
237
|
+
actions: actionsNotRequiringRow,
|
|
232
238
|
builtInActions: ["GoBack"],
|
|
233
239
|
library,
|
|
234
240
|
min_role: context.min_role,
|
package/routes/sync.js
CHANGED
|
@@ -335,13 +335,14 @@ router.post(
|
|
|
335
335
|
"/clean_sync_dir",
|
|
336
336
|
error_catcher(async (req, res) => {
|
|
337
337
|
const { dir_name } = req.body;
|
|
338
|
+
const safe_dir_name = File.normalise(dir_name);
|
|
338
339
|
try {
|
|
339
340
|
const rootFolder = await File.rootFolder();
|
|
340
341
|
const syncDir = path.join(
|
|
341
342
|
rootFolder.location,
|
|
342
343
|
"mobile_app",
|
|
343
344
|
"sync",
|
|
344
|
-
|
|
345
|
+
safe_dir_name
|
|
345
346
|
);
|
|
346
347
|
await fs.rm(syncDir, { recursive: true, force: true });
|
|
347
348
|
res.status(200).send("");
|
package/routes/tables.js
CHANGED
|
@@ -670,7 +670,10 @@ router.get(
|
|
|
670
670
|
* @param {string} lbl
|
|
671
671
|
* @returns {string}
|
|
672
672
|
*/
|
|
673
|
-
const badge = (col, lbl) =>
|
|
673
|
+
const badge = (col, lbl, title) =>
|
|
674
|
+
`<span ${
|
|
675
|
+
title ? `title="${title}" ` : ""
|
|
676
|
+
}class="badge bg-${col}">${lbl}</span> `;
|
|
674
677
|
|
|
675
678
|
/**
|
|
676
679
|
* @param {object} f
|
|
@@ -708,8 +711,11 @@ const attribBadges = (f) => {
|
|
|
708
711
|
].includes(k)
|
|
709
712
|
)
|
|
710
713
|
return;
|
|
711
|
-
if(Array.isArray(v) && !v.length) return;
|
|
712
|
-
|
|
714
|
+
if (Array.isArray(v) && !v.length) return;
|
|
715
|
+
const title = ["string", "number", "boolean"].includes(typeof v)
|
|
716
|
+
? `${v}`
|
|
717
|
+
: null;
|
|
718
|
+
if (v || v === 0) s += badge("secondary", k, title);
|
|
713
719
|
});
|
|
714
720
|
}
|
|
715
721
|
return s;
|
package/tests/fields.test.js
CHANGED
|
@@ -350,6 +350,38 @@ describe("Field Endpoints", () => {
|
|
|
350
350
|
.set("Cookie", loginCookie)
|
|
351
351
|
.expect(toInclude(" is: <pre>2</pre>"));
|
|
352
352
|
});
|
|
353
|
+
it("should show on stored expression with joinfield", async () => {
|
|
354
|
+
const loginCookie = await getAdminLoginCookie();
|
|
355
|
+
const table = Table.findOne({ name: "books" });
|
|
356
|
+
|
|
357
|
+
const ctx = encodeURIComponent(JSON.stringify({ table_id: table.id }));
|
|
358
|
+
const app = await getApp({ disableCsrf: true });
|
|
359
|
+
await request(app)
|
|
360
|
+
.post("/field/test-formula")
|
|
361
|
+
.send({
|
|
362
|
+
formula: "publisher.name",
|
|
363
|
+
tablename: "books",
|
|
364
|
+
stored: true,
|
|
365
|
+
})
|
|
366
|
+
.set("Cookie", loginCookie)
|
|
367
|
+
.expect(toInclude(" is: <pre>"));
|
|
368
|
+
});
|
|
369
|
+
it("should fail on non-stored expression with joinfield", async () => {
|
|
370
|
+
const loginCookie = await getAdminLoginCookie();
|
|
371
|
+
const table = Table.findOne({ name: "books" });
|
|
372
|
+
|
|
373
|
+
const ctx = encodeURIComponent(JSON.stringify({ table_id: table.id }));
|
|
374
|
+
const app = await getApp({ disableCsrf: true });
|
|
375
|
+
await request(app)
|
|
376
|
+
.post("/field/test-formula")
|
|
377
|
+
.send({
|
|
378
|
+
formula: "publisher.name",
|
|
379
|
+
tablename: "books",
|
|
380
|
+
stored: false,
|
|
381
|
+
})
|
|
382
|
+
.set("Cookie", loginCookie)
|
|
383
|
+
.expect(400);
|
|
384
|
+
});
|
|
353
385
|
it("should show calculated", async () => {
|
|
354
386
|
const loginCookie = await getAdminLoginCookie();
|
|
355
387
|
const table = Table.findOne({ name: "books" });
|