@saltcorn/server 0.9.6-beta.8 → 0.9.6

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.
Files changed (47) hide show
  1. package/app.js +9 -2
  2. package/auth/admin.js +51 -52
  3. package/auth/roleadmin.js +6 -2
  4. package/auth/routes.js +28 -10
  5. package/auth/testhelp.js +86 -0
  6. package/help/Field label.tmd +11 -0
  7. package/help/Field types.tmd +39 -0
  8. package/help/Inclusion Formula.tmd +38 -0
  9. package/help/Ownership field.tmd +76 -0
  10. package/help/Ownership formula.tmd +75 -0
  11. package/help/Table roles.tmd +20 -0
  12. package/help/User groups.tmd +35 -0
  13. package/help/User roles.tmd +30 -0
  14. package/load_plugins.js +28 -4
  15. package/locales/en.json +28 -1
  16. package/locales/it.json +3 -2
  17. package/markup/forms.js +5 -1
  18. package/package.json +9 -9
  19. package/public/log_viewer_utils.js +32 -0
  20. package/public/mermaid.min.js +705 -306
  21. package/public/saltcorn-builder.css +23 -0
  22. package/public/saltcorn-common.js +195 -71
  23. package/public/saltcorn.css +72 -0
  24. package/public/saltcorn.js +78 -0
  25. package/restart_watcher.js +1 -0
  26. package/routes/actions.js +27 -0
  27. package/routes/admin.js +180 -66
  28. package/routes/api.js +6 -0
  29. package/routes/common_lists.js +42 -32
  30. package/routes/fields.js +9 -1
  31. package/routes/homepage.js +2 -0
  32. package/routes/menu.js +69 -4
  33. package/routes/notifications.js +90 -10
  34. package/routes/pageedit.js +18 -13
  35. package/routes/plugins.js +5 -1
  36. package/routes/search.js +10 -4
  37. package/routes/tables.js +47 -27
  38. package/routes/tenant.js +4 -15
  39. package/routes/utils.js +20 -6
  40. package/routes/viewedit.js +11 -7
  41. package/serve.js +27 -5
  42. package/tests/edit.test.js +426 -0
  43. package/tests/fields.test.js +21 -0
  44. package/tests/filter.test.js +68 -0
  45. package/tests/page.test.js +2 -2
  46. package/tests/sync.test.js +59 -0
  47. package/wrapper.js +4 -1
@@ -509,3 +509,26 @@ div.builder-config-field {
509
509
  border: 1px solid black;
510
510
  margin-top: 2px;
511
511
  }
512
+
513
+ div.componets-and-library-accordion {
514
+ padding-right: 0 !important;
515
+ }
516
+ .builder-left-shrunk .componets-and-library-accordion {
517
+ min-height: 0 !important;
518
+ padding-right: 0;
519
+ }
520
+ .toolbox-card .builder-layers {
521
+ min-height: 200px;
522
+ overflow-y: auto;
523
+ max-height: max-content !important;
524
+ }
525
+ .toolbox-card:nth-child(2) {
526
+ margin-bottom: 0px;
527
+ }
528
+ #builder-main-canvas {
529
+ height: calc(100dvh - 50px) !important;
530
+ min-height: 600px;
531
+ }
532
+ #builder-main-canvas.h-100 {
533
+ height: 100% !important;
534
+ }
@@ -8,6 +8,23 @@ jQuery.fn.swapWith = function (to) {
8
8
  });
9
9
  };
10
10
 
11
+ function monospace_block_click(e) {
12
+ let e1 = $(e).next("pre");
13
+ let mine = $(e).html();
14
+ $(e).html($(e1).html());
15
+ $(e1).html(mine);
16
+ }
17
+
18
+ function copy_monospace_block(e) {
19
+ let e1 = $(e).next("pre");
20
+ let e2 = $(e1).next("pre");
21
+ if (!e2.length) return navigator.clipboard.writeText($(el).text());
22
+ const e1t = e1.text();
23
+ const e2t = e2.text();
24
+ if (e1t.length > e2t.length) return navigator.clipboard.writeText(e1t);
25
+ else return navigator.clipboard.writeText(e2t);
26
+ }
27
+
11
28
  function setScreenInfoCookie() {
12
29
  document.cookie = `_sc_screen_info_=${JSON.stringify({
13
30
  width: window.screen.width,
@@ -190,7 +207,8 @@ function apply_showif() {
190
207
  else qss.push(`dereference=${dynwhere.dereference}`);
191
208
  }
192
209
  const qs = qss.join("&");
193
- var current = e.attr("data-selected");
210
+ let current = e.attr("data-selected");
211
+ if (current === "null") current = null;
194
212
  e.change(function (ec) {
195
213
  e.attr("data-selected", ec.target.value);
196
214
  });
@@ -199,12 +217,14 @@ function apply_showif() {
199
217
  if (currentOptionsSet === qs) return;
200
218
 
201
219
  const activate = (success, qs) => {
220
+ //re-fetch current, because it may have changed
221
+ let current = e.attr("data-selected");
222
+ if (current === "null") current = null;
202
223
  if (e.prop("data-fetch-options-current-set") === qs) return;
203
224
  e.empty();
204
225
  e.prop("data-fetch-options-current-set", qs);
205
226
  const toAppend = [];
206
- if (!dynwhere.required)
207
- toAppend.push({ label: dynwhere.neutral_label || "", value: "" });
227
+
208
228
  let currentDataOption = undefined;
209
229
  const dataOptions = [];
210
230
  //console.log(success);
@@ -235,13 +255,24 @@ function apply_showif() {
235
255
  ? 1
236
256
  : -1
237
257
  );
258
+ if (!dynwhere.required)
259
+ toAppend.unshift({ label: dynwhere.neutral_label || "", value: "" });
260
+ if (dynwhere.required && dynwhere.placeholder)
261
+ toAppend.unshift({
262
+ disabled: true,
263
+ label: dynwhere.placeholder,
264
+ value: "",
265
+ selected: !current,
266
+ });
238
267
  e.html(
239
268
  toAppend
240
269
  .map(
241
- ({ label, value, selected }) =>
270
+ ({ label, value, selected, disabled }) =>
242
271
  `<option${selected ? ` selected` : ""}${
243
- typeof value !== "undefined" ? ` value="${value}"` : ""
244
- }>${label || ""}</option>`
272
+ disabled ? ` disabled` : ""
273
+ }${typeof value !== "undefined" ? ` value="${value}"` : ""}>${
274
+ label || ""
275
+ }</option>`
245
276
  )
246
277
  .join("")
247
278
  );
@@ -624,6 +655,138 @@ function escapeHtml(text) {
624
655
  function reload_on_init() {
625
656
  localStorage.setItem("reload_on_init", true);
626
657
  }
658
+
659
+ function doMobileTransforms() {
660
+ const replaceAttr = (el, attr, web, mobile) => {
661
+ const jThis = $(el);
662
+ const skip = jThis.attr("skip-mobile-adjust");
663
+ if (!skip) {
664
+ const attrVal = jThis.attr(attr);
665
+ if (attrVal?.includes(web)) {
666
+ jThis.attr(attr, attrVal.replace(web, mobile));
667
+ }
668
+ }
669
+ };
670
+
671
+ const replacers = {
672
+ href: [
673
+ {
674
+ web: "javascript:history.back()",
675
+ mobile: "javascript:parent.goBack()",
676
+ },
677
+ {
678
+ web: "javascript:ajax_modal",
679
+ mobile: "javascript:mobile_modal",
680
+ },
681
+ ],
682
+ onclick: [
683
+ {
684
+ web: "history.back()",
685
+ mobile: "parent.goBack()",
686
+ },
687
+ {
688
+ web: "ajax_modal",
689
+ mobile: "mobile_modal",
690
+ },
691
+ {
692
+ web: "ajax_post_",
693
+ mobile: "local_post_",
694
+ },
695
+ ],
696
+ };
697
+
698
+ $("a").each(function () {
699
+ let path = $(this).attr("href") || "";
700
+ if (path.startsWith("http")) {
701
+ const url = new URL(path);
702
+ path = `${url.pathname}${url.search}`;
703
+ }
704
+ if (path.startsWith("/view/") || path.startsWith("/page/")) {
705
+ const jThis = $(this);
706
+ const skip = jThis.attr("skip-mobile-adjust");
707
+ if (!skip) {
708
+ jThis.removeAttr("href");
709
+ jThis.attr("onclick", `execLink('${path}')`);
710
+ if (jThis.find("i,img").length === 0 && !jThis.css("color")) {
711
+ jThis.css(
712
+ "color",
713
+ "rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1))"
714
+ );
715
+ }
716
+ }
717
+ } else if (path.includes("/files/serve/")) {
718
+ const tokens = path.split("/files/serve/");
719
+ if (tokens.length > 1)
720
+ $(this).attr("href", `javascript:openFile('${tokens[1]}')`);
721
+ } else if (path.includes("/files/download/")) {
722
+ const tokens = path.split("/files/download/");
723
+ if (tokens.length > 1)
724
+ $(this).attr(
725
+ "href",
726
+ `javascript:notifyAlert('File donwloads are not supported.')`
727
+ );
728
+ } else {
729
+ for (const [k, v] of Object.entries(replacers)) {
730
+ for ({ web, mobile } of v) replaceAttr(this, k, web, mobile);
731
+ }
732
+ }
733
+ });
734
+
735
+ $("button").each(function () {
736
+ for (const [k, v] of Object.entries({ onclick: replacers.onclick })) {
737
+ for ({ web, mobile } of v) replaceAttr(this, k, v.web, v.mobile);
738
+ }
739
+ });
740
+
741
+ $("[mobile-img-path]").each(async function () {
742
+ if (parent.loadEncodedFile) {
743
+ const fileId = $(this).attr("mobile-img-path");
744
+ const base64Encoded = await parent.loadEncodedFile(fileId);
745
+ this.src = base64Encoded;
746
+ }
747
+ });
748
+
749
+ $("[mobile-bg-img-path]").each(async function () {
750
+ if (parent.loadEncodedFile) {
751
+ const fileId = $(this).attr("mobile-bg-img-path");
752
+ if (fileId) {
753
+ const base64Encoded = await parent.loadEncodedFile(fileId);
754
+ this.style.backgroundImage = `url("${base64Encoded}")`;
755
+ }
756
+ }
757
+ });
758
+
759
+ $("img:not([mobile-img-path]):not([mobile-bg-img-path])").each(
760
+ async function () {
761
+ if (parent.loadEncodedFile) {
762
+ const jThis = $(this);
763
+ const src = jThis.attr("src");
764
+ if (src?.includes("/files/serve/")) {
765
+ const tokens = src.split("/files/serve/");
766
+ if (tokens.length > 1) {
767
+ const fileId = tokens[1];
768
+ const base64Encoded = await parent.loadEncodedFile(fileId);
769
+ this.src = base64Encoded;
770
+ }
771
+ } else if (src?.includes("/files/resize/")) {
772
+ const tokens = src.split("/files/resize/");
773
+ if (tokens.length > 1) {
774
+ const idAndDims = tokens[1].split("/");
775
+ const width = idAndDims[0];
776
+ const height = idAndDims.length > 2 ? idAndDims[1] : undefined;
777
+ const fileId = idAndDims[idAndDims.length - 1];
778
+ const style = { width: `${width || 50}px` };
779
+ if (height > 0) style.height = `${height}px`;
780
+ const base64Encoded = await parent.loadEncodedFile(fileId);
781
+ this.src = base64Encoded;
782
+ jThis.css(style);
783
+ }
784
+ }
785
+ }
786
+ }
787
+ );
788
+ }
789
+
627
790
  function initialize_page() {
628
791
  if (window._sc_locale && window.dayjs) dayjs.locale(window._sc_locale);
629
792
  const isNode = getIsNode();
@@ -798,58 +961,7 @@ function initialize_page() {
798
961
  </form>`
799
962
  );
800
963
  });
801
- if (!isNode) {
802
- $("[mobile-img-path]").each(async function () {
803
- if (parent.loadEncodedFile) {
804
- const fileId = $(this).attr("mobile-img-path");
805
- const base64Encoded = await parent.loadEncodedFile(fileId);
806
- this.src = base64Encoded;
807
- }
808
- });
809
-
810
- $("[mobile-bg-img-path]").each(async function () {
811
- if (parent.loadEncodedFile) {
812
- const fileId = $(this).attr("mobile-bg-img-path");
813
- if (fileId) {
814
- const base64Encoded = await parent.loadEncodedFile(fileId);
815
- this.style.backgroundImage = `url("${base64Encoded}")`;
816
- }
817
- }
818
- });
819
-
820
- $("a").each(function () {
821
- let path = $(this).attr("href") || "";
822
- if (path.startsWith("http")) {
823
- const url = new URL(path);
824
- path = `${url.pathname}${url.search}`;
825
- }
826
- if (path.startsWith("/view/")) {
827
- const jThis = $(this);
828
- const skip = jThis.attr("skip-mobile-adjust");
829
- if (!skip) {
830
- jThis.attr("href", `javascript:execLink('${path}')`);
831
- if (jThis.find("i,img").length === 0 && !jThis.css("color")) {
832
- jThis.css(
833
- "color",
834
- "rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1))"
835
- );
836
- }
837
- }
838
- }
839
- });
840
-
841
- $("img").each(async function () {
842
- if (parent.loadEncodedFile) {
843
- const jThis = $(this);
844
- const src = jThis.attr("src");
845
- if (src?.startsWith("/files/serve/")) {
846
- const fileId = src.replace("/files/serve/", "");
847
- const base64Encoded = await parent.loadEncodedFile(fileId);
848
- this.src = base64Encoded;
849
- }
850
- }
851
- });
852
- }
964
+ if (!isNode) doMobileTransforms();
853
965
  function setExplainer(that) {
854
966
  var id = $(that).attr("id") + "_explainer";
855
967
 
@@ -1285,10 +1397,12 @@ async function common_done(res, viewnameOrElem, isWeb = true) {
1285
1397
  }
1286
1398
  };
1287
1399
  if (res.notify) await handle(res.notify, notifyAlert);
1288
- if (res.error)
1400
+ if (res.error) {
1401
+ if (window._sc_loglevel > 4) console.trace("error response", res.error);
1289
1402
  await handle(res.error, (text) =>
1290
1403
  notifyAlert({ type: "danger", text: text })
1291
1404
  );
1405
+ }
1292
1406
  if (res.notify_success)
1293
1407
  await handle(res.notify_success, (text) =>
1294
1408
  notifyAlert({ type: "success", text: text })
@@ -1322,6 +1436,10 @@ async function common_done(res, viewnameOrElem, isWeb = true) {
1322
1436
  if (input.attr("type") === "checkbox")
1323
1437
  input.prop("checked", res.set_fields[k]);
1324
1438
  else input.val(res.set_fields[k]);
1439
+ if (input.attr("data-selected")) {
1440
+ input.attr("data-selected", res.set_fields[k]);
1441
+ }
1442
+
1325
1443
  input.trigger("set_form_field");
1326
1444
  });
1327
1445
  }
@@ -1355,15 +1473,15 @@ async function common_done(res, viewnameOrElem, isWeb = true) {
1355
1473
  });
1356
1474
  }
1357
1475
  if (res.eval_js) await handle(res.eval_js, eval_it);
1358
-
1359
- if (res.goto && !isWeb)
1360
- // TODO ch
1361
- notifyAlert({
1362
- type: "danger",
1363
- text: "Goto is not supported in a mobile deployment.",
1364
- });
1365
1476
  else if (res.goto) {
1366
- if (res.target === "_blank") window.open(res.goto, "_blank").focus();
1477
+ if (!isWeb) {
1478
+ const next = new URL(res.goto, "http://localhost");
1479
+ const pathname = next.pathname;
1480
+ if (pathname.startsWith("/view/") || pathname.startsWith("/page/")) {
1481
+ const route = `get${pathname}${next.search ? "?" + next.search : ""}`;
1482
+ await parent.handleRoute(route);
1483
+ } else parent.cordova.InAppBrowser.open(res.goto, "_system");
1484
+ } else if (res.target === "_blank") window.open(res.goto, "_blank").focus();
1367
1485
  else {
1368
1486
  const prev = new URL(window.location.href);
1369
1487
  const next = new URL(res.goto, prev.origin);
@@ -1484,10 +1602,10 @@ const columnSummary = (col) => {
1484
1602
  };
1485
1603
 
1486
1604
  function submitWithEmptyAction(form) {
1487
- var formAction = form.action;
1488
- form.action = "javascript:void(0)";
1605
+ var formAction = form.getAttribute("action");
1606
+ form.setAttribute("action", "javascript:void(0)");
1489
1607
  form.submit();
1490
- form.action = formAction;
1608
+ form.setAttribute("action", formAction);
1491
1609
  }
1492
1610
 
1493
1611
  function unique_field_from_rows(
@@ -1711,7 +1829,13 @@ function close_saltcorn_modal() {
1711
1829
  function reload_embedded_view(viewname, new_query_string) {
1712
1830
  const isNode = getIsNode();
1713
1831
  const updater = ($e, res) => {
1714
- $e.html(res);
1832
+ const localState = $e.attr("data-sc-local-state");
1833
+ const parent = $e.parent();
1834
+ $e.replaceWith(res);
1835
+ if (localState && !new_query_string) {
1836
+ const newE = parent.find(`[data-sc-embed-viewname="${viewname}"]`);
1837
+ newE.attr("data-sc-local-state", localState);
1838
+ }
1715
1839
  initialize_page();
1716
1840
  };
1717
1841
  if (window._sc_loglevel > 4)
@@ -514,3 +514,75 @@ ul.katetree {
514
514
  ul.katetree details ul {
515
515
  list-style-type: none;
516
516
  }
517
+
518
+ pre.monospace-block {
519
+ font-family: monospace;
520
+ color: unset;
521
+ background: unset;
522
+ padding: unset;
523
+ border-radius: unset;
524
+ }
525
+
526
+ .d-none-prefer {
527
+ display: none;
528
+ }
529
+
530
+ button.monospace-copy-btn:has(+ pre.monospace-block:hover) {
531
+ display: block !important ;
532
+ }
533
+ button.monospace-copy-btn:hover {
534
+ display: block !important ;
535
+ }
536
+ button.monospace-copy-btn {
537
+ position: absolute;
538
+ }
539
+
540
+ #page-inner-content.sbadmin2-theme .table.table-card-rows {
541
+ border-spacing: 0px 8px;
542
+ border-collapse: separate;
543
+ }
544
+ #page-inner-content.sbadmin2-theme .table.table-card-rows tbody {
545
+ transform: translateY(8px);
546
+ }
547
+ #page-inner-content.sbadmin2-theme .table.table-card-rows tr {
548
+ border-radius: 6px;
549
+ }
550
+ #page-inner-content.sbadmin2-theme
551
+ .table-hover.table-card-rows
552
+ > tbody
553
+ > tr:hover
554
+ > * {
555
+ --tblr-table-bg-state: transparent !important;
556
+ --bs-table-accent-bg: transparent !important;
557
+ color: var(--bs-table-hover-color);
558
+ background-color: var(--bs-table-hover-bg);
559
+ }
560
+ #page-inner-content.sbadmin2-theme .table.table-card-rows thead th {
561
+ background-color: #eaecf4;
562
+ border-block: 1px solid #d3d3d3 !important;
563
+ padding: 18px 15px;
564
+ font-weight: 600;
565
+ }
566
+ #page-inner-content.sbadmin2-theme .table.table-card-rows thead th:first-child {
567
+ border-left: 1px solid #d3d3d3 !important;
568
+ border-radius: 6px 0 0 6px;
569
+ }
570
+ #page-inner-content.sbadmin2-theme .table.table-card-rows thead th:last-child {
571
+ border-right: 1px #d3d3d3 !important;
572
+ border-radius: 0 6px 6px 0;
573
+ }
574
+ #page-inner-content.sbadmin2-theme .table.table-card-rows tbody td {
575
+ border-block: 1px solid #e3e6f0 !important;
576
+ position: relative;
577
+ z-index: 11;
578
+ padding: 15px 15px;
579
+ background-color: #fff;
580
+ }
581
+ #page-inner-content.sbadmin2-theme .table.table-card-rows tbody td:first-child {
582
+ border-left: 1px solid #e3e6f0 !important;
583
+ border-radius: 6px 0 0 6px;
584
+ }
585
+ #page-inner-content.sbadmin2-theme .table.table-card-rows tbody td:last-child {
586
+ border-right: 1px #e3e6f0 !important;
587
+ border-radius: 0 6px 6px 0;
588
+ }
@@ -1034,6 +1034,84 @@ function execLink(path) {
1034
1034
  window.location.href = `${location.origin}${path}`;
1035
1035
  }
1036
1036
 
1037
+ let defferedPrompt;
1038
+ window.addEventListener("beforeinstallprompt", (e) => {
1039
+ e.preventDefault();
1040
+ defferedPrompt = e;
1041
+ });
1042
+
1043
+ function isAndroidMobile() {
1044
+ const ua = navigator.userAgent || navigator.vendor || window.opera;
1045
+ return /android/i.test(ua) && /mobile/i.test(ua);
1046
+ }
1047
+
1048
+ function validatePWAManifest(manifest) {
1049
+ const errors = [];
1050
+ if (!manifest) errors.push("The manifest.json file is missing. ");
1051
+ else {
1052
+ if (!manifest.icons || manifest.icons.length === 0)
1053
+ errors.push("At least one icon is required");
1054
+ else if (
1055
+ manifest.icons.length > 0 &&
1056
+ !manifest.icons.some((icon) => {
1057
+ const sizes = icon.sizes.split("x");
1058
+ const x = parseInt(sizes[0]);
1059
+ const y = parseInt(sizes[1]);
1060
+ return x === y && x >= 144;
1061
+ })
1062
+ ) {
1063
+ errors.push(
1064
+ "At least one square icon of size 144x144 or larger is required"
1065
+ );
1066
+ }
1067
+ if (isAndroidMobile() && manifest.display === "browser") {
1068
+ errors.push(
1069
+ "The display property 'browser' may not work on mobile devices"
1070
+ );
1071
+ }
1072
+ }
1073
+ return errors;
1074
+ }
1075
+
1076
+ function supportsBeforeInstallPrompt() {
1077
+ return "onbeforeinstallprompt" in window;
1078
+ }
1079
+
1080
+ function installPWA() {
1081
+ if (defferedPrompt) defferedPrompt.prompt();
1082
+ else if (!supportsBeforeInstallPrompt()) {
1083
+ notifyAlert({
1084
+ type: "danger",
1085
+ text:
1086
+ "It looks like your browser doesn’t support this feature. " +
1087
+ "Please try the standard installation method provided by your browser, or switch to a different browser.",
1088
+ });
1089
+ } else {
1090
+ const manifestUrl = `${window.location.origin}/notifications/manifest.json`;
1091
+ notifyAlert({
1092
+ type: "danger",
1093
+ text:
1094
+ "Unable to install the app. " +
1095
+ "Please check if the app is already installed or " +
1096
+ `inspect your manifest.json <a href="${manifestUrl}?pretty=true" target="_blank">here</a>`,
1097
+ });
1098
+ $.ajax(manifestUrl, {
1099
+ success: (res) => {
1100
+ const errors = validatePWAManifest(res);
1101
+ if (errors.length > 0)
1102
+ notifyAlert({
1103
+ type: "warning",
1104
+ text: `${errors.join(", ")}`,
1105
+ });
1106
+ },
1107
+ error: (res) => {
1108
+ console.log("Error fetching manifest.json");
1109
+ console.log(res);
1110
+ },
1111
+ });
1112
+ }
1113
+ }
1114
+
1037
1115
  (() => {
1038
1116
  const e = document.querySelector("[data-sidebar-toggler]");
1039
1117
  let closed = localStorage.getItem("sidebarClosed") === "true";
@@ -23,6 +23,7 @@ const relevantPackages = [
23
23
  "saltcorn-admin-models",
24
24
  "saltcorn-markup",
25
25
  "saltcorn-sbadmin2",
26
+ "saltcorn-types",
26
27
  "server",
27
28
  "sqlite",
28
29
  "filemanager",
package/routes/actions.js CHANGED
@@ -375,6 +375,7 @@ router.get(
375
375
 
376
376
  const form = await triggerForm(req, trigger);
377
377
  form.values = trigger;
378
+ form.onChange = `saveAndContinue(this)`;
378
379
  send_events_page({
379
380
  res,
380
381
  req,
@@ -383,6 +384,7 @@ router.get(
383
384
  contents: {
384
385
  type: "card",
385
386
  title: req.__("Edit trigger %s", id),
387
+ titleAjaxIndicator: true,
386
388
  contents: renderForm(form, req.csrfToken()),
387
389
  },
388
390
  });
@@ -464,6 +466,10 @@ router.post(
464
466
  ...form.values.configuration,
465
467
  };
466
468
  await Trigger.update(trigger.id, form.values); //{configuration: form.values});
469
+ if (req.xhr) {
470
+ res.json({ success: "ok" });
471
+ return;
472
+ }
467
473
  req.flash("success", req.__("Action information saved"));
468
474
  res.redirect(`/actions/`);
469
475
  }
@@ -881,3 +887,24 @@ router.get(
881
887
  }
882
888
  })
883
889
  );
890
+
891
+ /**
892
+ * @name post/clone/:id
893
+ * @function
894
+ * @memberof module:routes/actions~actionsRouter
895
+ * @function
896
+ */
897
+ router.post(
898
+ "/clone/:id",
899
+ isAdmin,
900
+ error_catcher(async (req, res) => {
901
+ const { id } = req.params;
902
+ const trig = await Trigger.findOne({ id });
903
+ const newtrig = await trig.clone();
904
+ req.flash(
905
+ "success",
906
+ req.__("Trigger %s duplicated as %s", trig.name, newtrig.name)
907
+ );
908
+ res.redirect(`/actions`);
909
+ })
910
+ );