@saltcorn/server 0.7.1-beta.1 → 0.7.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/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.7.1-beta.1",
3
+ "version": "0.7.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.7.1-beta.1",
10
- "@saltcorn/builder": "0.7.1-beta.1",
11
- "@saltcorn/data": "0.7.1-beta.1",
12
- "@saltcorn/admin-models": "0.7.1-beta.1",
13
- "@saltcorn/markup": "0.7.1-beta.1",
14
- "@saltcorn/sbadmin2": "0.7.1-beta.1",
9
+ "@saltcorn/base-plugin": "0.7.1",
10
+ "@saltcorn/builder": "0.7.1",
11
+ "@saltcorn/data": "0.7.1",
12
+ "@saltcorn/admin-models": "0.7.1",
13
+ "@saltcorn/markup": "0.7.1",
14
+ "@saltcorn/sbadmin2": "0.7.1",
15
15
  "@socket.io/cluster-adapter": "^0.1.0",
16
16
  "@socket.io/sticky": "^1.0.1",
17
17
  "aws-sdk": "^2.1037.0",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "repository": "github:saltcorn/saltcorn",
59
59
  "devDependencies": {
60
- "jest": "^25.1.0",
60
+ "jest": "26.6.3",
61
61
  "supertest": "^4.0.2"
62
62
  },
63
63
  "scripts": {
@@ -1,9 +1,13 @@
1
1
  function MenuEditor(e, t) {
2
+ const radioValue = (e) =>
3
+ $(`input:radio[name ='${$(e).attr("name")}']:checked`).val();
2
4
  var s = $("#" + e).data("level", "0"),
3
5
  l = {
4
6
  labelEdit: '<i class="fas fa-edit clickable"></i>',
5
7
  labelRemove: '<i class="fas fa-trash-alt clickable"></i>',
6
8
  textConfirmDelete: "This item will be deleted. Are you sure?",
9
+ getLabelText: (e) => e.text,
10
+ onUpdate: () => {},
7
11
  iconPicker: { cols: 4, rows: 4, footer: !1, iconset: "fontawesome5" },
8
12
  maxLevel: -1,
9
13
  listOptions: {
@@ -28,6 +32,8 @@ function MenuEditor(e, t) {
28
32
  },
29
33
  };
30
34
  $.extend(!0, l, t);
35
+ var mqeSets = l;
36
+
31
37
  var n = null,
32
38
  o = !0,
33
39
  i = null,
@@ -35,10 +41,11 @@ function MenuEditor(e, t) {
35
41
  a = l.iconPicker,
36
42
  c = ((t = l.listOptions), $("#" + e + "_icon").iconpicker(a));
37
43
  function d() {
38
- i[0].reset(),
39
- (c = c.iconpicker(a)).iconpicker("setIcon", "empty"),
40
- r.attr("disabled", !0),
41
- (n = null);
44
+ if (!i || !i[0])
45
+ i[0].reset(),
46
+ (c = c.iconpicker(a)).iconpicker("setIcon", "empty"),
47
+ r.attr("disabled", !0),
48
+ (n = null);
42
49
  }
43
50
  function p(e) {
44
51
  return $("<a>")
@@ -117,7 +124,14 @@ function MenuEditor(e, t) {
117
124
  (function (e) {
118
125
  var t = e.data();
119
126
  $.each(t, function (e, t) {
120
- i.find("[name=" + e + "]").val(t);
127
+ var el = i.find("[name=" + e + "]");
128
+ if (el.prop("type") == "checkbox") el.prop("checked", !!t);
129
+ else if (el.prop("type") == "radio") {
130
+ $(`input[name=${el.prop("name")}][value="${t}"]`).prop(
131
+ "checked",
132
+ true
133
+ );
134
+ } else el.val(t);
121
135
  }),
122
136
  i.find(".item-menu").first().focus(),
123
137
  t.hasOwnProperty("icon")
@@ -125,6 +139,7 @@ function MenuEditor(e, t) {
125
139
  : c.iconpicker("setIcon", "empty");
126
140
  r.removeAttr("disabled");
127
141
  })((n = $(this).closest("li")));
142
+ l.onUpdate();
128
143
  }),
129
144
  s.on("click", ".btnUp", function (e) {
130
145
  e.preventDefault();
@@ -180,20 +195,34 @@ function MenuEditor(e, t) {
180
195
  if (null !== e) {
181
196
  var t = e.data("icon");
182
197
  i.find(".item-menu").each(function () {
183
- e.data($(this).attr("name"), $(this).val());
198
+ if (!$(this).prop("disabled"))
199
+ e.data(
200
+ $(this).attr("name"),
201
+ $(this).attr("type") === "radio"
202
+ ? radioValue(this)
203
+ : $(this).attr("type") === "checkbox"
204
+ ? $(this).prop("checked")
205
+ : $(this).val()
206
+ );
184
207
  }),
185
208
  e.children().children("i").removeClass(t).addClass(e.data("icon")),
186
- e.find("span.txt").first().text(e.data("text")),
209
+ e.find("span.txt").first().text(mqeSets.getLabelText(e.data())),
187
210
  d();
188
211
  }
189
212
  }),
190
213
  (this.add = function () {
191
214
  var e = {};
192
215
  i.find(".item-menu").each(function () {
193
- e[$(this).attr("name")] = $(this).val();
216
+ if (!$(this).prop("disabled"))
217
+ e[$(this).attr("name")] =
218
+ $(this).attr("type") === "radio"
219
+ ? radioValue(this)
220
+ : $(this).attr("type") === "checkbox"
221
+ ? $(this).prop("checked")
222
+ : $(this).val();
194
223
  });
195
224
  var t = u(),
196
- l = $("<span>").addClass("txt").text(e.text),
225
+ l = $("<span>").addClass("txt").text(mqeSets.getLabelText(e)),
197
226
  n = $("<i>").addClass(e.icon),
198
227
  o = $("<div>")
199
228
  .css({ overflow: "auto" })
@@ -251,7 +280,7 @@ function MenuEditor(e, t) {
251
280
  d = $("<i>").addClass(s.icon),
252
281
  p = $("<span>")
253
282
  .addClass("txt")
254
- .append(s.text)
283
+ .append(mqeSets.getLabelText(s))
255
284
  .css("margin-right", "5px"),
256
285
  f = u();
257
286
  c.append(d).append("&nbsp;").append(p).append(f),
@@ -486,34 +515,38 @@ function MenuEditor(e, t) {
486
515
  : h(d),
487
516
  (d.oElOld = d.oEl),
488
517
  (s.el[0].style.visibility = "hidden"),
489
- (d.oEl = oEl = (function (t, s) {
490
- if (!document.elementFromPoint) return null;
491
- var l = d.isRelEFP;
492
- if (null === l) {
493
- var n, o;
494
- (n = d.doc.scrollTop()) > 0 &&
495
- (l =
496
- null ==
497
- (o = document.elementFromPoint(
498
- 0,
499
- n + e(window).height() - 1
500
- )) || "HTML" == o.tagName.toUpperCase()),
501
- (n = d.doc.scrollLeft()) > 0 &&
518
+ (d.oEl = oEl =
519
+ (function (t, s) {
520
+ if (!document.elementFromPoint) return null;
521
+ var l = d.isRelEFP;
522
+ if (null === l) {
523
+ var n, o;
524
+ (n = d.doc.scrollTop()) > 0 &&
502
525
  (l =
503
526
  null ==
504
527
  (o = document.elementFromPoint(
505
- n + e(window).width() - 1,
506
- 0
507
- )) || "HTML" == o.tagName.toUpperCase());
508
- }
509
- l && ((t -= d.doc.scrollLeft()), (s -= d.doc.scrollTop()));
510
- var i = e(document.elementFromPoint(t, s));
511
- if (!d.rootEl.el.find(i).length) return null;
512
- if (i.is("#sortableListsPlaceholder") || i.is("#sortableListsHint"))
513
- return null;
514
- if (!i.is("li")) return (i = i.closest("li"))[0] ? i : null;
515
- if (i.is("li")) return i;
516
- })(t.pageX, t.pageY)),
528
+ 0,
529
+ n + e(window).height() - 1
530
+ )) || "HTML" == o.tagName.toUpperCase()),
531
+ (n = d.doc.scrollLeft()) > 0 &&
532
+ (l =
533
+ null ==
534
+ (o = document.elementFromPoint(
535
+ n + e(window).width() - 1,
536
+ 0
537
+ )) || "HTML" == o.tagName.toUpperCase());
538
+ }
539
+ l && ((t -= d.doc.scrollLeft()), (s -= d.doc.scrollTop()));
540
+ var i = e(document.elementFromPoint(t, s));
541
+ if (!d.rootEl.el.find(i).length) return null;
542
+ if (
543
+ i.is("#sortableListsPlaceholder") ||
544
+ i.is("#sortableListsHint")
545
+ )
546
+ return null;
547
+ if (!i.is("li")) return (i = i.closest("li"))[0] ? i : null;
548
+ if (i.is("li")) return i;
549
+ })(t.pageX, t.pageY)),
517
550
  (s.el[0].style.visibility = "visible"),
518
551
  (function (e, t) {
519
552
  var s = t.oEl;
@@ -224,6 +224,3 @@ footer.bs-mobile-nav-footer {
224
224
  .form-group {
225
225
  margin-bottom: 1rem;
226
226
  }
227
- div.dropdown-menu {
228
- position: absolute;
229
- }
@@ -10,13 +10,17 @@ jQuery.fn.swapWith = function (to) {
10
10
 
11
11
  //avoids hiding in overflow:hidden
12
12
  function init_bs5_dropdowns() {
13
- $("body").on("show.bs.dropdown", "[data-bs-toggle=dropdown]", function () {
14
- let target;
15
- if (!$("#page-inner-content").length) target = $("body");
16
- else target = $("#page-inner-content");
17
- let dropdown = bootstrap.Dropdown.getInstance(this);
18
- $(dropdown._menu).insertAfter(target);
19
- });
13
+ $("body").on(
14
+ "show.bs.dropdown",
15
+ "table [data-bs-toggle=dropdown]",
16
+ function () {
17
+ let target;
18
+ if (!$("#page-inner-content").length) target = $("body");
19
+ else target = $("#page-inner-content");
20
+ let dropdown = bootstrap.Dropdown.getInstance(this);
21
+ $(dropdown._menu).insertAfter(target);
22
+ }
23
+ );
20
24
  }
21
25
  function sortby(k, desc) {
22
26
  set_state_fields({ _sortby: k, _sortdesc: desc ? "on" : { unset: true } });
@@ -108,6 +112,8 @@ function get_form_record(e, select_labels) {
108
112
  .each(function () {
109
113
  if (select_labels && $(this).prop("tagName").toLowerCase() === "select")
110
114
  rec[$(this).attr("name")] = $(this).find("option:selected").text();
115
+ else if ($(this).prop("type") === "checkbox")
116
+ rec[$(this).attr("name")] = $(this).prop("checked");
111
117
  else rec[$(this).attr("name")] = $(this).val();
112
118
  });
113
119
  return rec;
@@ -323,17 +329,36 @@ function enable_codemirror(f) {
323
329
  }
324
330
 
325
331
  //https://stackoverflow.com/a/6021027
326
- function updateQueryStringParameter(uri, key, value) {
332
+ function updateQueryStringParameter(uri1, key, value) {
333
+ let hash = "";
334
+ let uri = uri1;
335
+ if (uri && uri.includes("#")) {
336
+ let uris = uri1.split("#");
337
+ hash = "#" + uris[1];
338
+ uri = uris[0];
339
+ }
340
+
327
341
  var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
328
342
  var separator = uri.indexOf("?") !== -1 ? "&" : "?";
329
343
  if (uri.match(re)) {
330
- return uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2");
344
+ return (
345
+ uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2") +
346
+ hash
347
+ );
331
348
  } else {
332
- return uri + separator + key + "=" + encodeURIComponent(value);
349
+ return uri + separator + key + "=" + encodeURIComponent(value) + hash;
333
350
  }
334
351
  }
335
352
 
336
- function removeQueryStringParameter(uri, key) {
353
+ function removeQueryStringParameter(uri1, key) {
354
+ let hash = "";
355
+ let uri = uri1;
356
+ if (uri && uri.includes("#")) {
357
+ let uris = uri1.split("#");
358
+ hash = "#" + uris[1];
359
+ uri = uris[0];
360
+ }
361
+
337
362
  var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
338
363
  var separator = uri.indexOf("?") !== -1 ? "&" : "?";
339
364
  if (uri.match(re)) {
@@ -341,7 +366,7 @@ function removeQueryStringParameter(uri, key) {
341
366
  }
342
367
  if (uri[uri.length - 1] === "?" || uri[uri.length - 1] === "&")
343
368
  uri = uri.substring(0, uri.length - 1);
344
- return uri;
369
+ return uri + hash;
345
370
  }
346
371
 
347
372
  function select_id(id) {
@@ -522,7 +547,8 @@ function globalErrorCatcher(message, source, lineno, colno, error) {
522
547
  }
523
548
 
524
549
  function press_store_button(clicked) {
525
- $(clicked).html('<i class="fas fa-spinner fa-spin"></i>');
550
+ const width = $(clicked).width();
551
+ $(clicked).html('<i class="fas fa-spinner fa-spin"></i>').width(width);
526
552
  }
527
553
 
528
554
  function ajax_modal(url, opts = {}) {
@@ -541,6 +567,10 @@ function ajax_modal(url, opts = {}) {
541
567
  </div>
542
568
  </div>
543
569
  </div>`);
570
+ } else if ($("#scmodal").hasClass("show")) {
571
+ var myModalEl = document.getElementById("scmodal");
572
+ var modal = bootstrap.Modal.getInstance(myModalEl);
573
+ modal.dispose();
544
574
  }
545
575
  if (opts.submitReload === false) $("#scmodal").addClass("no-submit-reload");
546
576
  else $("#scmodal").removeClass("no-submit-reload");
@@ -800,6 +830,26 @@ async function fill_formula_btn_click(btn, k) {
800
830
  if (k) k();
801
831
  }
802
832
 
833
+ const columnSummary = (col) => {
834
+ if (!col) return "Unknown";
835
+ switch (col.type) {
836
+ case "Field":
837
+ return `Field ${col.field_name} ${col.fieldview}`;
838
+ case "Link":
839
+ return `Link ${col.link_text}`;
840
+ case "JoinField":
841
+ return `Join ${col.join_field}`;
842
+ case "ViewLink":
843
+ return `View link ${col.view_label || col.view.split(":")[1] || ""}`;
844
+ case "Action":
845
+ return `Action ${col.action_label || col.action_name}`;
846
+ case "Aggregation":
847
+ return `${col.stat} ${col.agg_field} ${col.agg_relation}`;
848
+ default:
849
+ return "Unknown";
850
+ }
851
+ };
852
+
803
853
  /*
804
854
  https://github.com/jeffdavidgreen/bootstrap-html5-history-tabs/blob/master/bootstrap-history-tabs.js
805
855
  Copyright (c) 2015 Jeff Green
package/routes/admin.js CHANGED
@@ -36,6 +36,9 @@ const {
36
36
  button,
37
37
  span,
38
38
  p,
39
+ code,
40
+ h5,
41
+ pre,
39
42
  } = require("@saltcorn/markup/tags");
40
43
  const db = require("@saltcorn/data/db");
41
44
  const {
@@ -50,6 +53,9 @@ const {
50
53
  create_backup,
51
54
  restore,
52
55
  } = require("@saltcorn/admin-models/models/backup");
56
+ const {
57
+ runConfigurationCheck,
58
+ } = require("@saltcorn/admin-models/models/config-check");
53
59
  const fs = require("fs");
54
60
  const load_plugins = require("../load_plugins");
55
61
  const {
@@ -365,6 +371,18 @@ router.get(
365
371
  ),
366
372
  hr(),
367
373
 
374
+ a(
375
+ {
376
+ href: "/admin/configuration-check",
377
+ class: "btn btn-info",
378
+ onClick: "press_store_button(this)",
379
+ },
380
+ i({ class: "fas fa-stethoscope" }),
381
+ " ",
382
+ req.__("Configuration check")
383
+ ),
384
+ hr(),
385
+
368
386
  a(
369
387
  { href: "/admin/clear-all", class: "btn btn-danger" },
370
388
  i({ class: "fas fa-trash-alt" }),
@@ -741,6 +759,49 @@ router.get(
741
759
  })
742
760
  );
743
761
 
762
+ router.get(
763
+ "/configuration-check",
764
+ isAdmin,
765
+ error_catcher(async (req, res) => {
766
+ const { passes, errors, pass } = await runConfigurationCheck(req);
767
+ const mkError = (err) =>
768
+ div(
769
+ { class: "alert alert-danger", role: "alert" },
770
+ pre({ class: "mb-0" }, code(err))
771
+ );
772
+ res.sendWrap(req.__(`Admin`), {
773
+ above: [
774
+ {
775
+ type: "breadcrumbs",
776
+ crumbs: [
777
+ { text: req.__("Settings") },
778
+ { text: req.__("Admin"), href: "/admin" },
779
+ { text: req.__("Configuration check") },
780
+ ],
781
+ },
782
+ {
783
+ type: "card",
784
+ title: req.__("Configuration errors"),
785
+ contents: div(
786
+ pass
787
+ ? div(
788
+ { class: "alert alert-success", role: "alert" },
789
+ i({ class: "fas fa-check-circle fa-lg me-2" }),
790
+ h5({ class: "d-inline" }, "No errors detected")
791
+ )
792
+ : errors.map(mkError)
793
+ ),
794
+ },
795
+ {
796
+ type: "card",
797
+ title: req.__("Configuration checks passed"),
798
+ contents: div(pre(code(passes.join("\n")))),
799
+ },
800
+ ],
801
+ });
802
+ })
803
+ );
804
+
744
805
  /**
745
806
  * @name post/clear-all
746
807
  * @function
package/routes/fields.js CHANGED
@@ -181,6 +181,7 @@ const fieldFlow = (req) =>
181
181
  var attributes = context.attributes || {};
182
182
  attributes.default = context.default;
183
183
  attributes.summary_field = context.summary_field;
184
+ attributes.on_delete_cascade = context.on_delete_cascade;
184
185
  const {
185
186
  table_id,
186
187
  name,
@@ -377,6 +378,14 @@ const fieldFlow = (req) =>
377
378
  input_type: "select",
378
379
  options: keyfields,
379
380
  }),
381
+ new Field({
382
+ name: "on_delete_cascade",
383
+ label: req.__("On delete cascade"),
384
+ type: "Bool",
385
+ sublabel: req.__(
386
+ "If the parent row is deleted, automatically delete the child rows."
387
+ ),
388
+ }),
380
389
  ],
381
390
  });
382
391
  },
@@ -620,10 +629,14 @@ router.post(
620
629
  */
621
630
  router.post(
622
631
  "/show-calculated/:tableName/:fieldName/:fieldview",
623
- isAdmin,
624
632
  error_catcher(async (req, res) => {
625
633
  const { tableName, fieldName, fieldview } = req.params;
626
634
  const table = await Table.findOne({ name: tableName });
635
+ const role = req.user && req.user.id ? req.user.role_id : 10;
636
+ if (role > table.min_role_read) {
637
+ res.status(401).send("");
638
+ return;
639
+ }
627
640
  const fields = await table.getFields();
628
641
  const row = { ...req.body };
629
642
  readState(row, fields);
@@ -634,6 +647,10 @@ router.post(
634
647
  if (kpath.length === 2 && row[kpath[0]]) {
635
648
  const field = fields.find((f) => f.name === kpath[0]);
636
649
  const reftable = await Table.findOne({ name: field.reftable_name });
650
+ if (role > reftable.min_role_read) {
651
+ res.status(401).send("");
652
+ return;
653
+ }
637
654
  const targetField = (await reftable.getFields()).find(
638
655
  (f) => f.name === kpath[1]
639
656
  );
@@ -662,6 +679,10 @@ router.post(
662
679
  if (field.is_fkey) {
663
680
  const reftable = await Table.findOne({ name: field.reftable_name });
664
681
  if (!oldRow[ref]) break;
682
+ if (role > reftable.min_role_read) {
683
+ res.status(401).send("");
684
+ return;
685
+ }
665
686
  const q = { [reftable.pk_name]: oldRow[ref] };
666
687
  oldRow = await reftable.getRow(q);
667
688
  oldTable = reftable;
package/routes/menu.js CHANGED
@@ -10,7 +10,6 @@ const Field = require("@saltcorn/data/models/field");
10
10
  const Form = require("@saltcorn/data/models/form");
11
11
  const { isAdmin, setTenant, error_catcher } = require("./utils.js");
12
12
  const { getState } = require("@saltcorn/data/db/state");
13
- const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
14
13
  const File = require("@saltcorn/data/models/file");
15
14
  const User = require("@saltcorn/data/models/user");
16
15
  const View = require("@saltcorn/data/models/view");
package/routes/plugins.js CHANGED
@@ -14,7 +14,7 @@ const {
14
14
  post_btn,
15
15
  post_delete_btn,
16
16
  } = require("@saltcorn/markup");
17
- const { getState } = require("@saltcorn/data/db/state");
17
+ const { getState, restart_tenant } = require("@saltcorn/data/db/state");
18
18
  const Form = require("@saltcorn/data/models/form");
19
19
  const Field = require("@saltcorn/data/models/field");
20
20
  const Plugin = require("@saltcorn/data/models/plugin");
@@ -52,6 +52,7 @@ const path = require("path");
52
52
  const { get_latest_npm_version } = require("@saltcorn/data/models/config");
53
53
  const { flash_restart } = require("../markup/admin.js");
54
54
  const { sleep } = require("@saltcorn/data/utils");
55
+ const { loadAllPlugins } = require("../load_plugins");
55
56
 
56
57
  /**
57
58
  * @type {object}
@@ -148,6 +149,7 @@ const local_has_theme = (name) => {
148
149
  */
149
150
  const get_store_items = async () => {
150
151
  const installed_plugins = await Plugin.find({});
152
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
151
153
 
152
154
  const instore = await Plugin.store_plugins_available();
153
155
  const packs_available = await fetch_available_packs();
@@ -155,15 +157,18 @@ const get_store_items = async () => {
155
157
  const schema = db.getTenantSchema();
156
158
  const installed_plugin_names = installed_plugins.map((p) => p.name);
157
159
  const store_plugin_names = instore.map((p) => p.name);
158
- const plugins_item = instore.map((plugin) => ({
159
- name: plugin.name,
160
- installed: installed_plugin_names.includes(plugin.name),
161
- plugin: true,
162
- description: plugin.description,
163
- documentation_link: plugin.documentation_link,
164
- has_theme: plugin.has_theme,
165
- has_auth: plugin.has_auth,
166
- }));
160
+ const plugins_item = instore
161
+ .map((plugin) => ({
162
+ name: plugin.name,
163
+ installed: installed_plugin_names.includes(plugin.name),
164
+ plugin: true,
165
+ description: plugin.description,
166
+ documentation_link: plugin.documentation_link,
167
+ has_theme: plugin.has_theme,
168
+ has_auth: plugin.has_auth,
169
+ unsafe: plugin.unsafe,
170
+ }))
171
+ .filter((p) => !p.unsafe || isRoot);
167
172
  const local_logins = installed_plugins
168
173
  .filter((p) => !store_plugin_names.includes(p.name) && p.name !== "base")
169
174
  .map((plugin) => ({
@@ -647,15 +652,16 @@ router.get(
647
652
  error_catcher(async (req, res) => {
648
653
  const { plugin } = req.params;
649
654
  const filepath = req.params[0];
650
- const location = getState().plugin_locations[
651
- plugin.includes("@") ? plugin.split("@")[0] : plugin
652
- ];
655
+ const hasVersion = plugin.includes("@");
656
+ const location =
657
+ getState().plugin_locations[hasVersion ? plugin.split("@")[0] : plugin];
653
658
  if (location) {
654
659
  const safeFile = path
655
660
  .normalize(filepath)
656
661
  .replace(/^(\.\.(\/|\\|$))+/, "");
657
662
  const fullpath = path.join(location, "public", safeFile);
658
- if (fs.existsSync(fullpath)) res.sendFile(fullpath, { maxAge: "1d" });
663
+ if (fs.existsSync(fullpath))
664
+ res.sendFile(fullpath, { maxAge: hasVersion ? "100d" : "1d" });
659
665
  else res.status(404).send(req.__("Not found"));
660
666
  } else {
661
667
  res.status(404).send(req.__("Not found"));
@@ -841,7 +847,9 @@ router.get(
841
847
  await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
842
848
  }
843
849
  req.flash("success", req.__(`Plugins up-to-date`));
844
-
850
+ await restart_tenant(loadAllPlugins);
851
+ process.send &&
852
+ process.send({ restart_tenant: true, tenant: db.getTenantSchema() });
845
853
  res.redirect(`/plugins`);
846
854
  })
847
855
  );
@@ -957,6 +965,15 @@ router.post(
957
965
  res.redirect(`/plugins`);
958
966
  return;
959
967
  }
968
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
969
+ if (!isRoot && plugin.unsafe) {
970
+ req.flash(
971
+ "error",
972
+ req.__("Cannot install unsafe plugins on subdomain tenants")
973
+ );
974
+ res.redirect(`/plugins`);
975
+ return;
976
+ }
960
977
  delete plugin.id;
961
978
  await load_plugins.loadAndSaveNewPlugin(plugin);
962
979
  const plugin_module = getState().plugins[name];
@@ -38,6 +38,7 @@ const View = require("@saltcorn/data/models/view");
38
38
  const Workflow = require("@saltcorn/data/models/workflow");
39
39
  const User = require("@saltcorn/data/models/user");
40
40
  const Page = require("@saltcorn/data/models/page");
41
+ const db = require("@saltcorn/data/db");
41
42
 
42
43
  const { add_to_menu } = require("@saltcorn/admin-models/models/pack");
43
44
  const { editRoleForm } = require("../markup/forms.js");
@@ -534,7 +535,23 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
534
535
  if (wfres.flash) req.flash(wfres.flash[0], wfres.flash[1]);
535
536
  if (wfres.renderForm)
536
537
  res.sendWrap(
537
- req.__(`View configuration`),
538
+ {
539
+ title: req.__(`View configuration`),
540
+ headers: [
541
+ {
542
+ script: `/static_assets/${db.connectObj.version_tag}/jquery-menu-editor.min.js`,
543
+ },
544
+ {
545
+ script: `/static_assets/${db.connectObj.version_tag}/iconset-fontawesome5-3-1.min.js`,
546
+ },
547
+ {
548
+ script: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.js`,
549
+ },
550
+ {
551
+ css: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.min.css`,
552
+ },
553
+ ],
554
+ },
538
555
  wrap(renderForm(wfres.renderForm, req.csrfToken()))
539
556
  );
540
557
  else if (wfres.renderBuilder) {