@saltcorn/server 0.8.0 → 0.8.1-beta.0

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
@@ -162,6 +162,11 @@ const user_dropdown = (user, req, can_reset) =>
162
162
  },
163
163
  '<i class="fas fa-edit"></i>&nbsp;' + req.__("Edit")
164
164
  ),
165
+ post_dropdown_item(
166
+ `/useradmin/become-user/${user.id}`,
167
+ '<i class="fas fa-ghost"></i>&nbsp;' + req.__("Become user"),
168
+ req
169
+ ),
165
170
  post_dropdown_item(
166
171
  `/useradmin/set-random-password/${user.id}`,
167
172
  '<i class="fas fa-random"></i>&nbsp;' + req.__("Set random password"),
@@ -1074,6 +1079,35 @@ router.post(
1074
1079
  })
1075
1080
  );
1076
1081
 
1082
+ /**
1083
+ * Become user
1084
+ * @name post/become-user/:id
1085
+ * @function
1086
+ * @memberof module:auth/admin~auth/adminRouter
1087
+ */
1088
+ router.post(
1089
+ "/become-user/:id",
1090
+ isAdmin,
1091
+ error_catcher(async (req, res) => {
1092
+ const { id } = req.params;
1093
+ const u = await User.findOne({ id });
1094
+ if (u) {
1095
+ u.relogin(req);
1096
+ req.flash(
1097
+ "success",
1098
+ req.__(
1099
+ `Your are now logged in as %s. Logout and login again to assume your usual identity`,
1100
+ u.email
1101
+ )
1102
+ );
1103
+ res.redirect(`/`);
1104
+ } else {
1105
+ req.flash("error", req.__(`User not found`));
1106
+ res.redirect(`/useradmin`);
1107
+ }
1108
+ })
1109
+ );
1110
+
1077
1111
  /**
1078
1112
  * @name post/disable/:id
1079
1113
  * @function
package/locales/en.json CHANGED
@@ -1057,5 +1057,15 @@
1057
1057
  "Specifies a default filter for what file types the user can pick from the file input dialog box. Example is `.doc, text/csv,audio/*,video/*,image/*`": "Specifies a default filter for what file types the user can pick from the file input dialog box. Example is `.doc, text/csv,audio/*,video/*,image/*`",
1058
1058
  "Destination page": "Destination page",
1059
1059
  "Module Store endpoint": "Module Store endpoint",
1060
- "Authentication settings updated": "Authentication settings updated"
1060
+ "Authentication settings updated": "Authentication settings updated",
1061
+ "Log client errors": "Log client errors",
1062
+ "Record all client errors in the crash log": "Record all client errors in the crash log",
1063
+ "Default File accept filter": "Default File accept filter",
1064
+ "File upload debug": "File upload debug",
1065
+ "Turn on to debug file upload in express-fileupload.": "Turn on to debug file upload in express-fileupload.",
1066
+ "Upload size limit (Kb)": "Upload size limit (Kb)",
1067
+ "Maximum upload file size in kilobytes": "Maximum upload file size in kilobytes",
1068
+ "File upload timeout": "File upload timeout",
1069
+ "Defines how long to wait for data before aborting file upload. Set to 0 if you want to turn off timeout checks. ": "Defines how long to wait for data before aborting file upload. Set to 0 if you want to turn off timeout checks. ",
1070
+ "Files settings": "Files settings"
1061
1071
  }
package/locales/fr.json CHANGED
@@ -274,5 +274,17 @@
274
274
  "Field %s deleted": "Champ %s supprimé",
275
275
  "Language: ": "Langage: ",
276
276
  "Local": "Local",
277
- "Language changed to %s": "Langage changé vers %s"
278
- }
277
+ "Language changed to %s": "Langage changé vers %s",
278
+ "CSV upload": "CSV upload",
279
+ "Create page": "Create page",
280
+ "Action": "Action",
281
+ "Table or Channel": "Table or Channel",
282
+ "Add trigger": "Add trigger",
283
+ "Upload file(s)": "Upload file(s)",
284
+ "About application": "About application",
285
+ "Modules": "Modules",
286
+ "Users and security": "Users and security",
287
+ "Site structure": "Site structure",
288
+ "Events": "Events",
289
+ "Are you sure?": "Are you sure?"
290
+ }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.0",
3
+ "version": "0.8.1-beta.0",
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.0",
10
- "@saltcorn/builder": "0.8.0",
11
- "@saltcorn/data": "0.8.0",
12
- "@saltcorn/admin-models": "0.8.0",
13
- "@saltcorn/filemanager": "0.8.0",
14
- "@saltcorn/markup": "0.8.0",
15
- "@saltcorn/sbadmin2": "0.8.0",
9
+ "@saltcorn/base-plugin": "0.8.1-beta.0",
10
+ "@saltcorn/builder": "0.8.1-beta.0",
11
+ "@saltcorn/data": "0.8.1-beta.0",
12
+ "@saltcorn/admin-models": "0.8.1-beta.0",
13
+ "@saltcorn/filemanager": "0.8.1-beta.0",
14
+ "@saltcorn/markup": "0.8.1-beta.0",
15
+ "@saltcorn/sbadmin2": "0.8.1-beta.0",
16
16
  "@socket.io/cluster-adapter": "^0.1.0",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "aws-sdk": "^2.1037.0",
@@ -36,11 +36,11 @@ function add_repeater(nm) {
36
36
  newe.appendTo($("div.repeats-" + nm));
37
37
  }
38
38
 
39
- const _apply_showif_plugins = []
39
+ const _apply_showif_plugins = [];
40
40
 
41
- const add_apply_showif_plugin = p => {
42
- _apply_showif_plugins.push(p)
43
- }
41
+ const add_apply_showif_plugin = (p) => {
42
+ _apply_showif_plugins.push(p);
43
+ };
44
44
  function apply_showif() {
45
45
  $("[data-show-if]").each(function (ix, element) {
46
46
  var e = $(element);
@@ -92,7 +92,8 @@ function apply_showif() {
92
92
  } else {
93
93
  e.append(
94
94
  $(
95
- `<option ${`${current}` === `${o.value}` ? "selected" : ""
95
+ `<option ${
96
+ `${current}` === `${o.value}` ? "selected" : ""
96
97
  } value="${o.value}">${o.label}</option>`
97
98
  )
98
99
  );
@@ -117,76 +118,127 @@ function apply_showif() {
117
118
  e.attr("data-selected", ec.target.value);
118
119
  });
119
120
 
120
- const currentOptionsSet = e.prop('data-fetch-options-current-set')
121
+ const currentOptionsSet = e.prop("data-fetch-options-current-set");
121
122
  if (currentOptionsSet === qs) return;
122
123
 
123
124
  const activate = (success, qs) => {
124
125
  e.empty();
125
- e.prop('data-fetch-options-current-set', qs)
126
+ e.prop("data-fetch-options-current-set", qs);
126
127
  if (!dynwhere.required) e.append($(`<option></option>`));
127
128
  let currentDataOption = undefined;
128
- const dataOptions = []
129
+ const dataOptions = [];
129
130
  success.forEach((r) => {
130
131
  const label = dynwhere.label_formula
131
132
  ? new Function(
132
- `{${Object.keys(r).join(",")}}`,
133
- "return " + dynwhere.label_formula
134
- )(r)
135
- : r[dynwhere.summary_field]
136
- const value = r[dynwhere.refname]
137
- const selected = `${current}` === `${r[dynwhere.refname]}`
133
+ `{${Object.keys(r).join(",")}}`,
134
+ "return " + dynwhere.label_formula
135
+ )(r)
136
+ : r[dynwhere.summary_field];
137
+ const value = r[dynwhere.refname];
138
+ const selected = `${current}` === `${r[dynwhere.refname]}`;
138
139
  dataOptions.push({ text: label, value });
139
140
  if (selected) currentDataOption = value;
140
- const html = `<option ${selected ? "selected" : ""
141
- } value="${value}">${label}</option>`
142
- e.append(
143
- $(html)
144
- );
141
+ const html = `<option ${
142
+ selected ? "selected" : ""
143
+ } value="${value}">${label}</option>`;
144
+ e.append($(html));
145
145
  });
146
- element.dispatchEvent(new Event('RefreshSelectOptions'))
146
+ element.dispatchEvent(new Event("RefreshSelectOptions"));
147
147
  if (e.hasClass("selectized") && $().selectize) {
148
148
  e.selectize()[0].selectize.clearOptions();
149
149
  e.selectize()[0].selectize.addOption(dataOptions);
150
150
  if (typeof currentDataOption !== "undefined")
151
151
  e.selectize()[0].selectize.setValue(currentDataOption);
152
-
153
152
  }
154
- }
153
+ };
155
154
 
156
- const cache = e.prop('data-fetch-options-cache') || {}
155
+ const cache = e.prop("data-fetch-options-cache") || {};
157
156
  if (cache[qs]) {
158
- activate(cache[qs], qs)
157
+ activate(cache[qs], qs);
159
158
  } else
160
159
  $.ajax(`/api/${dynwhere.table}?${qs}`).then((resp) => {
161
160
  if (resp.success) {
162
- activate(resp.success, qs)
163
- const cacheNow = e.prop('data-fetch-options-cache') || {}
164
- e.prop('data-fetch-options-cache', { ...cacheNow, [qs]: resp.success })
161
+ activate(resp.success, qs);
162
+ const cacheNow = e.prop("data-fetch-options-cache") || {};
163
+ e.prop("data-fetch-options-cache", {
164
+ ...cacheNow,
165
+ [qs]: resp.success,
166
+ });
165
167
  }
166
168
  });
167
169
  });
168
170
 
169
171
  $("[data-source-url]").each(function (ix, element) {
170
172
  const e = $(element);
171
- const rec = get_form_record(e);
173
+ const rec0 = get_form_record(e);
174
+
175
+ const relevantFieldsStr = e.attr("data-relevant-fields");
176
+ let rec;
177
+ if (relevantFieldsStr) {
178
+ rec = {};
179
+ relevantFieldsStr.split(",").forEach((k) => {
180
+ rec[k] = rec0[k];
181
+ });
182
+ } else rec = rec0;
183
+ const recS = JSON.stringify(rec);
184
+
185
+ const shown = e.prop("data-source-url-current");
186
+ if (shown === recS) return;
187
+
188
+ const cache = e.prop("data-source-url-cache") || {};
189
+
190
+ const activate_onchange_coldef = () => {
191
+ e.closest(".form-namespace")
192
+ .find("input,select, textarea")
193
+ .on("change", (ec) => {
194
+ const $ec = $(ec.target);
195
+ const k = $ec.attr("name");
196
+ if (!k || k === "_columndef") return;
197
+ const v = ec.target.value;
198
+ const $def = e
199
+ .closest(".form-namespace")
200
+ .find("input[name=_columndef]");
201
+ const def = JSON.parse($def.val());
202
+ def[k] = v;
203
+ $def.val(JSON.stringify(def));
204
+ });
205
+ };
206
+
207
+ if (typeof cache[recS] !== "undefined") {
208
+ e.html(cache[recS]);
209
+ activate_onchange_coldef();
210
+ return;
211
+ }
172
212
  ajax_post_json(e.attr("data-source-url"), rec, {
173
213
  success: (data) => {
174
214
  e.html(data);
215
+ const cacheNow = e.prop("data-source-url-cache") || {};
216
+ e.prop("data-source-url-cache", {
217
+ ...cacheNow,
218
+ [recS]: data,
219
+ });
220
+ e.prop("data-source-url-current", recS);
221
+ activate_onchange_coldef();
175
222
  },
176
223
  error: (err) => {
177
224
  console.error(err);
225
+ const cacheNow = e.prop("data-source-url-cache") || {};
226
+ e.prop("data-source-url-cache", {
227
+ ...cacheNow,
228
+ [recS]: "",
229
+ });
178
230
  e.html("");
179
231
  },
180
232
  });
181
233
  });
182
- _apply_showif_plugins.forEach(p => p())
234
+ _apply_showif_plugins.forEach((p) => p());
183
235
  }
184
236
 
185
237
  function splitTargetMatch(elemValue, target, keySpec) {
186
238
  if (!elemValue) return false;
187
- const [fld, keySpec1] = keySpec.split("|_")
188
- const [sep, pos] = keySpec1.split("_")
189
- const elemValueShort = elemValue.split(sep)[pos]
239
+ const [fld, keySpec1] = keySpec.split("|_");
240
+ const [sep, pos] = keySpec1.split("_");
241
+ const elemValueShort = elemValue.split(sep)[pos];
190
242
  return elemValueShort === target;
191
243
  }
192
244
 
@@ -195,7 +247,7 @@ function get_form_record(e, select_labels) {
195
247
  e.closest(".form-namespace")
196
248
  .find("input[name],select[name]")
197
249
  .each(function () {
198
- const name = $(this).attr("data-fieldname") || $(this).attr("name")
250
+ const name = $(this).attr("data-fieldname") || $(this).attr("name");
199
251
  if (select_labels && $(this).prop("tagName").toLowerCase() === "select")
200
252
  rec[name] = $(this).find("option:selected").text();
201
253
  else if ($(this).prop("type") === "checkbox")
@@ -508,14 +560,16 @@ function notifyAlert(note, spin) {
508
560
  }
509
561
 
510
562
  $("#alerts-area")
511
- .append(`<div class="alert alert-${type} alert-dismissible fade show ${spin ? "d-flex align-items-center" : ""
512
- }" role="alert">
563
+ .append(`<div class="alert alert-${type} alert-dismissible fade show ${
564
+ spin ? "d-flex align-items-center" : ""
565
+ }" role="alert">
513
566
  ${txt}
514
- ${spin
515
- ? `<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>`
516
- : `<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
567
+ ${
568
+ spin
569
+ ? `<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>`
570
+ : `<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
517
571
  </button>`
518
- }
572
+ }
519
573
  </div>`);
520
574
  }
521
575
 
@@ -532,8 +586,9 @@ function common_done(res, isWeb = true) {
532
586
  (isWeb ? location : parent.location).reload(); //TODO notify to cookie if reload or goto
533
587
  }
534
588
  if (res.download) {
535
- const dataurl = `data:${res.download.mimetype || "application/octet-stream"
536
- };base64,${res.download.blob}`;
589
+ const dataurl = `data:${
590
+ res.download.mimetype || "application/octet-stream"
591
+ };base64,${res.download.blob}`;
537
592
  fetch(dataurl)
538
593
  .then((res) => res.blob())
539
594
  .then((blob) => {
@@ -553,15 +608,19 @@ function common_done(res, isWeb = true) {
553
608
  else if (res.goto) {
554
609
  if (res.target === "_blank") window.open(res.goto, "_blank").focus();
555
610
  else {
556
- const prev = new URL(window.location.href)
557
- const next = new URL(res.goto, prev.origin)
611
+ const prev = new URL(window.location.href);
612
+ const next = new URL(res.goto, prev.origin);
558
613
  window.location.href = res.goto;
559
- if (prev.origin === next.origin && prev.pathname === next.pathname && next.hash !== prev.hash)
560
- location.reload()
614
+ if (
615
+ prev.origin === next.origin &&
616
+ prev.pathname === next.pathname &&
617
+ next.hash !== prev.hash
618
+ )
619
+ location.reload();
561
620
  }
562
621
  }
563
622
  if (res.popup) {
564
- ajax_modal(res.popup)
623
+ ajax_modal(res.popup);
565
624
  }
566
625
  }
567
626
 
@@ -572,7 +631,9 @@ const repeaterCopyValuesToForm = (form, editor, noTriggerChange) => {
572
631
  const $e = form.find(`input[name="${k}_${ix}"]`);
573
632
  if ($e.length) $e.val(v);
574
633
  else {
575
- const $ne = $(`<input type="hidden" data-repeater-ix="${ix}" name="${k}_${ix}"></input>`);
634
+ const $ne = $(
635
+ `<input type="hidden" data-repeater-ix="${ix}" name="${k}_${ix}"></input>`
636
+ );
576
637
  $ne.val(v);
577
638
  form.append($ne);
578
639
  }
@@ -700,9 +761,9 @@ function room_older(viewname, room_id, btn) {
700
761
  function init_room(viewname, room_id) {
701
762
  const socket = parent?.config?.server_path
702
763
  ? io(parent.config.server_path, {
703
- query: `jwt=${localStorage.getItem("auth_jwt")}`,
704
- transports: ["websocket"],
705
- })
764
+ query: `jwt=${localStorage.getItem("auth_jwt")}`,
765
+ transports: ["websocket"],
766
+ })
706
767
  : io({ transports: ["websocket"] });
707
768
 
708
769
  socket.emit("join_room", [viewname, room_id]);
@@ -735,32 +796,31 @@ function cancel_form(form) {
735
796
  }
736
797
 
737
798
  function split_paste_handler(e) {
738
- let clipboardData = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
799
+ let clipboardData =
800
+ e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
739
801
 
740
- const lines = clipboardData.getData('text').split(/\r\n/g)
802
+ const lines = clipboardData.getData("text").split(/\r\n/g);
741
803
 
742
804
  // do normal thing if not multiline - do not interfere with ordinary copy paste
743
805
  if (lines.length < 2) return;
744
806
  e.preventDefault();
745
- const form = $(e.target).closest('form')
807
+ const form = $(e.target).closest("form");
746
808
 
747
809
  let matched = false;
748
810
 
749
- form.find('input:not(:disabled):not([readonly]):not(:hidden)').each(function (ix, element) {
750
- if (!matched && element === e.target) matched = true;
751
- if (matched && lines.length > 0) {
752
- const $elem = $(element)
753
- if (ix === 0 && $elem.attr("type") !== "number") {
754
- //const existing = $elem.val()
755
- //const pasted =
756
- $elem.val(lines.shift())
757
-
758
- } else
759
- $elem.val(lines.shift())
760
- }
761
- })
762
-
763
-
811
+ form
812
+ .find("input:not(:disabled):not([readonly]):not(:hidden)")
813
+ .each(function (ix, element) {
814
+ if (!matched && element === e.target) matched = true;
815
+ if (matched && lines.length > 0) {
816
+ const $elem = $(element);
817
+ if (ix === 0 && $elem.attr("type") !== "number") {
818
+ //const existing = $elem.val()
819
+ //const pasted =
820
+ $elem.val(lines.shift());
821
+ } else $elem.val(lines.shift());
822
+ }
823
+ });
764
824
  }
765
825
 
766
826
  function is_paging_param(key) {
@@ -202,6 +202,19 @@ function view_post(viewname, route, data, onDone) {
202
202
  });
203
203
  }
204
204
  let logged_errors = [];
205
+ let error_catcher_enabled = false;
206
+ function enable_error_catcher() {
207
+ if (error_catcher_enabled) return;
208
+ document.addEventListener(
209
+ "DOMContentLoaded",
210
+ function () {
211
+ window.onerror = globalErrorCatcher;
212
+ },
213
+ false
214
+ );
215
+ error_catcher_enabled = true;
216
+ }
217
+
205
218
  function globalErrorCatcher(message, source, lineno, colno, error) {
206
219
  if (error && error.preventDefault) error.preventDefault();
207
220
  if (logged_errors.includes(message)) return;
@@ -260,16 +273,22 @@ function ajax_modal(url, opts = {}) {
260
273
  if (opts.submitReload === false) $("#scmodal").addClass("no-submit-reload");
261
274
  else $("#scmodal").removeClass("no-submit-reload");
262
275
  $.ajax(url, {
276
+ headers: {
277
+ SaltcornModalRequest: "true",
278
+ },
263
279
  success: function (res, textStatus, request) {
264
280
  var title = request.getResponseHeader("Page-Title");
281
+ var width = request.getResponseHeader("SaltcornModalWidth");
282
+ if (width) $(".modal-dialog").css("max-width", width);
283
+ else $(".modal-dialog").css("max-width", "");
265
284
  if (title) $("#scmodal .modal-title").html(decodeURIComponent(title));
266
285
  $("#scmodal .modal-body").html(res);
267
286
  $("#scmodal").prop("data-modal-state", url);
268
287
  new bootstrap.Modal($("#scmodal")).show();
269
288
  initialize_page();
270
- (opts.onOpen || function () { })(res);
289
+ (opts.onOpen || function () {})(res);
271
290
  $("#scmodal").on("hidden.bs.modal", function (e) {
272
- (opts.onClose || function () { })(res);
291
+ (opts.onClose || function () {})(res);
273
292
  $("body").css("overflow", "");
274
293
  });
275
294
  },
@@ -278,7 +297,7 @@ function ajax_modal(url, opts = {}) {
278
297
 
279
298
  function saveAndContinue(e, k) {
280
299
  var form = $(e).closest("form");
281
- const valres = form[0].reportValidity()
300
+ const valres = form[0].reportValidity();
282
301
  if (!valres) return;
283
302
  submitWithEmptyAction(form[0]);
284
303
  var url = form.attr("action");
@@ -323,7 +342,7 @@ function applyViewConfig(e, url, k) {
323
342
  "CSRF-Token": _sc_globalCsrf,
324
343
  },
325
344
  data: JSON.stringify(cfg),
326
- error: function (request) { },
345
+ error: function (request) {},
327
346
  success: function (res) {
328
347
  k && k(res);
329
348
  !k && updateViewPreview();
@@ -344,10 +363,14 @@ function updateViewPreview() {
344
363
  "CSRF-Token": _sc_globalCsrf,
345
364
  },
346
365
 
347
- error: function (request) { },
366
+ error: function (request) {},
348
367
  success: function (res) {
349
368
  $preview.css({ opacity: 1.0 });
350
369
 
370
+ //disable functions preview migght try to call
371
+ set_state_field = () => {};
372
+ set_state_fields = () => {};
373
+
351
374
  //disable elements in preview
352
375
  $preview.html(res);
353
376
  $preview.find("a").attr("href", "#");
@@ -357,11 +380,6 @@ function updateViewPreview() {
357
380
 
358
381
  $preview.find("textarea").attr("disabled", true);
359
382
  $preview.find("input").attr("readonly", true);
360
-
361
- //disable functions preview migght try to call
362
- set_state_field = () => { }
363
- set_state_fields = () => { }
364
-
365
383
  },
366
384
  });
367
385
  }
@@ -596,15 +614,13 @@ function build_mobile_app(button) {
596
614
  localStorage.setItem("sidebarClosed", `${closed}`);
597
615
  });
598
616
  }
599
- })()
600
-
601
-
617
+ })() +
602
618
  /*
603
619
  https://github.com/jeffdavidgreen/bootstrap-html5-history-tabs/blob/master/bootstrap-history-tabs.js
604
620
  Copyright (c) 2015 Jeff Green
605
621
  */
606
622
 
607
- + (function ($) {
623
+ (function ($) {
608
624
  "use strict";
609
625
  $.fn.historyTabs = function () {
610
626
  var that = this;
@@ -619,21 +635,24 @@ function build_mobile_app(button) {
619
635
  $(element).on("show.bs.tab", function () {
620
636
  var stateObject = { url: $(this).attr("href") };
621
637
 
622
- if (window.location.hash && stateObject.url !== window.location.hash) {
638
+ if (
639
+ window.location.hash &&
640
+ stateObject.url !== window.location.hash
641
+ ) {
623
642
  window.history.pushState(
624
643
  stateObject,
625
644
  document.title,
626
645
  window.location.pathname +
627
- window.location.search +
628
- $(this).attr("href")
646
+ window.location.search +
647
+ $(this).attr("href")
629
648
  );
630
649
  } else {
631
650
  window.history.replaceState(
632
651
  stateObject,
633
652
  document.title,
634
653
  window.location.pathname +
635
- window.location.search +
636
- $(this).attr("href")
654
+ window.location.search +
655
+ $(this).attr("href")
637
656
  );
638
657
  }
639
658
  });
@@ -649,4 +668,33 @@ function build_mobile_app(button) {
649
668
 
650
669
  // Copyright (c) 2011 Marcus Ekwall, http://writeless.se/
651
670
  // https://github.com/mekwall/jquery-throttle
652
- (function (a) { var b = a.jQuery || a.me || (a.me = {}), i = function (e, f, g, h, c, a) { f || (f = 100); var d = !1, j = !1, i = typeof g === "function", l = function (a, b) { d = setTimeout(function () { d = !1; if (h || c) e.apply(a, b), c && (j = +new Date); i && g.apply(a, b) }, f) }, k = function () { if (!d || a) { if (!d && !h && (!c || +new Date - j > f)) e.apply(this, arguments), c && (j = +new Date); (a || !c) && clearTimeout(d); l(this, arguments) } }; if (b.guid) k.guid = e.guid = e.guid || b.guid++; return k }; b.throttle = i; b.debounce = function (a, b, g, h, c) { return i(a, b, g, h, c, !0) } })(this);
671
+ (function (a) {
672
+ var b = a.jQuery || a.me || (a.me = {}),
673
+ i = function (e, f, g, h, c, a) {
674
+ f || (f = 100);
675
+ var d = !1,
676
+ j = !1,
677
+ i = typeof g === "function",
678
+ l = function (a, b) {
679
+ d = setTimeout(function () {
680
+ d = !1;
681
+ if (h || c) e.apply(a, b), c && (j = +new Date());
682
+ i && g.apply(a, b);
683
+ }, f);
684
+ },
685
+ k = function () {
686
+ if (!d || a) {
687
+ if (!d && !h && (!c || +new Date() - j > f))
688
+ e.apply(this, arguments), c && (j = +new Date());
689
+ (a || !c) && clearTimeout(d);
690
+ l(this, arguments);
691
+ }
692
+ };
693
+ if (b.guid) k.guid = e.guid = e.guid || b.guid++;
694
+ return k;
695
+ };
696
+ b.throttle = i;
697
+ b.debounce = function (a, b, g, h, c) {
698
+ return i(a, b, g, h, c, !0);
699
+ };
700
+ })(this);
package/routes/admin.js CHANGED
@@ -1822,7 +1822,12 @@ router.post(
1822
1822
  const dev_form = async (req) => {
1823
1823
  return await config_fields_form({
1824
1824
  req,
1825
- field_names: ["development_mode", "log_sql", "log_level"],
1825
+ field_names: [
1826
+ "development_mode",
1827
+ "log_sql",
1828
+ "log_client_errors",
1829
+ "log_level",
1830
+ ],
1826
1831
  action: "/admin/dev",
1827
1832
  });
1828
1833
  };
package/routes/api.js CHANGED
@@ -381,10 +381,7 @@ router.post(
381
381
  res.status(400).json({ error: errors.join(", ") });
382
382
  return;
383
383
  }
384
- const ins_res = await table.tryInsertRow(
385
- row,
386
- req.user ? +req.user.id : undefined
387
- );
384
+ const ins_res = await table.tryInsertRow(row, req.user);
388
385
  if (ins_res.error) res.status(400).json(ins_res);
389
386
  else res.json(ins_res);
390
387
  } else {
@@ -439,11 +436,7 @@ router.post(
439
436
  res.status(400).json({ error: errors.join(", ") });
440
437
  return;
441
438
  }
442
- const ins_res = await table.tryUpdateRow(
443
- row,
444
- id,
445
- req.user ? +req.user.id : undefined
446
- );
439
+ const ins_res = await table.tryUpdateRow(row, id, req.user);
447
440
 
448
441
  if (ins_res.error) res.status(400).json(ins_res);
449
442
  else res.json(ins_res);
package/routes/fields.js CHANGED
@@ -28,11 +28,13 @@ const expressionBlurb = require("../markup/expression_blurb");
28
28
  const {
29
29
  readState,
30
30
  add_free_variables_to_joinfields,
31
+ calcfldViewConfig,
31
32
  } = require("@saltcorn/data/plugin-helper");
32
33
  const { wizardCardTitle } = require("../markup/forms.js");
33
34
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
34
35
  const { applyAsync } = require("@saltcorn/data/utils");
35
36
  const { text } = require("@saltcorn/markup/tags");
37
+ const { mkFormContentNoLayout } = require("@saltcorn/markup/form");
36
38
 
37
39
  /**
38
40
  * @type {object}
@@ -903,3 +905,45 @@ router.post(
903
905
  res.send("");
904
906
  })
905
907
  );
908
+
909
+ router.post(
910
+ "/fieldviewcfgform/:tableName",
911
+ isAdmin,
912
+ error_catcher(async (req, res) => {
913
+ const { tableName } = req.params;
914
+ const {
915
+ field_name,
916
+ fieldview,
917
+ type,
918
+ join_field,
919
+ join_fieldview,
920
+ _columndef,
921
+ } = req.body;
922
+ const table = await Table.findOne({ name: tableName });
923
+ const fieldName = type == "Field" ? field_name : join_field;
924
+ const fv_name = type == "Field" ? fieldview : join_fieldview;
925
+ if (!fieldName) {
926
+ res.send("");
927
+ return;
928
+ }
929
+
930
+ const field = await table.getField(fieldName);
931
+
932
+ const fieldViewConfigForms = await calcfldViewConfig([field], false, 0);
933
+ const formFields = fieldViewConfigForms[field.name][fv_name];
934
+ if (!formFields) {
935
+ res.send("");
936
+ return;
937
+ }
938
+ formFields.forEach((ff) => {
939
+ ff.class = ff.class ? `${ff.class} item-menu` : "item-menu";
940
+ });
941
+
942
+ const form = new Form({
943
+ formStyle: "vert",
944
+ fields: formFields,
945
+ });
946
+ if (_columndef) form.values = JSON.parse(_columndef);
947
+ res.send(mkFormContentNoLayout(form));
948
+ })
949
+ );
package/routes/tenant.js CHANGED
@@ -21,6 +21,7 @@ const {
21
21
  renderForm,
22
22
  link,
23
23
  post_delete_btn,
24
+ localeDateTime,
24
25
  mkTable,
25
26
  } = require("@saltcorn/markup");
26
27
  const {
@@ -384,7 +385,7 @@ router.get(
384
385
  },
385
386
  {
386
387
  label: req.__("Created"),
387
- key: (r) => text(r.created),
388
+ key: (r) => localeDateTime(r.created),
388
389
  },
389
390
  {
390
391
  label: req.__("Information"),
package/routes/view.js CHANGED
@@ -9,7 +9,7 @@ const Router = require("express-promise-router");
9
9
  const View = require("@saltcorn/data/models/view");
10
10
  const Table = require("@saltcorn/data/models/table");
11
11
 
12
- const { text } = require("@saltcorn/markup/tags");
12
+ const { text, style } = require("@saltcorn/markup/tags");
13
13
  const {
14
14
  isAdmin,
15
15
  error_catcher,
@@ -62,8 +62,20 @@ router.get(
62
62
  res.redirect("/");
63
63
  return;
64
64
  }
65
+ const isModal = req.headers?.saltcornmodalrequest;
66
+
65
67
  const contents = await view.run_possibly_on_page(query, req, res);
66
- const title = scan_for_page_title(contents, view.name);
68
+ const title =
69
+ isModal && view.attributes?.popup_title
70
+ ? view.attributes?.popup_title
71
+ : scan_for_page_title(contents, view.name);
72
+ if (isModal && view.attributes?.popup_width)
73
+ res.set(
74
+ "SaltcornModalWidth",
75
+ `${view.attributes?.popup_width}${
76
+ view.attributes?.popup_width_units || "px"
77
+ }`
78
+ );
67
79
  res.sendWrap(
68
80
  title,
69
81
  add_edit_bar({
@@ -134,6 +134,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
134
134
  action: addOnDoneRedirect("/viewedit/save", req),
135
135
  submitLabel: req.__("Configure") + " &raquo;",
136
136
  blurb: req.__("First, please give some basic information about the view."),
137
+ tabs: { tabsStyle: "Accordion" },
137
138
  fields: [
138
139
  new Field({
139
140
  label: req.__("View name"),
@@ -191,6 +192,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
191
192
  "Requests to render this view directly will instead show the chosen page, if any. The chosen page should embed this view. Use this to decorate the view with additional elements."
192
193
  ),
193
194
  input_type: "select",
195
+ tab: "View settings",
194
196
  options: [
195
197
  { value: "", label: "" },
196
198
  ...pages.map((p) => ({ value: p.name, label: p.name })),
@@ -201,6 +203,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
201
203
  label: req.__("Slug"),
202
204
  sublabel: req.__("Field that can be used for a prettier URL structure"),
203
205
  type: "String",
206
+ tab: "View settings",
204
207
  attributes: {
205
208
  calcOptions: [
206
209
  "table_name",
@@ -209,6 +212,33 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
209
212
  },
210
213
  showIf: { viewtemplate: hasTable },
211
214
  }),
215
+ new Field({
216
+ name: "popup_title",
217
+ label: req.__("Title"),
218
+ type: "String",
219
+ parent_field: "attributes",
220
+ tab: "Popup settings",
221
+ }),
222
+ {
223
+ name: "popup_width",
224
+ label: req.__("Column width"),
225
+ type: "Integer",
226
+ tab: "Popup settings",
227
+ parent_field: "attributes",
228
+ attributes: { asideNext: true },
229
+ },
230
+ {
231
+ name: "popup_width_units",
232
+ label: req.__("Units"),
233
+ type: "String",
234
+ tab: "Popup settings",
235
+ fieldview: "radio_group",
236
+ parent_field: "attributes",
237
+ attributes: {
238
+ inline: true,
239
+ options: ["px", "%", "vw", "em", "rem"],
240
+ },
241
+ },
212
242
  ...(isEdit
213
243
  ? [
214
244
  new Field({
@@ -396,6 +426,7 @@ router.post(
396
426
  const vt = getState().viewtemplates[v.viewtemplate];
397
427
  if (vt.initial_config) v.configuration = await vt.initial_config(v);
398
428
  else v.configuration = {};
429
+ //console.log(v);
399
430
  await View.create(v);
400
431
  }
401
432
  res.redirect(
@@ -503,6 +534,9 @@ router.get(
503
534
  res.redirect("/viewedit");
504
535
  return;
505
536
  }
537
+ (view.configuration?.columns || []).forEach((c) => {
538
+ c._columndef = JSON.stringify(c);
539
+ });
506
540
  const configFlow = await view.get_config_flow(req);
507
541
  const hasConfig =
508
542
  view.configuration && Object.keys(view.configuration).length > 0;
@@ -644,6 +678,7 @@ router.post(
644
678
 
645
679
  if (viewname && req.body) {
646
680
  const view = await View.findOne({ name: viewname });
681
+ req.staticFieldViewConfig = true;
647
682
  const configFlow = await view.get_config_flow(req);
648
683
  const step = await configFlow.singleStepForm(req.body, req);
649
684
  if (step?.renderForm) {
package/s3storage.js CHANGED
@@ -46,7 +46,8 @@ module.exports = {
46
46
  s3upload(req, res, next);
47
47
  } else {
48
48
  // Use regular file upload https://www.npmjs.com/package/express-fileupload
49
- const fileSizeLimit = getState().getConfig("file_upload_limit", 0);
49
+ const fileSizeLimit =
50
+ 1024 * +getState().getConfig("file_upload_limit", 0);
50
51
  fileUpload({
51
52
  useTempFiles: true,
52
53
  createParentPath: true,
@@ -58,9 +59,11 @@ module.exports = {
58
59
  defCharset: "utf8",
59
60
  defParamCharset: "utf8",
60
61
  // 0 - means no upload limit check
61
- limits: {
62
- fileSize: fileSizeLimit,
63
- },
62
+ limits: fileSizeLimit
63
+ ? {
64
+ fileSize: fileSizeLimit,
65
+ }
66
+ : {},
64
67
  abortOnLimit: fileSizeLimit !== 0,
65
68
  // 0 - means no upload limit check
66
69
  uploadTimeout: getState().getConfig("file_upload_timeout", 0),
@@ -367,3 +367,26 @@ describe("Field Endpoints", () => {
367
367
  .expect((r) => +r.body > 1);
368
368
  });
369
369
  });
370
+
371
+ describe("Fieldview config", () => {
372
+ //itShouldRedirectUnauthToLogin("/field/2");
373
+ it("should return fieldview options", async () => {
374
+ const loginCookie = await getAdminLoginCookie();
375
+
376
+ const app = await getApp({ disableCsrf: true });
377
+
378
+ await request(app)
379
+ .post("/field/fieldviewcfgform/books")
380
+ .set("Cookie", loginCookie)
381
+ .send({
382
+ type: "Field",
383
+ field_name: "pages",
384
+ fieldview: "progress_bar",
385
+ })
386
+ .expect(
387
+ toInclude(
388
+ `<div class="form-group"><div><label for="inputmax">max</label></div><div><input type="number" class="form-control item-menu" data-fieldname="max" name="max" id="inputmax" step="1" required></div></div><div class="form-group"><div><label for="inputbar_color">Bar color</label></div><div><input type="color" class="form-control item-menu" data-fieldname="bar_color" name="bar_color" id="inputbar_color"></div></div><div class="form-group"><div><label for="inputbg_color">Background color</label></div><div><input type="color" class="form-control item-menu" data-fieldname="bg_color" name="bg_color" id="inputbg_color"></div></div><div class="form-group"><div><label for="inputpx_height">Height in px</label></div><div><input type="number" class="form-control item-menu" data-fieldname="px_height" name="px_height" id="inputpx_height" step="1"></div></div>`
389
+ )
390
+ );
391
+ });
392
+ });
package/wrapper.js CHANGED
@@ -203,6 +203,8 @@ const get_headers = (req, version_tag, description, extras = []) => {
203
203
  from_cfg.push({ style: state.getConfig("page_custom_css", "") });
204
204
  if (state.getConfig("page_custom_html", ""))
205
205
  from_cfg.push({ headerTag: state.getConfig("page_custom_html", "") });
206
+ if (state.getConfig("log_client_errors", false))
207
+ from_cfg.push({ scriptBody: `enable_error_catcher()` });
206
208
  const state_headers = [];
207
209
  for (const hs of Object.values(state.headers)) {
208
210
  state_headers.push(...hs);