@saltcorn/server 0.9.6-beta.2 → 0.9.6-beta.20

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 (49) hide show
  1. package/app.js +6 -1
  2. package/auth/admin.js +55 -53
  3. package/auth/routes.js +28 -10
  4. package/auth/testhelp.js +86 -0
  5. package/help/Field label.tmd +11 -0
  6. package/help/Field types.tmd +39 -0
  7. package/help/Ownership field.tmd +76 -0
  8. package/help/Ownership formula.tmd +75 -0
  9. package/help/Table roles.tmd +20 -0
  10. package/help/User groups.tmd +35 -0
  11. package/load_plugins.js +33 -5
  12. package/locales/en.json +29 -1
  13. package/locales/it.json +3 -2
  14. package/markup/admin.js +1 -0
  15. package/markup/forms.js +5 -1
  16. package/package.json +9 -9
  17. package/public/log_viewer_utils.js +32 -0
  18. package/public/mermaid.min.js +705 -306
  19. package/public/saltcorn-builder.css +23 -0
  20. package/public/saltcorn-common.js +248 -80
  21. package/public/saltcorn.css +80 -0
  22. package/public/saltcorn.js +86 -2
  23. package/restart_watcher.js +1 -0
  24. package/routes/actions.js +27 -0
  25. package/routes/admin.js +175 -64
  26. package/routes/api.js +6 -0
  27. package/routes/common_lists.js +42 -32
  28. package/routes/fields.js +70 -42
  29. package/routes/homepage.js +2 -0
  30. package/routes/index.js +2 -0
  31. package/routes/menu.js +69 -4
  32. package/routes/notifications.js +90 -10
  33. package/routes/pageedit.js +18 -13
  34. package/routes/plugins.js +11 -2
  35. package/routes/registry.js +289 -0
  36. package/routes/search.js +10 -4
  37. package/routes/tables.js +51 -27
  38. package/routes/tenant.js +4 -15
  39. package/routes/utils.js +25 -8
  40. package/routes/view.js +1 -1
  41. package/routes/viewedit.js +11 -7
  42. package/serve.js +27 -5
  43. package/tests/edit.test.js +426 -0
  44. package/tests/fields.test.js +21 -0
  45. package/tests/filter.test.js +68 -0
  46. package/tests/page.test.js +2 -2
  47. package/tests/plugins.test.js +2 -0
  48. package/tests/sync.test.js +59 -0
  49. 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,
@@ -179,14 +196,19 @@ function apply_showif() {
179
196
  is_or ? "&_or_field=" + k : ""
180
197
  }`;
181
198
  };
182
- const qss = Object.entries(dynwhere.whereParsed).map(kvToQs);
199
+ const qss = Object.entries(dynwhere.whereParsed).map((kv) => kvToQs(kv));
200
+ if (dynwhere.existingValue) {
201
+ qss.push(`id=${dynwhere.existingValue}`);
202
+ qss.push(`_or_field=id`);
203
+ }
183
204
  if (dynwhere.dereference) {
184
205
  if (Array.isArray(dynwhere.dereference))
185
206
  qss.push(...dynwhere.dereference.map((d) => `dereference=${d}`));
186
207
  else qss.push(`dereference=${dynwhere.dereference}`);
187
208
  }
188
209
  const qs = qss.join("&");
189
- var current = e.attr("data-selected");
210
+ let current = e.attr("data-selected");
211
+ if (current === "null") current = null;
190
212
  e.change(function (ec) {
191
213
  e.attr("data-selected", ec.target.value);
192
214
  });
@@ -195,12 +217,14 @@ function apply_showif() {
195
217
  if (currentOptionsSet === qs) return;
196
218
 
197
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;
198
223
  if (e.prop("data-fetch-options-current-set") === qs) return;
199
224
  e.empty();
200
225
  e.prop("data-fetch-options-current-set", qs);
201
226
  const toAppend = [];
202
- if (!dynwhere.required)
203
- toAppend.push({ label: dynwhere.neutral_label || "", value: "" });
227
+
204
228
  let currentDataOption = undefined;
205
229
  const dataOptions = [];
206
230
  //console.log(success);
@@ -231,13 +255,24 @@ function apply_showif() {
231
255
  ? 1
232
256
  : -1
233
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
+ });
234
267
  e.html(
235
268
  toAppend
236
269
  .map(
237
- ({ label, value, selected }) =>
270
+ ({ label, value, selected, disabled }) =>
238
271
  `<option${selected ? ` selected` : ""}${
239
- typeof value !== "undefined" ? ` value="${value}"` : ""
240
- }>${label || ""}</option>`
272
+ disabled ? ` disabled` : ""
273
+ }${typeof value !== "undefined" ? ` value="${value}"` : ""}>${
274
+ label || ""
275
+ }</option>`
241
276
  )
242
277
  .join("")
243
278
  );
@@ -428,7 +463,7 @@ function get_form_record(e_in, select_labels) {
428
463
 
429
464
  const e = e_in.viewname
430
465
  ? $(`form[data-viewname="${e_in.viewname}"]`)
431
- : e_in.closest(".form-namespace");
466
+ : $(e_in).closest(".form-namespace");
432
467
 
433
468
  const form = $(e).closest("form");
434
469
 
@@ -462,6 +497,38 @@ function get_form_record(e_in, select_labels) {
462
497
  rec[name] = f(rec[name], $this);
463
498
  }
464
499
  });
500
+
501
+ const joinFieldsStr =
502
+ typeof e_in !== "string" && $(e_in).attr("data-show-if-joinfields");
503
+ if (joinFieldsStr) {
504
+ const joinFields = JSON.parse(decodeURIComponent(joinFieldsStr));
505
+
506
+ const joinVals = $(e_in).prop("data-join-values");
507
+ const kvals = $(e_in).prop("data-join-key-values") || {};
508
+ let differentKeys = false;
509
+ for (const { ref } of joinFields) {
510
+ if (rec[ref] != kvals[ref]) differentKeys = true;
511
+ }
512
+ if (!joinVals || differentKeys) {
513
+ $(e_in).prop("data-join-values", {});
514
+ const keyVals = {};
515
+ for (const { ref, target, refTable } of joinFields) {
516
+ keyVals[ref] = rec[ref];
517
+ $.ajax(`/api/${refTable}?id=${rec[ref]}`, {
518
+ success: (val) => {
519
+ const jvs = $(e_in).prop("data-join-values") || {};
520
+
521
+ jvs[ref] = val.success[0];
522
+ $(e_in).prop("data-join-values", jvs);
523
+ apply_showif();
524
+ },
525
+ });
526
+ }
527
+ $(e_in).prop("data-join-key-values", keyVals);
528
+ } else if (joinFieldsStr) {
529
+ Object.assign(rec, joinVals);
530
+ }
531
+ }
465
532
  return rec;
466
533
  }
467
534
  function showIfFormulaInputs(e, fml) {
@@ -588,6 +655,138 @@ function escapeHtml(text) {
588
655
  function reload_on_init() {
589
656
  localStorage.setItem("reload_on_init", true);
590
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
+
591
790
  function initialize_page() {
592
791
  if (window._sc_locale && window.dayjs) dayjs.locale(window._sc_locale);
593
792
  const isNode = getIsNode();
@@ -762,58 +961,7 @@ function initialize_page() {
762
961
  </form>`
763
962
  );
764
963
  });
765
- if (!isNode) {
766
- $("[mobile-img-path]").each(async function () {
767
- if (parent.loadEncodedFile) {
768
- const fileId = $(this).attr("mobile-img-path");
769
- const base64Encoded = await parent.loadEncodedFile(fileId);
770
- this.src = base64Encoded;
771
- }
772
- });
773
-
774
- $("[mobile-bg-img-path]").each(async function () {
775
- if (parent.loadEncodedFile) {
776
- const fileId = $(this).attr("mobile-bg-img-path");
777
- if (fileId) {
778
- const base64Encoded = await parent.loadEncodedFile(fileId);
779
- this.style.backgroundImage = `url("${base64Encoded}")`;
780
- }
781
- }
782
- });
783
-
784
- $("a").each(function () {
785
- let path = $(this).attr("href") || "";
786
- if (path.startsWith("http")) {
787
- const url = new URL(path);
788
- path = `${url.pathname}${url.search}`;
789
- }
790
- if (path.startsWith("/view/")) {
791
- const jThis = $(this);
792
- const skip = jThis.attr("skip-mobile-adjust");
793
- if (!skip) {
794
- jThis.attr("href", `javascript:execLink('${path}')`);
795
- if (jThis.find("i,img").length === 0 && !jThis.css("color")) {
796
- jThis.css(
797
- "color",
798
- "rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1))"
799
- );
800
- }
801
- }
802
- }
803
- });
804
-
805
- $("img").each(async function () {
806
- if (parent.loadEncodedFile) {
807
- const jThis = $(this);
808
- const src = jThis.attr("src");
809
- if (src?.startsWith("/files/serve/")) {
810
- const fileId = src.replace("/files/serve/", "");
811
- const base64Encoded = await parent.loadEncodedFile(fileId);
812
- this.src = base64Encoded;
813
- }
814
- }
815
- });
816
- }
964
+ if (!isNode) doMobileTransforms();
817
965
  function setExplainer(that) {
818
966
  var id = $(that).attr("id") + "_explainer";
819
967
 
@@ -1219,7 +1367,13 @@ function restore_old_button(btnId) {
1219
1367
  btn.removeData("old-text");
1220
1368
  }
1221
1369
 
1222
- async function common_done(res, viewname, isWeb = true) {
1370
+ async function common_done(res, viewnameOrElem, isWeb = true) {
1371
+ const viewname =
1372
+ typeof viewnameOrElem === "string"
1373
+ ? viewnameOrElem
1374
+ : $(viewnameOrElem)
1375
+ .closest("[data-sc-embed-viewname]")
1376
+ .attr("data-sc-embed-viewname");
1223
1377
  if (window._sc_loglevel > 4)
1224
1378
  console.log("ajax result directives", viewname, res);
1225
1379
  const handle = async (element, fn) => {
@@ -1231,30 +1385,33 @@ async function common_done(res, viewname, isWeb = true) {
1231
1385
  if (res.row && res.field_names) {
1232
1386
  const f = new Function(`viewname, row, {${res.field_names}}`, s);
1233
1387
  const evalres = await f(viewname, res.row, res.row);
1234
- if (evalres) await common_done(evalres, viewname, isWeb);
1388
+ if (evalres) await common_done(evalres, viewnameOrElem, isWeb);
1235
1389
  } else if (res.row) {
1236
1390
  const f = new Function(`viewname, row`, s);
1237
1391
  const evalres = await f(viewname, res.row);
1238
- if (evalres) await common_done(evalres, viewname, isWeb);
1392
+ if (evalres) await common_done(evalres, viewnameOrElem, isWeb);
1239
1393
  } else {
1240
1394
  const f = new Function(`viewname`, s);
1241
1395
  const evalres = await f(viewname);
1242
- if (evalres) await common_done(evalres, viewname, isWeb);
1396
+ if (evalres) await common_done(evalres, viewnameOrElem, isWeb);
1243
1397
  }
1244
1398
  };
1245
1399
  if (res.notify) await handle(res.notify, notifyAlert);
1246
- if (res.error)
1400
+ if (res.error) {
1401
+ if (window._sc_loglevel > 4) console.trace("error response", res.error);
1247
1402
  await handle(res.error, (text) =>
1248
1403
  notifyAlert({ type: "danger", text: text })
1249
1404
  );
1405
+ }
1250
1406
  if (res.notify_success)
1251
1407
  await handle(res.notify_success, (text) =>
1252
1408
  notifyAlert({ type: "success", text: text })
1253
1409
  );
1254
1410
  if (res.set_fields && (viewname || res.set_fields._viewname)) {
1255
- const form = $(
1256
- `form[data-viewname="${res.set_fields._viewname || viewname}"]`
1257
- );
1411
+ const form =
1412
+ typeof viewnameOrElem === "string" || res.set_fields._viewname
1413
+ ? $(`form[data-viewname="${res.set_fields._viewname || viewname}"]`)
1414
+ : $(viewnameOrElem).closest("form[data-viewname]");
1258
1415
  if (form.length === 0 && set_state_fields) {
1259
1416
  // assume this is a filter
1260
1417
  set_state_fields(
@@ -1279,9 +1436,14 @@ async function common_done(res, viewname, isWeb = true) {
1279
1436
  if (input.attr("type") === "checkbox")
1280
1437
  input.prop("checked", res.set_fields[k]);
1281
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
+
1282
1443
  input.trigger("set_form_field");
1283
1444
  });
1284
1445
  }
1446
+ form.trigger("change");
1285
1447
  }
1286
1448
 
1287
1449
  if (res.download) {
@@ -1311,15 +1473,15 @@ async function common_done(res, viewname, isWeb = true) {
1311
1473
  });
1312
1474
  }
1313
1475
  if (res.eval_js) await handle(res.eval_js, eval_it);
1314
-
1315
- if (res.goto && !isWeb)
1316
- // TODO ch
1317
- notifyAlert({
1318
- type: "danger",
1319
- text: "Goto is not supported in a mobile deployment.",
1320
- });
1321
1476
  else if (res.goto) {
1322
- 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();
1323
1485
  else {
1324
1486
  const prev = new URL(window.location.href);
1325
1487
  const next = new URL(res.goto, prev.origin);
@@ -1440,10 +1602,10 @@ const columnSummary = (col) => {
1440
1602
  };
1441
1603
 
1442
1604
  function submitWithEmptyAction(form) {
1443
- var formAction = form.action;
1444
- form.action = "javascript:void(0)";
1605
+ var formAction = form.getAttribute("action");
1606
+ form.setAttribute("action", "javascript:void(0)");
1445
1607
  form.submit();
1446
- form.action = formAction;
1608
+ form.setAttribute("action", formAction);
1447
1609
  }
1448
1610
 
1449
1611
  function unique_field_from_rows(
@@ -1667,7 +1829,13 @@ function close_saltcorn_modal() {
1667
1829
  function reload_embedded_view(viewname, new_query_string) {
1668
1830
  const isNode = getIsNode();
1669
1831
  const updater = ($e, res) => {
1670
- $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
+ }
1671
1839
  initialize_page();
1672
1840
  };
1673
1841
  if (window._sc_loglevel > 4)
@@ -506,3 +506,83 @@ tr[onclick] {
506
506
  .modal-header {
507
507
  justify-content: space-between;
508
508
  }
509
+
510
+ ul.katetree {
511
+ list-style-type: none;
512
+ }
513
+
514
+ ul.katetree details ul {
515
+ list-style-type: none;
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
+ }
@@ -236,7 +236,13 @@ function reset_spinners() {
236
236
  });
237
237
  }
238
238
 
239
- function view_post(viewname, route, data, onDone, sendState) {
239
+ function view_post(viewnameOrElem, route, data, onDone, sendState) {
240
+ const viewname =
241
+ typeof viewnameOrElem === "string"
242
+ ? viewnameOrElem
243
+ : $(viewnameOrElem)
244
+ .closest("[data-sc-embed-viewname]")
245
+ .attr("data-sc-embed-viewname");
240
246
  const query = sendState
241
247
  ? `?${new URL(get_current_state_url()).searchParams.toString()}`
242
248
  : "";
@@ -254,7 +260,7 @@ function view_post(viewname, route, data, onDone, sendState) {
254
260
  })
255
261
  .done(function (res) {
256
262
  if (onDone) onDone(res);
257
- ajax_done(res, viewname);
263
+ ajax_done(res, viewnameOrElem);
258
264
  reset_spinners();
259
265
  })
260
266
  .fail(function (res) {
@@ -1028,6 +1034,84 @@ function execLink(path) {
1028
1034
  window.location.href = `${location.origin}${path}`;
1029
1035
  }
1030
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
+
1031
1115
  (() => {
1032
1116
  const e = document.querySelector("[data-sidebar-toggler]");
1033
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",