@saltcorn/server 0.8.1-beta.5 → 0.8.1-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auth/admin.js CHANGED
@@ -336,6 +336,7 @@ const http_settings_form = async (req) =>
336
336
  "cookie_duration",
337
337
  "cookie_duration_remember",
338
338
  "cookie_sessions",
339
+ "public_cache_maxage",
339
340
  "custom_http_headers",
340
341
  ],
341
342
  action: "/useradmin/http",
@@ -376,6 +377,7 @@ router.get(
376
377
  active_sub: "Login and Signup",
377
378
  contents: {
378
379
  type: "card",
380
+ titleAjaxIndicator: true,
379
381
  title: req.__("Authentication settings"),
380
382
  contents: [renderForm(form, req.csrfToken())],
381
383
  },
@@ -408,9 +410,10 @@ router.post(
408
410
  });
409
411
  } else {
410
412
  await save_config_from_form(form);
411
- req.flash("success", req.__("Authentication settings updated"));
412
- if (!req.xhr) res.redirect("/useradmin/settings");
413
- else res.json({ success: "ok" });
413
+ if (!req.xhr) {
414
+ req.flash("success", req.__("Authentication settings updated"));
415
+ res.redirect("/useradmin/settings");
416
+ } else res.json({ success: "ok" });
414
417
  }
415
418
  })
416
419
  );
@@ -432,6 +435,7 @@ router.get(
432
435
  active_sub: "HTTP",
433
436
  contents: {
434
437
  type: "card",
438
+ titleAjaxIndicator: true,
435
439
  title: req.__("HTTP settings"),
436
440
  contents: [renderForm(form, req.csrfToken())],
437
441
  },
@@ -464,9 +468,11 @@ router.post(
464
468
  });
465
469
  } else {
466
470
  await save_config_from_form(form);
467
- req.flash("success", req.__("HTTP settings updated"));
468
- if (!req.xhr) res.redirect("/useradmin/http");
469
- else res.json({ success: "ok" });
471
+
472
+ if (!req.xhr) {
473
+ req.flash("success", req.__("HTTP settings updated"));
474
+ res.redirect("/useradmin/http");
475
+ } else res.json({ success: "ok" });
470
476
  }
471
477
  })
472
478
  );
@@ -488,6 +494,7 @@ router.get(
488
494
  active_sub: "Permissions",
489
495
  contents: {
490
496
  type: "card",
497
+ titleAjaxIndicator: true,
491
498
  title: req.__("Permissions settings"),
492
499
  contents: [renderForm(form, req.csrfToken())],
493
500
  },
@@ -514,15 +521,17 @@ router.post(
514
521
  active_sub: "Permissions",
515
522
  contents: {
516
523
  type: "card",
524
+ titleAjaxIndicator: true,
517
525
  title: req.__("Permissions settings"),
518
526
  contents: [renderForm(form, req.csrfToken())],
519
527
  },
520
528
  });
521
529
  } else {
522
530
  await save_config_from_form(form);
523
- req.flash("success", req.__("Permissions settings updated"));
524
- if (!req.xhr) res.redirect("/useradmin/permissions");
525
- else res.json({ success: "ok" });
531
+ if (!req.xhr) {
532
+ req.flash("success", req.__("Permissions settings updated"));
533
+ res.redirect("/useradmin/permissions");
534
+ } else res.json({ success: "ok" });
526
535
  }
527
536
  })
528
537
  );
@@ -677,8 +686,9 @@ router.get(
677
686
  active_sub: "SSL",
678
687
  contents: {
679
688
  type: "card",
680
- title: req.__("Authentication settings"),
689
+ title: req.__("Custom SSL certificates"),
681
690
  sub2_page: req.__("Custom SSL certificates"),
691
+ titleAjaxIndicator: true,
682
692
  contents: [renderForm(form, req.csrfToken())],
683
693
  },
684
694
  });
@@ -817,6 +827,7 @@ router.get(
817
827
  contents: {
818
828
  type: "card",
819
829
  title: req.__("Table access"),
830
+ titleAjaxIndicator: true,
820
831
  contents,
821
832
  },
822
833
  });
package/auth/routes.js CHANGED
@@ -1061,6 +1061,7 @@ router.post(
1061
1061
  else req.session.cookie.expires = false;
1062
1062
  }
1063
1063
  Trigger.emitEvent("Login", null, req.user);
1064
+ res?.cookie?.("loggedin", "true");
1064
1065
  req.flash("success", req.__("Welcome, %s!", req.user.email));
1065
1066
  if (req.smr) {
1066
1067
  const dbUser = await User.findOne({ id: req.user.id });
package/auth/testhelp.js CHANGED
@@ -88,6 +88,9 @@ const toNotInclude =
88
88
  }
89
89
  };
90
90
 
91
+ const resToLoginCookie = (res) =>
92
+ res.headers["set-cookie"].find((c) => c.includes("connect.sid"));
93
+
91
94
  /**
92
95
  *
93
96
  * @returns {Promise<void>}
@@ -99,7 +102,7 @@ const getStaffLoginCookie = async () => {
99
102
  .send("email=staff@foo.com")
100
103
  .send("password=ghrarhr54hg");
101
104
  if (res.statusCode !== 302) console.log(res.text);
102
- return res.headers["set-cookie"][0];
105
+ return resToLoginCookie(res);
103
106
  };
104
107
 
105
108
  /**
@@ -113,8 +116,7 @@ const getAdminLoginCookie = async () => {
113
116
  .send("email=admin@foo.com")
114
117
  .send("password=AhGGr6rhu45");
115
118
  if (res.statusCode !== 302) console.log(res.text);
116
-
117
- return res.headers["set-cookie"][0];
119
+ return resToLoginCookie(res);
118
120
  };
119
121
 
120
122
  /**
package/locales/da.json CHANGED
@@ -555,5 +555,8 @@
555
555
  "Create database view": "Create database view",
556
556
  "Create an SQL view in the database with the fields in this list": "Create an SQL view in the database with the fields in this list",
557
557
  "Rows per page": "Rows per page",
558
- "List options": "List options"
558
+ "List options": "List options",
559
+ "Modules": "Modules",
560
+ "File not found": "File not found",
561
+ "Welcome, %s!": "Welcome, %s!"
559
562
  }
package/locales/en.json CHANGED
@@ -1073,5 +1073,12 @@
1073
1073
  "Become user": "Become user",
1074
1074
  "Your are now logged in as %s. Logout and login again to assume your usual identity": "Your are now logged in as %s. Logout and login again to assume your usual identity",
1075
1075
  "Done": "Done",
1076
- "Configure trigger %s": "Configure trigger %s"
1076
+ "Configure trigger %s": "Configure trigger %s",
1077
+ "Saved 2FA policy for role": "Saved 2FA policy for role",
1078
+ "HTTP settings updated": "HTTP settings updated",
1079
+ "%s configuration": "%s configuration",
1080
+ "Save indicator": "Save indicator",
1081
+ "Public cache TTL (minutes)": "Public cache TTL (minutes)",
1082
+ "Cache-control max-age for public views and pages. 0 to disable": "Cache-control max-age for public views and pages. 0 to disable",
1083
+ "Files accept filter": "Files accept filter"
1077
1084
  }
package/markup/admin.js CHANGED
@@ -135,6 +135,7 @@ const send_settings_page = ({
135
135
  headers,
136
136
  no_nav_pills,
137
137
  sub2_page,
138
+ page_title,
138
139
  }) => {
139
140
  const pillCard = no_nav_pills
140
141
  ? []
@@ -163,12 +164,13 @@ const send_settings_page = ({
163
164
  },
164
165
  ];
165
166
  // headers
167
+ const pg_title = page_title || req.__(active_sub);
166
168
  const title = headers
167
169
  ? {
168
- title: req.__(active_sub),
170
+ title: pg_title,
169
171
  headers,
170
172
  }
171
- : req.__(active_sub);
173
+ : pg_title;
172
174
  res.sendWrap(title, {
173
175
  above: [
174
176
  {
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.1-beta.5",
3
+ "version": "0.8.1-rc.3",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@saltcorn/base-plugin": "0.8.1-beta.5",
10
- "@saltcorn/builder": "0.8.1-beta.5",
11
- "@saltcorn/data": "0.8.1-beta.5",
12
- "@saltcorn/admin-models": "0.8.1-beta.5",
13
- "@saltcorn/filemanager": "0.8.1-beta.5",
14
- "@saltcorn/markup": "0.8.1-beta.5",
15
- "@saltcorn/sbadmin2": "0.8.1-beta.5",
9
+ "@saltcorn/base-plugin": "0.8.1-rc.3",
10
+ "@saltcorn/builder": "0.8.1-rc.3",
11
+ "@saltcorn/data": "0.8.1-rc.3",
12
+ "@saltcorn/admin-models": "0.8.1-rc.3",
13
+ "@saltcorn/filemanager": "0.8.1-rc.3",
14
+ "@saltcorn/markup": "0.8.1-rc.3",
15
+ "@saltcorn/sbadmin2": "0.8.1-rc.3",
16
16
  "@socket.io/cluster-adapter": "^0.1.0",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "aws-sdk": "^2.1037.0",
@@ -523,6 +523,33 @@ function initialize_page() {
523
523
 
524
524
  $(initialize_page);
525
525
 
526
+ function ajax_indicator(show, e) {
527
+ const $ind = e
528
+ ? $(e).closest(".card,.modal").find(".sc-ajax-indicator")
529
+ : $(".sc-ajax-indicator");
530
+ $ind.find("svg").attr("data-icon", "save");
531
+ $ind.find("i").removeClass("fa-exclamation-triangle").addClass("fa-save");
532
+ $ind.css("color", "");
533
+ $ind.removeAttr("title");
534
+ if (show) $ind.show();
535
+ else $ind.fadeOut();
536
+ }
537
+
538
+ function ajax_indicate_error(e, resp) {
539
+ //console.error("ind error", resp);
540
+ const $ind = e
541
+ ? $(e).closest(".card,.modal").find(".sc-ajax-indicator")
542
+ : $(".sc-ajax-indicator");
543
+ $ind.css("color", "#e74a3b");
544
+ $ind.find("svg").attr("data-icon", "exclamation-triangle");
545
+ $ind.find("i").removeClass("fa-save").addClass("fa-exclamation-triangle");
546
+ $ind.attr(
547
+ "title",
548
+ "Save error: " + (resp ? resp.responseText || resp.statusText : "unknown")
549
+ );
550
+ $ind.show();
551
+ }
552
+
526
553
  function enable_codemirror(f) {
527
554
  $("<link/>", {
528
555
  rel: "stylesheet",
@@ -246,6 +246,9 @@ function ensure_modal_exists_and_closed() {
246
246
  <div class="modal-content">
247
247
  <div class="modal-header">
248
248
  <h5 class="modal-title">Modal title</h5>
249
+ <span class="sc-ajax-indicator-wrapper">
250
+ <span class="sc-ajax-indicator ms-2" style="display: none;"><i class="fas fa-save"></i></span>
251
+ </span>
249
252
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
250
253
  </button>
251
254
  </div>
@@ -280,6 +283,11 @@ function ajax_modal(url, opts = {}) {
280
283
  success: function (res, textStatus, request) {
281
284
  var title = request.getResponseHeader("Page-Title");
282
285
  var width = request.getResponseHeader("SaltcornModalWidth");
286
+ var saveIndicate = !!request.getResponseHeader(
287
+ "SaltcornModalSaveIndicator"
288
+ );
289
+ if (saveIndicate) $(".sc-ajax-indicator-wrapper").show();
290
+ else $(".sc-ajax-indicator-wrapper").hide();
283
291
  if (width) $(".modal-dialog").css("max-width", width);
284
292
  else $(".modal-dialog").css("max-width", "");
285
293
  if (title) $("#scmodal .modal-title").html(decodeURIComponent(title));
@@ -303,6 +311,7 @@ function saveAndContinue(e, k) {
303
311
  submitWithEmptyAction(form[0]);
304
312
  var url = form.attr("action");
305
313
  var form_data = form.serialize();
314
+ ajax_indicator(true, e);
306
315
  $.ajax(url, {
307
316
  type: "POST",
308
317
  headers: {
@@ -310,6 +319,7 @@ function saveAndContinue(e, k) {
310
319
  },
311
320
  data: form_data,
312
321
  success: function (res) {
322
+ ajax_indicator(false);
313
323
  if (res.id && form.find("input[name=id")) {
314
324
  form.append(
315
325
  `<input type="hidden" class="form-control " name="id" value="${res.id}">`
@@ -318,6 +328,7 @@ function saveAndContinue(e, k) {
318
328
  },
319
329
  error: function (request) {
320
330
  $("#page-inner-content").html(request.responseText);
331
+ ajax_indicate_error(e, request);
321
332
  initialize_page();
322
333
  },
323
334
  complete: function () {
@@ -335,6 +346,7 @@ function applyViewConfig(e, url, k) {
335
346
  form_data.forEach((item) => {
336
347
  cfg[item.name] = item.value;
337
348
  });
349
+ ajax_indicator(true, e);
338
350
  $.ajax(url, {
339
351
  type: "POST",
340
352
  dataType: "json",
@@ -343,11 +355,15 @@ function applyViewConfig(e, url, k) {
343
355
  "CSRF-Token": _sc_globalCsrf,
344
356
  },
345
357
  data: JSON.stringify(cfg),
346
- error: function (request) {},
358
+ error: function (request) {
359
+ ajax_indicate_error(e, request);
360
+ },
347
361
  success: function (res) {
362
+ ajax_indicator(false);
348
363
  k && k(res);
349
364
  !k && updateViewPreview();
350
365
  },
366
+ complete: () => {},
351
367
  });
352
368
 
353
369
  return false;
package/routes/actions.js CHANGED
@@ -399,8 +399,10 @@ router.get(
399
399
  req,
400
400
  active_sub: "Triggers",
401
401
  sub2_page: "Configure",
402
+ page_title: trigger.name,
402
403
  contents: {
403
404
  type: "card",
405
+ titleAjaxIndicator: true,
404
406
  title: req.__("Configure trigger %s", trigger.name),
405
407
  contents: {
406
408
  widths: [8, 4],
@@ -468,8 +470,10 @@ router.get(
468
470
  req,
469
471
  active_sub: "Triggers",
470
472
  sub2_page: "Configure",
473
+ page_title: req.__(`%s configuration`, trigger.name),
471
474
  contents: {
472
475
  type: "card",
476
+ titleAjaxIndicator: true,
473
477
  title: req.__("Configure trigger %s", trigger.name),
474
478
  contents: renderForm(form, req.csrfToken()),
475
479
  },
package/routes/admin.js CHANGED
@@ -97,6 +97,7 @@ const View = require("@saltcorn/data/models/view");
97
97
  const { getConfigFile } = require("@saltcorn/data/db/connect");
98
98
  const os = require("os");
99
99
  const Page = require("@saltcorn/data/models/page");
100
+ const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
100
101
 
101
102
  const router = new Router();
102
103
  module.exports = router;
@@ -194,6 +195,7 @@ router.get(
194
195
  contents: {
195
196
  type: "card",
196
197
  title: req.__("Site identity settings"),
198
+ titleAjaxIndicator: true,
197
199
  contents: [renderForm(form, req.csrfToken())],
198
200
  },
199
201
  });
@@ -251,6 +253,7 @@ router.get(
251
253
  contents: {
252
254
  type: "card",
253
255
  title: req.__("Email settings"),
256
+ titleAjaxIndicator: true,
254
257
  contents: [
255
258
  renderForm(form, req.csrfToken()),
256
259
  a(
@@ -321,9 +324,10 @@ router.post(
321
324
  });
322
325
  } else {
323
326
  await save_config_from_form(form);
324
- req.flash("success", req.__("Email settings updated"));
325
- if (!req.xhr) res.redirect("/admin/email");
326
- else res.json({ success: "ok" });
327
+ if (!req.xhr) {
328
+ req.flash("success", req.__("Email settings updated"));
329
+ res.redirect("/admin/email");
330
+ } else res.json({ success: "ok" });
327
331
  }
328
332
  })
329
333
  );
@@ -391,6 +395,7 @@ router.get(
391
395
  ? {
392
396
  type: "card",
393
397
  title: req.__("Automated backup"),
398
+ titleAjaxIndicator: true,
394
399
  contents: div(
395
400
  renderForm(backupForm, req.csrfToken()),
396
401
  a(
@@ -408,6 +413,7 @@ router.get(
408
413
  {
409
414
  type: "card",
410
415
  title: req.__("Snapshots"),
416
+ titleAjaxIndicator: true,
411
417
  contents: div(
412
418
  p(
413
419
  i(
@@ -708,9 +714,11 @@ router.post(
708
714
  form.validate(req.body);
709
715
 
710
716
  await save_config_from_form(form);
711
- req.flash("success", req.__("Snapshot settings updated"));
712
- if (!req.xhr) res.redirect("/admin/backup");
713
- else res.json({ success: "ok" });
717
+
718
+ if (!req.xhr) {
719
+ req.flash("success", req.__("Snapshot settings updated"));
720
+ res.redirect("/admin/backup");
721
+ } else res.json({ success: "ok" });
714
722
  })
715
723
  );
716
724
  router.post(
@@ -732,9 +740,10 @@ router.post(
732
740
  });
733
741
  } else {
734
742
  await save_config_from_form(form);
735
- req.flash("success", req.__("Backup settings updated"));
736
- if (!req.xhr) res.redirect("/admin/backup");
737
- else res.json({ success: "ok" });
743
+ if (!req.xhr) {
744
+ req.flash("success", req.__("Backup settings updated"));
745
+ res.redirect("/admin/backup");
746
+ } else res.json({ success: "ok" });
738
747
  }
739
748
  })
740
749
  );
@@ -1306,7 +1315,7 @@ const buildDialogScript = () => {
1306
1315
  }
1307
1316
 
1308
1317
  function handleMessages() {
1309
- notifyAlert("This is still under development and might run longer.")
1318
+ notifyAlert("Building the app, please wait.")
1310
1319
  ${
1311
1320
  getState().getConfig("apple_team_id") &&
1312
1321
  getState().getConfig("apple_team_id") !== "null"
@@ -1651,7 +1660,7 @@ router.post(
1651
1660
  // end http call, return the out directory name
1652
1661
  // the gui polls for results
1653
1662
  res.json({ build_dir_name: outDirName });
1654
- const child = spawn("saltcorn", spawnParams, {
1663
+ const child = spawn(getSafeSaltcornCmd(), spawnParams, {
1655
1664
  stdio: ["ignore", "pipe", "pipe"],
1656
1665
  cwd: ".",
1657
1666
  });
@@ -1858,6 +1867,7 @@ router.get(
1858
1867
  contents: {
1859
1868
  type: "card",
1860
1869
  title: req.__("Development settings"),
1870
+ titleAjaxIndicator: true,
1861
1871
  contents: [
1862
1872
  renderForm(form, req.csrfToken()) /*,
1863
1873
  a(
@@ -1899,9 +1909,10 @@ router.post(
1899
1909
  });
1900
1910
  } else {
1901
1911
  await save_config_from_form(form);
1902
- req.flash("success", req.__("Development mode settings updated"));
1903
- if (!req.xhr) res.redirect("/admin/dev");
1904
- else res.json({ success: "ok" });
1912
+ if (!req.xhr) {
1913
+ req.flash("success", req.__("Development mode settings updated"));
1914
+ res.redirect("/admin/dev");
1915
+ } else res.json({ success: "ok" });
1905
1916
  }
1906
1917
  })
1907
1918
  );
@@ -108,6 +108,7 @@ router.get(
108
108
  //sub2_page: "Events to log",
109
109
  contents: {
110
110
  type: "card",
111
+ titleAjaxIndicator: true,
111
112
  title: req.__("Events to log"),
112
113
  contents: renderForm(form, req.csrfToken()),
113
114
  },
package/routes/fields.js CHANGED
@@ -711,20 +711,54 @@ router.post(
711
711
  const fields = await table.getFields();
712
712
  let row = { ...req.body };
713
713
  if (row && Object.keys(row).length > 0) readState(row, fields);
714
+
715
+ //need to get join fields from ownership into row
716
+ const joinFields = {};
717
+ if (table.ownership_formula && role > table.min_role_read) {
718
+ const freeVars = freeVariables(table.ownership_formula);
719
+ add_free_variables_to_joinfields(freeVars, joinFields, fields);
720
+ }
721
+ //console.log(joinFields, row);
714
722
  const id = req.query.id || row.id;
715
723
  if (id) {
716
- let dbrow = await table.getRow({ id });
724
+ let [dbrow] = await table.getJoinedRows({ where: { id }, joinFields });
717
725
  row = { ...dbrow, ...row };
718
726
  //prevent overwriting ownership field
719
727
  if (table.ownership_field_id) {
720
728
  const ofield = fields.find((f) => f.id === table.ownership_field_id);
721
729
  row[ofield.name] = dbrow[ofield.name];
722
730
  }
731
+ } else {
732
+ //may need to add joinfields
733
+ for (const { ref } of Object.values(joinFields)) {
734
+ if (row[ref]) {
735
+ const field = fields.find((f) => f.name === ref);
736
+ const reftable = await Table.findOne({ name: field.reftable_name });
737
+ const refFields = await reftable.getFields();
738
+
739
+ const joinFields = {};
740
+ if (reftable.ownership_formula && role > reftable.min_role_read) {
741
+ const freeVars = freeVariables(reftable.ownership_formula);
742
+ add_free_variables_to_joinfields(freeVars, joinFields, refFields);
743
+ }
744
+ const [refRow] = await reftable.getJoinedRows({
745
+ where: { id: row[ref] },
746
+ joinFields,
747
+ });
748
+ if (
749
+ role <= reftable.min_role_read ||
750
+ (req.user && reftable.is_owner(req.user, refRow))
751
+ ) {
752
+ row[ref] = refRow;
753
+ }
754
+ }
755
+ }
723
756
  }
724
757
  if (
725
758
  role > table.min_role_read &&
726
759
  !(req.user && table.is_owner(req.user, row))
727
760
  ) {
761
+ //console.log("not owner", row, table.is_owner(req.user, row));
728
762
  res.status(401).send("");
729
763
  return;
730
764
  }
@@ -734,16 +768,25 @@ router.post(
734
768
  if (kpath.length === 2 && row[kpath[0]]) {
735
769
  const field = fields.find((f) => f.name === kpath[0]);
736
770
  const reftable = await Table.findOne({ name: field.reftable_name });
737
- if (role > reftable.min_role_read) {
771
+ const refFields = await reftable.getFields();
772
+ const targetField = refFields.find((f) => f.name === kpath[1]);
773
+ //console.log({ kpath, fieldview, targetField });
774
+ const q = { [reftable.pk_name]: row[kpath[0]] };
775
+ const joinFields = {};
776
+ if (reftable.ownership_formula && role > reftable.min_role_read) {
777
+ const freeVars = freeVariables(reftable.ownership_formula);
778
+ add_free_variables_to_joinfields(freeVars, joinFields, refFields);
779
+ }
780
+ const [refRow] = await reftable.getJoinedRows({ where: q, joinFields });
781
+ if (
782
+ role > reftable.min_role_read &&
783
+ !(req.user && reftable.is_owner(req.user, refRow))
784
+ ) {
785
+ //console.log("not jointable owner", refRow);
786
+
738
787
  res.status(401).send("");
739
788
  return;
740
789
  }
741
- const targetField = (await reftable.getFields()).find(
742
- (f) => f.name === kpath[1]
743
- );
744
- //console.log({ kpath, fieldview, targetField });
745
- const q = { [reftable.pk_name]: row[kpath[0]] };
746
- const refRow = await reftable.getRow(q);
747
790
  let fv;
748
791
  if (targetField.type === "Key") {
749
792
  fv = getState().keyFieldviews[fieldview];
@@ -953,7 +996,8 @@ router.post(
953
996
  formStyle: "vert",
954
997
  fields: formFields,
955
998
  });
956
- if (_columndef) form.values = JSON.parse(_columndef);
999
+ if (_columndef && _columndef !== "undefined")
1000
+ form.values = JSON.parse(_columndef);
957
1001
  res.send(mkFormContentNoLayout(form));
958
1002
  })
959
1003
  );
package/routes/files.js CHANGED
@@ -393,6 +393,13 @@ router.post(
393
393
  f.s3_store ? s3storage.unlinkObject : undefined
394
394
  );
395
395
  if (result && result.error) {
396
+ if (req.xhr) {
397
+ const root = path.join(db.connectObj.file_store, db.getTenantSchema());
398
+ res.json({
399
+ error: result.error.replaceAll(root, ""),
400
+ });
401
+ return;
402
+ }
396
403
  req.flash("error", result.error);
397
404
  }
398
405
  res.redirect(`/files?dir=${encodeURIComponent(f.current_folder)}`);
@@ -438,6 +445,7 @@ router.get(
438
445
  contents: {
439
446
  type: "card",
440
447
  title: req.__("Storage settings"),
448
+ titleAjaxIndicator: true,
441
449
  contents: [renderForm(form, req.csrfToken())],
442
450
  },
443
451
  });
@@ -512,6 +520,7 @@ router.get(
512
520
  active_sub: "Settings",
513
521
  contents: {
514
522
  type: "card",
523
+ titleAjaxIndicator: true,
515
524
  title: req.__("Files settings"),
516
525
  contents: [renderForm(form, req.csrfToken())],
517
526
  },
package/routes/list.js CHANGED
@@ -308,6 +308,13 @@ router.get(
308
308
  ],
309
309
  right: div(
310
310
  { class: "d-flex" },
311
+ div(
312
+ {
313
+ class: "sc-ajax-indicator me-2",
314
+ style: { display: "none" },
315
+ },
316
+ i({ class: "fas fa-save" })
317
+ ),
311
318
  button(
312
319
  {
313
320
  class: "btn btn-sm btn-primary me-2",
@@ -391,6 +398,7 @@ router.get(
391
398
  });
392
399
  window.tabulator_table.on("cellEdited", function(cell){
393
400
  const row = cell.getRow().getData()
401
+ ajax_indicator(true);
394
402
  $.ajax({
395
403
  type: "POST",
396
404
  url: "/api/${table.name}/" + (row.id||""),
@@ -400,12 +408,15 @@ router.get(
400
408
  },
401
409
  error: tabulator_error_handler,
402
410
  }).done(function (resp) {
411
+ ajax_indicator(false);
403
412
  //if (item._versions) item._versions = +item._versions + 1;
404
413
  //data.resolve(fixKeys(item));
405
414
  if(resp.success &&typeof resp.success ==="number" && !row.id) {
406
415
  window.tabulator_table.updateRow(cell.getRow(), {id: resp.success});
407
416
  }
408
417
 
418
+ }).fail(function (resp) {
419
+ ajax_indicate_error(undefined, resp);
409
420
  });
410
421
  });
411
422
  window.tabulator_table_name="${table.name}";`)
package/routes/menu.js CHANGED
@@ -309,8 +309,14 @@ const menuEditorScript = (menu_items) => `
309
309
  const s = editor.getString()
310
310
  if(s===lastState && !skip_check) return;
311
311
  lastState=s;
312
- ajax_post('/menu', {data: s,
313
- success: ()=>{}, dataType : 'json', contentType: 'application/json;charset=UTF-8'})
312
+ ajax_indicator(true);
313
+ ajax_post('/menu', {
314
+ data: s,
315
+ success: ()=>{ ajax_indicator(false)},
316
+ dataType : 'json',
317
+ contentType: 'application/json;charset=UTF-8',
318
+ error: (r) => {ajax_indicate_error(undefined, r); }
319
+ })
314
320
  }
315
321
  var sortableListOptions = {
316
322
  placeholderCss: {'background-color': "#cccccc"},
@@ -395,6 +401,7 @@ router.get(
395
401
  contents: {
396
402
  type: "card",
397
403
  title: req.__(`Menu editor`),
404
+ titleAjaxIndicator: true,
398
405
  contents: {
399
406
  above: [
400
407
  {
@@ -251,6 +251,7 @@ router.get(
251
251
  {
252
252
  type: "card",
253
253
  title: req.__("Root pages"),
254
+ titleAjaxIndicator: true,
254
255
  contents: renderForm(
255
256
  getRootPageForm(pages, roles, req),
256
257
  req.csrfToken()
@@ -402,7 +403,7 @@ router.get(
402
403
  version_tag: db.connectObj.version_tag,
403
404
  };
404
405
  res.sendWrap(
405
- req.__(`Page configuration`),
406
+ req.__(`%s configuration`, page.name),
406
407
  wrap(renderBuilder(builderData, req.csrfToken()), true, req, page)
407
408
  );
408
409
  }
package/routes/plugins.js CHANGED
@@ -583,10 +583,23 @@ router.get(
583
583
  }
584
584
 
585
585
  res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
586
- type: "card",
587
- class: "mt-0",
588
- title: req.__(`Configure %s Plugin`, plugin.name),
589
- contents: renderForm(wfres.renderForm, req.csrfToken()),
586
+ above: [
587
+ {
588
+ type: "breadcrumbs",
589
+ crumbs: [
590
+ { text: req.__("Settings"), href: "/settings" },
591
+ { text: req.__("Module store"), href: "/plugins" },
592
+ { text: plugin.name },
593
+ ],
594
+ },
595
+ {
596
+ type: "card",
597
+ class: "mt-0",
598
+ title: req.__(`Configure %s Plugin`, plugin.name),
599
+ titleAjaxIndicator: true,
600
+ contents: renderForm(wfres.renderForm, req.csrfToken()),
601
+ },
602
+ ],
590
603
  });
591
604
  })
592
605
  );
package/routes/search.js CHANGED
@@ -90,6 +90,7 @@ router.get(
90
90
  contents: {
91
91
  type: "card",
92
92
  title: req.__(`Search configuration`),
93
+ titleAjaxIndicator: true,
93
94
  contents: renderForm(form, req.csrfToken()),
94
95
  },
95
96
  });
package/routes/tables.js CHANGED
@@ -828,6 +828,7 @@ router.get(
828
828
  {
829
829
  type: "card",
830
830
  title: req.__("Edit table properties"),
831
+ titleAjaxIndicator: true,
831
832
  contents: renderForm(tblForm, req.csrfToken()),
832
833
  },
833
834
  ],
@@ -899,20 +900,21 @@ router.post(
899
900
  }
900
901
  } else rest.ownership_formula = null;
901
902
  await table.update(rest);
902
- if (!old_versioned && rest.versioned)
903
- req.flash(
904
- "success",
905
- req.__("Table saved with version history enabled")
906
- );
907
- else if (old_versioned && !rest.versioned)
908
- req.flash(
909
- "success",
910
- req.__("Table saved with version history disabled")
911
- );
912
- else if (!hasError) req.flash("success", req.__("Table saved"));
913
903
 
914
- if (!req.xhr) res.redirect(`/table/${id}`);
915
- else res.json({ success: "ok", notify });
904
+ if (!req.xhr) {
905
+ if (!old_versioned && rest.versioned)
906
+ req.flash(
907
+ "success",
908
+ req.__("Table saved with version history enabled")
909
+ );
910
+ else if (old_versioned && !rest.versioned)
911
+ req.flash(
912
+ "success",
913
+ req.__("Table saved with version history disabled")
914
+ );
915
+ else if (!hasError) req.flash("success", req.__("Table saved"));
916
+ res.redirect(`/table/${id}`);
917
+ } else res.json({ success: "ok", notify });
916
918
  }
917
919
  })
918
920
  );
package/routes/tenant.js CHANGED
@@ -459,6 +459,7 @@ router.get(
459
459
  active_sub: "Multitenancy",
460
460
  contents: {
461
461
  type: "card",
462
+ titleAjaxIndicator: true,
462
463
  title: req.__("Multitenancy settings"),
463
464
  contents: [renderForm(form, req.csrfToken())],
464
465
  },
package/routes/utils.js CHANGED
@@ -76,7 +76,7 @@ const setLanguage = (req, res, state) => {
76
76
  } else if (req.cookies?.lang) {
77
77
  req.setLocale(req.cookies?.lang);
78
78
  }
79
- set_custom_http_headers(res, state);
79
+ set_custom_http_headers(res, req, state);
80
80
  };
81
81
 
82
82
  /**
@@ -85,8 +85,17 @@ const setLanguage = (req, res, state) => {
85
85
  * @param {string} state
86
86
  * @returns {void}
87
87
  */
88
- const set_custom_http_headers = (res, state) => {
89
- const hdrs = (state || getState()).getConfig("custom_http_headers");
88
+ const set_custom_http_headers = (res, req, state) => {
89
+ const state1 = state || getState();
90
+ const hdrs = state1.getConfig("custom_http_headers");
91
+ if (!req.user) {
92
+ const public_cache_maxage = +state1.getConfig("public_cache_maxage", 0);
93
+ if (public_cache_maxage)
94
+ res.header(
95
+ "Cache-Control",
96
+ `public, max-age=${public_cache_maxage * 60}`
97
+ );
98
+ }
90
99
  if (!hdrs) return;
91
100
  for (const ln of hdrs.split("\n")) {
92
101
  const [k, v] = ln.split(":");
package/routes/view.js CHANGED
@@ -76,6 +76,8 @@ router.get(
76
76
  view.attributes?.popup_width_units || "px"
77
77
  }`
78
78
  );
79
+ if (isModal && view.attributes?.popup_save_indicator)
80
+ res.set("SaltcornModalSaveIndicator", `true`);
79
81
  res.sendWrap(
80
82
  title,
81
83
  add_edit_bar({
@@ -239,6 +239,13 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
239
239
  options: ["px", "%", "vw", "em", "rem"],
240
240
  },
241
241
  },
242
+ {
243
+ name: "popup_save_indicator",
244
+ label: req.__("Save indicator"),
245
+ type: "Bool",
246
+ parent_field: "attributes",
247
+ tab: "Popup settings",
248
+ },
242
249
  ...(isEdit
243
250
  ? [
244
251
  new Field({
@@ -457,7 +464,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
457
464
  type: "breadcrumbs",
458
465
  crumbs: [
459
466
  { text: req.__("Views"), href: "/viewedit" },
460
- { href: `/viewedit/edit/${view.name}`, text: view.name },
467
+ { href: `/view/${view.name}`, text: view.name },
461
468
  { workflow: wf, step: wfres },
462
469
  ],
463
470
  },
@@ -465,6 +472,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
465
472
  type: noCard ? "container" : "card",
466
473
  class: !noCard && "mt-0",
467
474
  title: wfres.title,
475
+ titleAjaxIndicator: true,
468
476
  contents,
469
477
  },
470
478
  ...(previewURL
@@ -485,7 +493,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
485
493
  if (wfres.renderForm)
486
494
  res.sendWrap(
487
495
  {
488
- title: req.__(`View configuration`),
496
+ title: req.__(`%s configuration`, view.name),
489
497
  headers: [
490
498
  {
491
499
  script: `/static_assets/${db.connectObj.version_tag}/jquery-menu-editor.min.js`,
@@ -510,7 +518,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
510
518
  else if (wfres.renderBuilder) {
511
519
  wfres.renderBuilder.options.view_id = view.id;
512
520
  res.sendWrap(
513
- req.__(`View configuration`),
521
+ req.__(`%s configuration`, view.name),
514
522
  wrap(renderBuilder(wfres.renderBuilder, req.csrfToken()), true)
515
523
  );
516
524
  } else res.redirect(wfres.redirect);
package/serve.js CHANGED
@@ -377,7 +377,7 @@ const setupSocket = (...servers) => {
377
377
  const view = View.findOne({ name: viewname });
378
378
  if (view.viewtemplateObj.authorize_join) {
379
379
  view.viewtemplateObj
380
- .authorize_join(view.configuration, room_id, socket.request.user)
380
+ .authorize_join(view, room_id, socket.request.user)
381
381
  .then((authorized) => {
382
382
  if (authorized) socket.join(`${ten}_${viewname}_${room_id}`);
383
383
  });
package/wrapper.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * @module wrapper
4
4
  */
5
5
  const { getState } = require("@saltcorn/data/db/state");
6
+ const { get_extra_menu } = require("@saltcorn/data/web-mobile-commons");
6
7
  //const db = require("@saltcorn/data/db");
7
8
  const { h3, div, small } = require("@saltcorn/markup/tags");
8
9
  const { renderForm, link } = require("@saltcorn/markup");
@@ -18,43 +19,6 @@ const getFlashes = (req) =>
18
19
  return { type, msg: req.flash(type) };
19
20
  })
20
21
  .filter((a) => a.msg && a.msg.length && a.msg.length > 0);
21
- /**
22
- * Get extra menu
23
- * @param role
24
- * @param state
25
- * @param req
26
- * @returns {*}
27
- */
28
- const get_extra_menu = (role, state, req) => {
29
- let cfg = getState().getConfig("unrolled_menu_items", []);
30
- if (!cfg || cfg.length === 0) {
31
- cfg = getState().getConfig("menu_items", []);
32
- }
33
- const locale = req.getLocale();
34
- const __ = (s) => state.i18n.__({ phrase: s, locale }) || s;
35
- const transform = (items) =>
36
- items
37
- .filter((item) => role <= +item.min_role)
38
- .map((item) => ({
39
- label: __(item.label),
40
- icon: item.icon,
41
- location: item.location,
42
- style: item.style || "",
43
- type: item.type,
44
- link:
45
- item.type === "Link"
46
- ? item.url
47
- : item.type === "Action"
48
- ? `javascript:ajax_post_json('/menu/runaction/${item.action_name}')`
49
- : item.type === "View"
50
- ? `/view/${encodeURIComponent(item.viewname)}`
51
- : item.type === "Page"
52
- ? `/page/${encodeURIComponent(item.pagename)}`
53
- : undefined,
54
- ...(item.subitems ? { subitems: transform(item.subitems) } : {}),
55
- }));
56
- return transform(cfg);
57
- };
58
22
  /**
59
23
  * Get menu
60
24
  * @param req
@@ -67,7 +31,9 @@ const get_menu = (req) => {
67
31
 
68
32
  const allow_signup = state.getConfig("allow_signup");
69
33
  const login_menu = state.getConfig("login_menu");
70
- const extra_menu = get_extra_menu(role, state, req);
34
+ const locale = req.getLocale();
35
+ const __ = (s) => state.i18n.__({ phrase: s, locale }) || s;
36
+ const extra_menu = get_extra_menu(role, __);
71
37
  const authItems = isAuth
72
38
  ? [
73
39
  {