@saltcorn/server 0.8.1-beta.4 → 0.8.1-rc.2

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
@@ -376,6 +376,7 @@ router.get(
376
376
  active_sub: "Login and Signup",
377
377
  contents: {
378
378
  type: "card",
379
+ titleAjaxIndicator: true,
379
380
  title: req.__("Authentication settings"),
380
381
  contents: [renderForm(form, req.csrfToken())],
381
382
  },
@@ -408,9 +409,10 @@ router.post(
408
409
  });
409
410
  } else {
410
411
  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" });
412
+ if (!req.xhr) {
413
+ req.flash("success", req.__("Authentication settings updated"));
414
+ res.redirect("/useradmin/settings");
415
+ } else res.json({ success: "ok" });
414
416
  }
415
417
  })
416
418
  );
@@ -432,6 +434,7 @@ router.get(
432
434
  active_sub: "HTTP",
433
435
  contents: {
434
436
  type: "card",
437
+ titleAjaxIndicator: true,
435
438
  title: req.__("HTTP settings"),
436
439
  contents: [renderForm(form, req.csrfToken())],
437
440
  },
@@ -464,9 +467,11 @@ router.post(
464
467
  });
465
468
  } else {
466
469
  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" });
470
+
471
+ if (!req.xhr) {
472
+ req.flash("success", req.__("HTTP settings updated"));
473
+ res.redirect("/useradmin/http");
474
+ } else res.json({ success: "ok" });
470
475
  }
471
476
  })
472
477
  );
@@ -488,6 +493,7 @@ router.get(
488
493
  active_sub: "Permissions",
489
494
  contents: {
490
495
  type: "card",
496
+ titleAjaxIndicator: true,
491
497
  title: req.__("Permissions settings"),
492
498
  contents: [renderForm(form, req.csrfToken())],
493
499
  },
@@ -514,15 +520,17 @@ router.post(
514
520
  active_sub: "Permissions",
515
521
  contents: {
516
522
  type: "card",
523
+ titleAjaxIndicator: true,
517
524
  title: req.__("Permissions settings"),
518
525
  contents: [renderForm(form, req.csrfToken())],
519
526
  },
520
527
  });
521
528
  } else {
522
529
  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" });
530
+ if (!req.xhr) {
531
+ req.flash("success", req.__("Permissions settings updated"));
532
+ res.redirect("/useradmin/permissions");
533
+ } else res.json({ success: "ok" });
526
534
  }
527
535
  })
528
536
  );
@@ -677,8 +685,9 @@ router.get(
677
685
  active_sub: "SSL",
678
686
  contents: {
679
687
  type: "card",
680
- title: req.__("Authentication settings"),
688
+ title: req.__("Custom SSL certificates"),
681
689
  sub2_page: req.__("Custom SSL certificates"),
690
+ titleAjaxIndicator: true,
682
691
  contents: [renderForm(form, req.csrfToken())],
683
692
  },
684
693
  });
@@ -817,6 +826,7 @@ router.get(
817
826
  contents: {
818
827
  type: "card",
819
828
  title: req.__("Table access"),
829
+ titleAjaxIndicator: true,
820
830
  contents,
821
831
  },
822
832
  });
package/locales/en.json CHANGED
@@ -1073,5 +1073,9 @@
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"
1077
1081
  }
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.4",
3
+ "version": "0.8.1-rc.2",
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.4",
10
- "@saltcorn/builder": "0.8.1-beta.4",
11
- "@saltcorn/data": "0.8.1-beta.4",
12
- "@saltcorn/admin-models": "0.8.1-beta.4",
13
- "@saltcorn/filemanager": "0.8.1-beta.4",
14
- "@saltcorn/markup": "0.8.1-beta.4",
15
- "@saltcorn/sbadmin2": "0.8.1-beta.4",
9
+ "@saltcorn/base-plugin": "0.8.1-rc.2",
10
+ "@saltcorn/builder": "0.8.1-rc.2",
11
+ "@saltcorn/data": "0.8.1-rc.2",
12
+ "@saltcorn/admin-models": "0.8.1-rc.2",
13
+ "@saltcorn/filemanager": "0.8.1-rc.2",
14
+ "@saltcorn/markup": "0.8.1-rc.2",
15
+ "@saltcorn/sbadmin2": "0.8.1-rc.2",
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
@@ -194,6 +194,7 @@ router.get(
194
194
  contents: {
195
195
  type: "card",
196
196
  title: req.__("Site identity settings"),
197
+ titleAjaxIndicator: true,
197
198
  contents: [renderForm(form, req.csrfToken())],
198
199
  },
199
200
  });
@@ -251,6 +252,7 @@ router.get(
251
252
  contents: {
252
253
  type: "card",
253
254
  title: req.__("Email settings"),
255
+ titleAjaxIndicator: true,
254
256
  contents: [
255
257
  renderForm(form, req.csrfToken()),
256
258
  a(
@@ -321,9 +323,10 @@ router.post(
321
323
  });
322
324
  } else {
323
325
  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" });
326
+ if (!req.xhr) {
327
+ req.flash("success", req.__("Email settings updated"));
328
+ res.redirect("/admin/email");
329
+ } else res.json({ success: "ok" });
327
330
  }
328
331
  })
329
332
  );
@@ -391,6 +394,7 @@ router.get(
391
394
  ? {
392
395
  type: "card",
393
396
  title: req.__("Automated backup"),
397
+ titleAjaxIndicator: true,
394
398
  contents: div(
395
399
  renderForm(backupForm, req.csrfToken()),
396
400
  a(
@@ -408,6 +412,7 @@ router.get(
408
412
  {
409
413
  type: "card",
410
414
  title: req.__("Snapshots"),
415
+ titleAjaxIndicator: true,
411
416
  contents: div(
412
417
  p(
413
418
  i(
@@ -708,9 +713,11 @@ router.post(
708
713
  form.validate(req.body);
709
714
 
710
715
  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" });
716
+
717
+ if (!req.xhr) {
718
+ req.flash("success", req.__("Snapshot settings updated"));
719
+ res.redirect("/admin/backup");
720
+ } else res.json({ success: "ok" });
714
721
  })
715
722
  );
716
723
  router.post(
@@ -732,9 +739,10 @@ router.post(
732
739
  });
733
740
  } else {
734
741
  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" });
742
+ if (!req.xhr) {
743
+ req.flash("success", req.__("Backup settings updated"));
744
+ res.redirect("/admin/backup");
745
+ } else res.json({ success: "ok" });
738
746
  }
739
747
  })
740
748
  );
@@ -1306,7 +1314,7 @@ const buildDialogScript = () => {
1306
1314
  }
1307
1315
 
1308
1316
  function handleMessages() {
1309
- notifyAlert("This is still under development and might run longer.")
1317
+ notifyAlert("Building the app, please wait.")
1310
1318
  ${
1311
1319
  getState().getConfig("apple_team_id") &&
1312
1320
  getState().getConfig("apple_team_id") !== "null"
@@ -1858,6 +1866,7 @@ router.get(
1858
1866
  contents: {
1859
1867
  type: "card",
1860
1868
  title: req.__("Development settings"),
1869
+ titleAjaxIndicator: true,
1861
1870
  contents: [
1862
1871
  renderForm(form, req.csrfToken()) /*,
1863
1872
  a(
@@ -1899,9 +1908,10 @@ router.post(
1899
1908
  });
1900
1909
  } else {
1901
1910
  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" });
1911
+ if (!req.xhr) {
1912
+ req.flash("success", req.__("Development mode settings updated"));
1913
+ res.redirect("/admin/dev");
1914
+ } else res.json({ success: "ok" });
1905
1915
  }
1906
1916
  })
1907
1917
  );
@@ -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];
package/routes/files.js CHANGED
@@ -438,6 +438,7 @@ router.get(
438
438
  contents: {
439
439
  type: "card",
440
440
  title: req.__("Storage settings"),
441
+ titleAjaxIndicator: true,
441
442
  contents: [renderForm(form, req.csrfToken())],
442
443
  },
443
444
  });
@@ -512,6 +513,7 @@ router.get(
512
513
  active_sub: "Settings",
513
514
  contents: {
514
515
  type: "card",
516
+ titleAjaxIndicator: true,
515
517
  title: req.__("Files settings"),
516
518
  contents: [renderForm(form, req.csrfToken())],
517
519
  },
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/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({
@@ -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/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
  {