@saltcorn/server 0.8.7-beta.5 → 0.8.7

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/load_plugins.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  const db = require("@saltcorn/data/db");
9
9
  const { PluginManager } = require("live-plugin-manager");
10
- const { getState } = require("@saltcorn/data/db/state");
10
+ const { getState, getRootState } = require("@saltcorn/data/db/state");
11
11
  const Plugin = require("@saltcorn/data/models/plugin");
12
12
  const fs = require("fs");
13
13
  const proc = require("child_process");
@@ -179,10 +179,23 @@ const loadAllPlugins = async () => {
179
179
  * @returns {Promise<void>}
180
180
  */
181
181
  const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
182
+ const tenants_unsafe_plugins = getRootState().getConfig(
183
+ "tenants_unsafe_plugins",
184
+ false
185
+ );
186
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
187
+ if (!isRoot && !tenants_unsafe_plugins) {
188
+ if (plugin.source !== "npm") return;
189
+ //get allowed plugins
190
+ const instore = await Plugin.store_plugins_available();
191
+ const safes = instore.filter((p) => !p.unsafe).map((p) => p.location);
192
+ if (!safes.includes(plugin.location)) return;
193
+ }
182
194
  const { version, plugin_module, location } = await requirePlugin(
183
195
  plugin,
184
196
  force
185
197
  );
198
+
186
199
  // install dependecies
187
200
  for (const loc of plugin_module.dependencies || []) {
188
201
  const existing = await Plugin.findOne({ location: loc });
package/locales/en.json CHANGED
@@ -1191,5 +1191,7 @@
1191
1191
  "Do not allow the following fields to have a value set from the query string or state": "Do not allow the following fields to have a value set from the query string or state",
1192
1192
  "Action configuration saved": "Action configuration saved",
1193
1193
  "Prevent any deletion of parent rows": "Prevent any deletion of parent rows",
1194
- "If the parent row is deleted, set key fields on child rows to null": "If the parent row is deleted, set key fields on child rows to null"
1194
+ "If the parent row is deleted, set key fields on child rows to null": "If the parent row is deleted, set key fields on child rows to null",
1195
+ "Link out?": "Link out?",
1196
+ "Show a link to open popup contents in new tab": "Show a link to open popup contents in new tab"
1195
1197
  }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.7-beta.5",
3
+ "version": "0.8.7",
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.7-beta.5",
10
- "@saltcorn/builder": "0.8.7-beta.5",
11
- "@saltcorn/data": "0.8.7-beta.5",
12
- "@saltcorn/admin-models": "0.8.7-beta.5",
13
- "@saltcorn/filemanager": "0.8.7-beta.5",
14
- "@saltcorn/markup": "0.8.7-beta.5",
15
- "@saltcorn/sbadmin2": "0.8.7-beta.5",
9
+ "@saltcorn/base-plugin": "0.8.7",
10
+ "@saltcorn/builder": "0.8.7",
11
+ "@saltcorn/data": "0.8.7",
12
+ "@saltcorn/admin-models": "0.8.7",
13
+ "@saltcorn/filemanager": "0.8.7",
14
+ "@saltcorn/markup": "0.8.7",
15
+ "@saltcorn/sbadmin2": "0.8.7",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -39,7 +39,8 @@ var erHelper = (() => {
39
39
  }px) scale(${scale || 1.0})`
40
40
  );
41
41
  };
42
-
42
+ let mouseDown = false;
43
+ let isMoving = false;
43
44
  return {
44
45
  translateY: (val) => {
45
46
  const parsed = parseTransform();
@@ -54,22 +55,43 @@ var erHelper = (() => {
54
55
  zoom: (val) => {
55
56
  const parsed = parseTransform();
56
57
  parsed.scale += val;
58
+ if (parsed.scale < 0.1) parsed.scale = 0.1;
59
+ else if (parsed.scale > 20) parsed.scale = 20;
57
60
  buildTransform(parsed);
58
61
  },
59
62
  reset: () => {
60
63
  buildTransform();
61
64
  },
62
65
  takePicture: () => {
63
- // TODO as png, so that you can download it via right click
64
- // right now, you can only print it to pdf
65
66
  const svg = $("svg[aria-roledescription='er']")[0];
66
- const DOMURL = self.URL || self.webkitURL || self;
67
- const blob = new Blob([new XMLSerializer().serializeToString(svg)], {
68
- type: "image/svg+xml;charset=utf-8",
69
- });
70
- const url = DOMURL.createObjectURL(blob);
71
- window.open(url);
72
- DOMURL.revokeObjectURL(url);
67
+ const link = document.createElement("a");
68
+ link.href = `data:image/svg+xml;base64,${btoa(
69
+ new XMLSerializer().serializeToString(svg)
70
+ )}`;
71
+ link.download = "er-diagram.svg";
72
+ link.click();
73
+ },
74
+ onWheel: (event) => {
75
+ event.preventDefault();
76
+ erHelper.zoom(-0.001 * event.deltaY);
77
+ },
78
+ onMouseDown: () => {
79
+ mouseDown = true;
80
+ isMoving = false;
81
+ },
82
+ onMouseUp: () => {
83
+ mouseDown = false;
84
+ },
85
+ onMouseMove: (event) => {
86
+ if (mouseDown) {
87
+ isMoving = true;
88
+ document.getSelection().removeAllRanges();
89
+ erHelper.translateX(event.movementX);
90
+ erHelper.translateY(event.movementY);
91
+ }
92
+ },
93
+ isTranslating: () => {
94
+ return isMoving;
73
95
  },
74
96
  };
75
97
  })();
@@ -464,3 +464,6 @@ div.unread-notify {
464
464
  grid-column: 3;
465
465
  grid-row: 3;
466
466
  }
467
+ .sc-modal-linkout {
468
+ color: inherit;
469
+ }
@@ -149,6 +149,8 @@ function pjax_to(href, e) {
149
149
  success: function (res, textStatus, request) {
150
150
  if (!inModal && !localizer.length)
151
151
  window.history.pushState({ url: href }, "", href);
152
+ if (inModal && !localizer.length)
153
+ $(".sc-modal-linkout").attr("href", href);
152
154
  setTimeout(() => {
153
155
  loadPage = true;
154
156
  }, 0);
@@ -258,11 +260,14 @@ function ensure_modal_exists_and_closed() {
258
260
  <div class="modal-content">
259
261
  <div class="modal-header">
260
262
  <h5 class="modal-title">Modal title</h5>
261
- <span class="sc-ajax-indicator-wrapper">
262
- <span class="sc-ajax-indicator ms-2" style="display: none;"><i class="fas fa-save"></i></span>
263
- </span>
264
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
265
- </button>
263
+ <div class="">
264
+ <span class="sc-ajax-indicator-wrapper">
265
+ <span class="sc-ajax-indicator ms-2" style="display: none;"><i class="fas fa-save"></i></span>
266
+ </span>
267
+ <a class="sc-modal-linkout ms-2" href="" target="_blank"><i class="fas fa-expand-alt"></i></a>
268
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
269
+ </button>
270
+ </div>
266
271
  </div>
267
272
  <div class="modal-body">
268
273
  <p>Modal body text goes here.</p>
@@ -300,6 +305,9 @@ function ajax_modal(url, opts = {}) {
300
305
  );
301
306
  if (saveIndicate) $(".sc-ajax-indicator-wrapper").show();
302
307
  else $(".sc-ajax-indicator-wrapper").hide();
308
+ var linkOut = !!request.getResponseHeader("SaltcornModalLinkOut");
309
+ if (linkOut) $(".sc-modal-linkout").show().attr("href", url);
310
+ else $(".sc-modal-linkout").hide();
303
311
  if (width) $(".modal-dialog").css("max-width", width);
304
312
  else $(".modal-dialog").css("max-width", "");
305
313
  if (title) $("#scmodal .modal-title").html(decodeURIComponent(title));
package/routes/tables.js CHANGED
@@ -463,7 +463,7 @@ const navigationPanel = () =>
463
463
  button(
464
464
  {
465
465
  class: "btn btn-primary er-up",
466
- onclick: "erHelper.translateY(-100)",
466
+ onclick: "erHelper.translateY(100)",
467
467
  },
468
468
  i({ class: "fas fa-chevron-up" })
469
469
  ),
@@ -477,7 +477,7 @@ const navigationPanel = () =>
477
477
  button(
478
478
  {
479
479
  class: "btn btn-primary er-left",
480
- onclick: "erHelper.translateX(-100)",
480
+ onclick: "erHelper.translateX(100)",
481
481
  },
482
482
  i({ class: "fas fa-chevron-left" })
483
483
  ),
@@ -488,14 +488,14 @@ const navigationPanel = () =>
488
488
  button(
489
489
  {
490
490
  class: "btn btn-primary er-right",
491
- onclick: "erHelper.translateX(100)",
491
+ onclick: "erHelper.translateX(-100)",
492
492
  },
493
493
  i({ class: "fas fa-chevron-right" })
494
494
  ),
495
495
  button(
496
496
  {
497
497
  class: "btn btn-primary er-down",
498
- onclick: "erHelper.translateY(100)",
498
+ onclick: "erHelper.translateY(-100)",
499
499
  },
500
500
  i({ class: "fas fa-chevron-down" })
501
501
  ),
@@ -542,11 +542,30 @@ router.get(
542
542
  {
543
543
  headerTag: `
544
544
  <script type="module">
545
- mermaid.initialize({ startOnLoad: false });
545
+ mermaid.initialize({
546
+ startOnLoad: false,
547
+ securityLevel: 'loose',
548
+ });
546
549
  await mermaid.run({
547
550
  querySelector: ".mermaid",
548
551
  postRenderCallback: (id) => {
549
552
  $("#" + id).css("height", "calc(100vh - 250px)");
553
+ $("#" + id + " > g").each(function(index) {
554
+ const jThis = $(this);
555
+ const id = jThis.attr("id");
556
+ if (id) {
557
+ const arr = /^entity-(.+)-(\\w+-\\w+-\\w+-\\w+-\\w+$)/.exec(id);
558
+ if (arr?.length === 3) {
559
+ const textEnt = $("#text-entity-" + arr[1] + "-" + arr[2]);
560
+ textEnt.css("cursor", "pointer");
561
+ textEnt.on("click", function () {
562
+ if (!erHelper.isTranslating()) {
563
+ window.open("/table/" + encodeURIComponent(this.innerHTML));
564
+ }
565
+ });
566
+ }
567
+ }
568
+ });
550
569
  }
551
570
  });
552
571
  </script>`,
@@ -573,17 +592,30 @@ router.get(
573
592
  contents: [
574
593
  div(
575
594
  {
595
+ id: "erd-wrapper",
576
596
  style: "height: calc(100vh - 250px);",
577
597
  class: "overflow-scroll position-relative",
578
598
  },
579
599
  screenshotPanel(),
580
600
  pre(
581
- { class: "mermaid", style: "height: calc(100vh - 250px);" },
601
+ {
602
+ class: "mermaid",
603
+ style: "height: calc(100vh - 250px); color: transparent;",
604
+ },
582
605
  buildMermaidMarkup(tables)
583
606
  ),
584
607
  navigationPanel()
585
608
  ),
586
609
  script({ src: "/relationship_diagram_utils.js" }),
610
+ script(
611
+ domReady(`
612
+ const erdWrapper = $("#erd-wrapper")[0];
613
+ erdWrapper.onwheel = erHelper.onWheel;
614
+ erdWrapper.onmousedown = erHelper.onMouseDown;
615
+ erdWrapper.onmouseup = erHelper.onMouseUp;
616
+ window.addEventListener("mousemove", erHelper.onMouseMove);
617
+ `)
618
+ ),
587
619
  ],
588
620
  },
589
621
  ],
package/routes/view.js CHANGED
@@ -83,6 +83,8 @@ router.get(
83
83
  );
84
84
  if (isModal && view.attributes?.popup_save_indicator)
85
85
  res.set("SaltcornModalSaveIndicator", `true`);
86
+ if (isModal && view.attributes?.popup_link_out)
87
+ res.set("SaltcornModalLinkOut", `true`);
86
88
  if (tic) {
87
89
  const tock = new Date();
88
90
  const ms = tock.getTime() - tic.getTime();
@@ -250,6 +250,14 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
250
250
  parent_field: "attributes",
251
251
  tab: "Popup settings",
252
252
  },
253
+ {
254
+ name: "popup_link_out",
255
+ label: req.__("Link out?"),
256
+ sublabel: req.__("Show a link to open popup contents in new tab"),
257
+ type: "Bool",
258
+ parent_field: "attributes",
259
+ tab: "Popup settings",
260
+ },
253
261
  ...(isEdit
254
262
  ? [
255
263
  new Field({
@@ -2,8 +2,13 @@ const request = require("supertest");
2
2
  const getApp = require("../app");
3
3
  const Table = require("@saltcorn/data/models/table");
4
4
  const Plugin = require("@saltcorn/data/models/plugin");
5
- const { getState } = require("@saltcorn/data/db/state");
6
-
5
+ const { getState, add_tenant } = require("@saltcorn/data/db/state");
6
+ const { install_pack } = require("@saltcorn/admin-models/models/pack");
7
+ const {
8
+ switchToTenant,
9
+ insertTenant,
10
+ create_tenant,
11
+ } = require("@saltcorn/admin-models/models/tenant");
7
12
  const {
8
13
  getAdminLoginCookie,
9
14
  itShouldRedirectUnauthToLogin,
@@ -16,6 +21,7 @@ const db = require("@saltcorn/data/db");
16
21
  const load_plugins = require("../load_plugins");
17
22
 
18
23
  beforeAll(async () => {
24
+ if (!db.isSQLite) await db.query(`drop schema if exists test101 CASCADE `);
19
25
  await resetToFixtures();
20
26
  });
21
27
  afterAll(db.close);
@@ -325,3 +331,98 @@ describe("config endpoints", () => {
325
331
  .expect(toInclude(">FooSiteName<"));
326
332
  });
327
333
  });
334
+
335
+ const plugin_pack = (plugin) => ({
336
+ tables: [],
337
+ views: [],
338
+ plugins: [
339
+ {
340
+ ...plugin,
341
+ configuration: null,
342
+ },
343
+ ],
344
+ pages: [],
345
+ roles: [],
346
+ library: [],
347
+ triggers: [],
348
+ });
349
+
350
+ describe("Tenant cannot install unsafe plugins", () => {
351
+ if (!db.isSQLite) {
352
+ it("creates a new tenant", async () => {
353
+ db.enable_multi_tenant();
354
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
355
+
356
+ await getState().setConfig("base_url", "http://example.com/");
357
+
358
+ add_tenant("test101");
359
+
360
+ await switchToTenant(
361
+ await insertTenant("test101", "foo@foo.com", ""),
362
+ "http://test101.example.com/"
363
+ );
364
+
365
+ await create_tenant({
366
+ t: "test101",
367
+ loadAndSaveNewPlugin,
368
+ plugin_loader() {},
369
+ });
370
+ });
371
+ it("can install safe plugins on tenant", async () => {
372
+ await db.runWithTenant("test101", async () => {
373
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
374
+
375
+ await install_pack(
376
+ plugin_pack({
377
+ name: "html",
378
+ source: "npm",
379
+ location: "@saltcorn/html",
380
+ }),
381
+ "Todo list",
382
+ loadAndSaveNewPlugin
383
+ );
384
+ const dbPlugin = await Plugin.findOne({ name: "html" });
385
+ expect(dbPlugin).not.toBe(null);
386
+ });
387
+ });
388
+ it("cannot install unsafe plugins on tenant", async () => {
389
+ await db.runWithTenant("test101", async () => {
390
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
391
+
392
+ await install_pack(
393
+ plugin_pack({
394
+ name: "sql-list",
395
+ source: "npm",
396
+ location: "@saltcorn/sql-list",
397
+ }),
398
+ "Todo list",
399
+ loadAndSaveNewPlugin
400
+ );
401
+ const dbPlugin = await Plugin.findOne({ name: "sql-list" });
402
+ expect(dbPlugin).toBe(null);
403
+ });
404
+ });
405
+ it("can install unsafe plugins on tenant when permitted", async () => {
406
+ await getState().setConfig("tenants_unsafe_plugins", true);
407
+ await db.runWithTenant("test101", async () => {
408
+ const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
409
+
410
+ await install_pack(
411
+ plugin_pack({
412
+ name: "sql-list",
413
+ source: "npm",
414
+ location: "@saltcorn/sql-list",
415
+ }),
416
+ "Todo list",
417
+ loadAndSaveNewPlugin
418
+ );
419
+ const dbPlugin = await Plugin.findOne({ name: "sql-list" });
420
+ expect(dbPlugin).not.toBe(null);
421
+ });
422
+ });
423
+ } else {
424
+ it("does not support tenants on SQLite", async () => {
425
+ expect(db.isSQLite).toBe(true);
426
+ });
427
+ }
428
+ });