@saltcorn/server 0.8.0 → 0.8.1-beta.1

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,21 @@
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",
1071
+ "NPM packages in code": "NPM packages in code",
1072
+ "Comma-separated list of packages which will be available in JavaScript actions": "Comma-separated list of packages which will be available in JavaScript actions",
1073
+ "Become user": "Become user",
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
+ "Done": "Done",
1076
+ "Configure trigger %s": "Configure trigger %s"
1061
1077
  }
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/locales/it.json CHANGED
@@ -484,5 +484,6 @@
484
484
  "Table access": "Table access",
485
485
  "HTTP": "HTTP",
486
486
  "Rights": "Rights",
487
- "Permissions": "Permissions"
487
+ "Permissions": "Permissions",
488
+ "Become user": "Become user"
488
489
  }
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.1",
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.1",
10
+ "@saltcorn/builder": "0.8.1-beta.1",
11
+ "@saltcorn/data": "0.8.1-beta.1",
12
+ "@saltcorn/admin-models": "0.8.1-beta.1",
13
+ "@saltcorn/filemanager": "0.8.1-beta.1",
14
+ "@saltcorn/markup": "0.8.1-beta.1",
15
+ "@saltcorn/sbadmin2": "0.8.1-beta.1",
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")
@@ -392,10 +444,19 @@ function initialize_page() {
392
444
  setTimeout(() => {
393
445
  codes.forEach((el) => {
394
446
  //console.log($(el).attr("mode"), el);
395
- CodeMirror.fromTextArea(el, {
447
+ const cm = CodeMirror.fromTextArea(el, {
396
448
  lineNumbers: true,
397
449
  mode: $(el).attr("mode"),
398
450
  });
451
+ cm.on(
452
+ "change",
453
+ $.debounce(() => {
454
+ $(el).closest("form").trigger("change");
455
+ }),
456
+ 500,
457
+ null,
458
+ true
459
+ );
399
460
  });
400
461
  }, 100);
401
462
  });
@@ -508,14 +569,16 @@ function notifyAlert(note, spin) {
508
569
  }
509
570
 
510
571
  $("#alerts-area")
511
- .append(`<div class="alert alert-${type} alert-dismissible fade show ${spin ? "d-flex align-items-center" : ""
512
- }" role="alert">
572
+ .append(`<div class="alert alert-${type} alert-dismissible fade show ${
573
+ spin ? "d-flex align-items-center" : ""
574
+ }" role="alert">
513
575
  ${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">
576
+ ${
577
+ spin
578
+ ? `<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>`
579
+ : `<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
517
580
  </button>`
518
- }
581
+ }
519
582
  </div>`);
520
583
  }
521
584
 
@@ -532,8 +595,9 @@ function common_done(res, isWeb = true) {
532
595
  (isWeb ? location : parent.location).reload(); //TODO notify to cookie if reload or goto
533
596
  }
534
597
  if (res.download) {
535
- const dataurl = `data:${res.download.mimetype || "application/octet-stream"
536
- };base64,${res.download.blob}`;
598
+ const dataurl = `data:${
599
+ res.download.mimetype || "application/octet-stream"
600
+ };base64,${res.download.blob}`;
537
601
  fetch(dataurl)
538
602
  .then((res) => res.blob())
539
603
  .then((blob) => {
@@ -553,15 +617,19 @@ function common_done(res, isWeb = true) {
553
617
  else if (res.goto) {
554
618
  if (res.target === "_blank") window.open(res.goto, "_blank").focus();
555
619
  else {
556
- const prev = new URL(window.location.href)
557
- const next = new URL(res.goto, prev.origin)
620
+ const prev = new URL(window.location.href);
621
+ const next = new URL(res.goto, prev.origin);
558
622
  window.location.href = res.goto;
559
- if (prev.origin === next.origin && prev.pathname === next.pathname && next.hash !== prev.hash)
560
- location.reload()
623
+ if (
624
+ prev.origin === next.origin &&
625
+ prev.pathname === next.pathname &&
626
+ next.hash !== prev.hash
627
+ )
628
+ location.reload();
561
629
  }
562
630
  }
563
631
  if (res.popup) {
564
- ajax_modal(res.popup)
632
+ ajax_modal(res.popup);
565
633
  }
566
634
  }
567
635
 
@@ -572,7 +640,9 @@ const repeaterCopyValuesToForm = (form, editor, noTriggerChange) => {
572
640
  const $e = form.find(`input[name="${k}_${ix}"]`);
573
641
  if ($e.length) $e.val(v);
574
642
  else {
575
- const $ne = $(`<input type="hidden" data-repeater-ix="${ix}" name="${k}_${ix}"></input>`);
643
+ const $ne = $(
644
+ `<input type="hidden" data-repeater-ix="${ix}" name="${k}_${ix}"></input>`
645
+ );
576
646
  $ne.val(v);
577
647
  form.append($ne);
578
648
  }
@@ -700,9 +770,9 @@ function room_older(viewname, room_id, btn) {
700
770
  function init_room(viewname, room_id) {
701
771
  const socket = parent?.config?.server_path
702
772
  ? io(parent.config.server_path, {
703
- query: `jwt=${localStorage.getItem("auth_jwt")}`,
704
- transports: ["websocket"],
705
- })
773
+ query: `jwt=${localStorage.getItem("auth_jwt")}`,
774
+ transports: ["websocket"],
775
+ })
706
776
  : io({ transports: ["websocket"] });
707
777
 
708
778
  socket.emit("join_room", [viewname, room_id]);
@@ -735,32 +805,31 @@ function cancel_form(form) {
735
805
  }
736
806
 
737
807
  function split_paste_handler(e) {
738
- let clipboardData = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
808
+ let clipboardData =
809
+ e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
739
810
 
740
- const lines = clipboardData.getData('text').split(/\r\n/g)
811
+ const lines = clipboardData.getData("text").split(/\r\n/g);
741
812
 
742
813
  // do normal thing if not multiline - do not interfere with ordinary copy paste
743
814
  if (lines.length < 2) return;
744
815
  e.preventDefault();
745
- const form = $(e.target).closest('form')
816
+ const form = $(e.target).closest("form");
746
817
 
747
818
  let matched = false;
748
819
 
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
-
820
+ form
821
+ .find("input:not(:disabled):not([readonly]):not(:hidden)")
822
+ .each(function (ix, element) {
823
+ if (!matched && element === e.target) matched = true;
824
+ if (matched && lines.length > 0) {
825
+ const $elem = $(element);
826
+ if (ix === 0 && $elem.attr("type") !== "number") {
827
+ //const existing = $elem.val()
828
+ //const pasted =
829
+ $elem.val(lines.shift());
830
+ } else $elem.val(lines.shift());
831
+ }
832
+ });
764
833
  }
765
834
 
766
835
  function is_paging_param(key) {
@@ -97,7 +97,7 @@ function invalidate_pagings(href) {
97
97
  return newhref;
98
98
  }
99
99
 
100
- function set_state_fields(kvs) {
100
+ function set_state_fields(kvs, disable_pjax) {
101
101
  let newhref = get_current_state_url();
102
102
  if (Object.keys(kvs).some((k) => !is_paging_param(k))) {
103
103
  newhref = invalidate_pagings(newhref);
@@ -107,7 +107,8 @@ function set_state_fields(kvs) {
107
107
  newhref = removeQueryStringParameter(newhref, kv[0]);
108
108
  else newhref = updateQueryStringParameter(newhref, kv[0], kv[1]);
109
109
  });
110
- pjax_to(newhref.replace("&&", "&").replace("?&", "?"));
110
+ if (disable_pjax) href_to(newhref.replace("&&", "&").replace("?&", "?"));
111
+ else pjax_to(newhref.replace("&&", "&").replace("?&", "?"));
111
112
  }
112
113
  function unset_state_field(key) {
113
114
  pjax_to(removeQueryStringParameter(get_current_state_url(), key));
@@ -202,6 +203,19 @@ function view_post(viewname, route, data, onDone) {
202
203
  });
203
204
  }
204
205
  let logged_errors = [];
206
+ let error_catcher_enabled = false;
207
+ function enable_error_catcher() {
208
+ if (error_catcher_enabled) return;
209
+ document.addEventListener(
210
+ "DOMContentLoaded",
211
+ function () {
212
+ window.onerror = globalErrorCatcher;
213
+ },
214
+ false
215
+ );
216
+ error_catcher_enabled = true;
217
+ }
218
+
205
219
  function globalErrorCatcher(message, source, lineno, colno, error) {
206
220
  if (error && error.preventDefault) error.preventDefault();
207
221
  if (logged_errors.includes(message)) return;
@@ -260,16 +274,22 @@ function ajax_modal(url, opts = {}) {
260
274
  if (opts.submitReload === false) $("#scmodal").addClass("no-submit-reload");
261
275
  else $("#scmodal").removeClass("no-submit-reload");
262
276
  $.ajax(url, {
277
+ headers: {
278
+ SaltcornModalRequest: "true",
279
+ },
263
280
  success: function (res, textStatus, request) {
264
281
  var title = request.getResponseHeader("Page-Title");
282
+ var width = request.getResponseHeader("SaltcornModalWidth");
283
+ if (width) $(".modal-dialog").css("max-width", width);
284
+ else $(".modal-dialog").css("max-width", "");
265
285
  if (title) $("#scmodal .modal-title").html(decodeURIComponent(title));
266
286
  $("#scmodal .modal-body").html(res);
267
287
  $("#scmodal").prop("data-modal-state", url);
268
288
  new bootstrap.Modal($("#scmodal")).show();
269
289
  initialize_page();
270
- (opts.onOpen || function () { })(res);
290
+ (opts.onOpen || function () {})(res);
271
291
  $("#scmodal").on("hidden.bs.modal", function (e) {
272
- (opts.onClose || function () { })(res);
292
+ (opts.onClose || function () {})(res);
273
293
  $("body").css("overflow", "");
274
294
  });
275
295
  },
@@ -278,7 +298,7 @@ function ajax_modal(url, opts = {}) {
278
298
 
279
299
  function saveAndContinue(e, k) {
280
300
  var form = $(e).closest("form");
281
- const valres = form[0].reportValidity()
301
+ const valres = form[0].reportValidity();
282
302
  if (!valres) return;
283
303
  submitWithEmptyAction(form[0]);
284
304
  var url = form.attr("action");
@@ -323,7 +343,7 @@ function applyViewConfig(e, url, k) {
323
343
  "CSRF-Token": _sc_globalCsrf,
324
344
  },
325
345
  data: JSON.stringify(cfg),
326
- error: function (request) { },
346
+ error: function (request) {},
327
347
  success: function (res) {
328
348
  k && k(res);
329
349
  !k && updateViewPreview();
@@ -344,10 +364,14 @@ function updateViewPreview() {
344
364
  "CSRF-Token": _sc_globalCsrf,
345
365
  },
346
366
 
347
- error: function (request) { },
367
+ error: function (request) {},
348
368
  success: function (res) {
349
369
  $preview.css({ opacity: 1.0 });
350
370
 
371
+ //disable functions preview migght try to call
372
+ set_state_field = () => {};
373
+ set_state_fields = () => {};
374
+
351
375
  //disable elements in preview
352
376
  $preview.html(res);
353
377
  $preview.find("a").attr("href", "#");
@@ -357,11 +381,6 @@ function updateViewPreview() {
357
381
 
358
382
  $preview.find("textarea").attr("disabled", true);
359
383
  $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
384
  },
366
385
  });
367
386
  }
@@ -596,15 +615,13 @@ function build_mobile_app(button) {
596
615
  localStorage.setItem("sidebarClosed", `${closed}`);
597
616
  });
598
617
  }
599
- })()
600
-
601
-
618
+ })() +
602
619
  /*
603
620
  https://github.com/jeffdavidgreen/bootstrap-html5-history-tabs/blob/master/bootstrap-history-tabs.js
604
621
  Copyright (c) 2015 Jeff Green
605
622
  */
606
623
 
607
- + (function ($) {
624
+ (function ($) {
608
625
  "use strict";
609
626
  $.fn.historyTabs = function () {
610
627
  var that = this;
@@ -619,21 +636,24 @@ function build_mobile_app(button) {
619
636
  $(element).on("show.bs.tab", function () {
620
637
  var stateObject = { url: $(this).attr("href") };
621
638
 
622
- if (window.location.hash && stateObject.url !== window.location.hash) {
639
+ if (
640
+ window.location.hash &&
641
+ stateObject.url !== window.location.hash
642
+ ) {
623
643
  window.history.pushState(
624
644
  stateObject,
625
645
  document.title,
626
646
  window.location.pathname +
627
- window.location.search +
628
- $(this).attr("href")
647
+ window.location.search +
648
+ $(this).attr("href")
629
649
  );
630
650
  } else {
631
651
  window.history.replaceState(
632
652
  stateObject,
633
653
  document.title,
634
654
  window.location.pathname +
635
- window.location.search +
636
- $(this).attr("href")
655
+ window.location.search +
656
+ $(this).attr("href")
637
657
  );
638
658
  }
639
659
  });
@@ -649,4 +669,33 @@ function build_mobile_app(button) {
649
669
 
650
670
  // Copyright (c) 2011 Marcus Ekwall, http://writeless.se/
651
671
  // 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);
672
+ (function (a) {
673
+ var b = a.jQuery || a.me || (a.me = {}),
674
+ i = function (e, f, g, h, c, a) {
675
+ f || (f = 100);
676
+ var d = !1,
677
+ j = !1,
678
+ i = typeof g === "function",
679
+ l = function (a, b) {
680
+ d = setTimeout(function () {
681
+ d = !1;
682
+ if (h || c) e.apply(a, b), c && (j = +new Date());
683
+ i && g.apply(a, b);
684
+ }, f);
685
+ },
686
+ k = function () {
687
+ if (!d || a) {
688
+ if (!d && !h && (!c || +new Date() - j > f))
689
+ e.apply(this, arguments), c && (j = +new Date());
690
+ (a || !c) && clearTimeout(d);
691
+ l(this, arguments);
692
+ }
693
+ };
694
+ if (b.guid) k.guid = e.guid = e.guid || b.guid++;
695
+ return k;
696
+ };
697
+ b.throttle = i;
698
+ b.debounce = function (a, b, g, h, c) {
699
+ return i(a, b, g, h, c, !0);
700
+ };
701
+ })(this);
package/routes/actions.js CHANGED
@@ -401,7 +401,7 @@ router.get(
401
401
  sub2_page: "Configure",
402
402
  contents: {
403
403
  type: "card",
404
- title: req.__("Configure trigger"),
404
+ title: req.__("Configure trigger %s", trigger.name),
405
405
  contents: {
406
406
  widths: [8, 4],
407
407
  besides: [
@@ -456,6 +456,8 @@ router.get(
456
456
  // create form
457
457
  const form = new Form({
458
458
  action: addOnDoneRedirect(`/actions/configure/${id}`, req),
459
+ onChange: "saveAndContinue(this)",
460
+ submitLabel: req.__("Done"),
459
461
  fields: cfgFields,
460
462
  });
461
463
  // populate form values
@@ -468,7 +470,7 @@ router.get(
468
470
  sub2_page: "Configure",
469
471
  contents: {
470
472
  type: "card",
471
- title: req.__("Configure trigger"),
473
+ title: req.__("Configure trigger %s", trigger.name),
472
474
  contents: renderForm(form, req.csrfToken()),
473
475
  },
474
476
  });
@@ -512,6 +514,10 @@ router.post(
512
514
  });
513
515
  } else {
514
516
  await Trigger.update(trigger.id, { configuration: form.values });
517
+ if (req.xhr) {
518
+ res.json({ success: "ok" });
519
+ return;
520
+ }
515
521
  req.flash("success", "Action configuration saved");
516
522
  res.redirect(
517
523
  req.query.on_done_redirect
package/routes/admin.js CHANGED
@@ -59,6 +59,7 @@ const {
59
59
  getState,
60
60
  restart_tenant,
61
61
  getTenant,
62
+ getRootState,
62
63
  //get_other_domain_tenant,
63
64
  get_process_init_time,
64
65
  } = require("@saltcorn/data/db/state");
@@ -1820,9 +1821,22 @@ router.post(
1820
1821
  * @returns {Promise<Form>} form
1821
1822
  */
1822
1823
  const dev_form = async (req) => {
1824
+ const role_to_create_tenant = +getRootState().getConfig(
1825
+ "role_to_create_tenant"
1826
+ );
1827
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
1828
+
1823
1829
  return await config_fields_form({
1824
1830
  req,
1825
- field_names: ["development_mode", "log_sql", "log_level"],
1831
+ field_names: [
1832
+ "development_mode",
1833
+ "log_sql",
1834
+ "log_client_errors",
1835
+ "log_level",
1836
+ ...(isRoot || role_to_create_tenant < 10
1837
+ ? ["npm_available_js_code"]
1838
+ : []),
1839
+ ],
1826
1840
  action: "/admin/dev",
1827
1841
  });
1828
1842
  };
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) => (r.created ? 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);