@saltcorn/server 0.8.7-beta.3 → 0.8.7-beta.5

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/routes/diagram.js CHANGED
@@ -22,6 +22,7 @@ const { isAdmin, error_catcher } = require("./utils.js");
22
22
  const Tag = require("@saltcorn/data/models/tag");
23
23
  const Router = require("express-promise-router");
24
24
  const User = require("@saltcorn/data/models/user");
25
+ const db = require("@saltcorn/data/db");
25
26
 
26
27
  const router = new Router();
27
28
  module.exports = router;
@@ -343,16 +344,13 @@ router.get(
343
344
  }`,
344
345
  },
345
346
  {
346
- script:
347
- "https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js",
347
+ script: `/static_assets/${db.connectObj.version_tag}/popper.min.js`,
348
348
  },
349
349
  {
350
- script:
351
- "https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.22.1/cytoscape.min.js",
350
+ script: `/static_assets/${db.connectObj.version_tag}/cytoscape.min.js`,
352
351
  },
353
352
  {
354
- script:
355
- "https://cdnjs.cloudflare.com/ajax/libs/cytoscape-popper/2.0.0/cytoscape-popper.min.js",
353
+ script: `/static_assets/${db.connectObj.version_tag}/cytoscape-popper.min.js`,
356
354
  },
357
355
  ],
358
356
  });
package/routes/fields.js CHANGED
@@ -454,11 +454,11 @@ const fieldFlow = (req) =>
454
454
  required: true,
455
455
  attributes: {
456
456
  explainers: {
457
- Fail: "Prevent any deletion of parent rows",
457
+ Fail: req.__("Prevent any deletion of parent rows"),
458
458
  Cascade:
459
- "If the parent row is deleted, automatically delete the child rows.",
459
+ req.__("If the parent row is deleted, automatically delete the child rows."),
460
460
  "Set null":
461
- "If the parent row is deleted, set key fields on child rows to null",
461
+ req.__("If the parent row is deleted, set key fields on child rows to null"),
462
462
  },
463
463
  },
464
464
  sublabel: req.__(
package/routes/files.js CHANGED
@@ -69,9 +69,12 @@ router.get(
69
69
  isAdmin,
70
70
  error_catcher(async (req, res) => {
71
71
  // todo limit select from file by 10 or 20
72
- const { dir } = req.query;
72
+ const { dir, search } = req.query;
73
73
  const safeDir = File.normalise(dir || "/");
74
- const rows = await File.find({ folder: dir }, { orderBy: "filename" });
74
+ const rows = await File.find(
75
+ { folder: dir, search },
76
+ { orderBy: "filename" }
77
+ );
75
78
  const roles = await User.get_roles();
76
79
  if (safeDir && safeDir !== "/" && safeDir !== ".") {
77
80
  let dirname = path.dirname(safeDir);
@@ -383,7 +386,7 @@ router.post(
383
386
  "/upload",
384
387
  setTenant,
385
388
  error_catcher(async (req, res) => {
386
- let { folder } = req.body;
389
+ let { folder, sortBy, sortDesc } = req.body;
387
390
  let jsonResp = {};
388
391
  const min_role_upload = getState().getConfig("min_role_upload", 1);
389
392
  const role = req.user && req.user.id ? req.user.role_id : 100;
@@ -404,16 +407,11 @@ router.post(
404
407
  );
405
408
  const many = Array.isArray(f);
406
409
  file_for_redirect = many ? f[0] : f;
407
- if (!req.xhr)
408
- req.flash(
409
- "success",
410
- req.__(
411
- `File %s uploaded`,
412
- many
413
- ? f.map((fl) => text(fl.filename)).join(", ")
414
- : text(f.filename)
415
- )
416
- );
410
+ const successMsg = req.__(
411
+ `File %s uploaded`,
412
+ many ? f.map((fl) => text(fl.filename)).join(", ") : text(f.filename)
413
+ );
414
+ if (!req.xhr) req.flash("success", successMsg);
417
415
  else
418
416
  jsonResp = {
419
417
  success: {
@@ -422,16 +420,18 @@ router.post(
422
420
  url: many
423
421
  ? f.map((fl) => `/files/serve/${fl.path_to_serve}`)
424
422
  : `/files/serve/${f.path_to_serve}`,
423
+ msg: successMsg,
425
424
  },
426
425
  };
427
426
  }
428
- if (!req.xhr)
429
- res.redirect(
430
- !file_for_redirect
431
- ? "/files"
432
- : `/files?dir=${encodeURIComponent(file_for_redirect.current_folder)}`
433
- );
434
- else res.json(jsonResp);
427
+ if (!req.xhr) {
428
+ const sp = new URLSearchParams();
429
+ if (file_for_redirect) sp.append("dir", file_for_redirect.current_folder);
430
+ if (sortBy) sp.append("sortBy", sortBy);
431
+ if (sortDesc) sp.append("sortDesc", sortDesc);
432
+ const query = sp.toString();
433
+ res.redirect(`/files${query ? `?${query}` : ""}`);
434
+ } else res.json(jsonResp);
435
435
  })
436
436
  );
437
437
 
@@ -449,7 +449,7 @@ router.post(
449
449
  const { redirect } = req.query;
450
450
  const f = await File.findOne(serve_path);
451
451
  if (!f) {
452
- req.flash("error", "File not found");
452
+ req.flash("error", req.__("File not found"));
453
453
  res.redirect("/files");
454
454
  return;
455
455
  }
package/routes/list.js CHANGED
@@ -100,15 +100,7 @@ router.post(
100
100
  error_catcher(async (req, res) => {
101
101
  const { tableName, id, _version } = req.params;
102
102
  const table = Table.findOne({ name: tableName });
103
-
104
- const fields = table.getFields();
105
- const row = await db.selectOne(`${db.sqlsanitize(table.name)}__history`, {
106
- id,
107
- _version,
108
- });
109
- var r = {};
110
- fields.forEach((f) => (r[f.name] = row[f.name]));
111
- await table.updateRow(r, +id);
103
+ await table.restore_row_version(id, _version);
112
104
  req.flash("success", req.__("Version %s restored", _version));
113
105
  res.redirect(`/list/_versions/${table.name}/${id}`);
114
106
  })
@@ -70,7 +70,7 @@ router.get(
70
70
  {
71
71
  type: "card",
72
72
  contents: [
73
- "Receive notifications by:",
73
+ req.__("Receive notifications by:"),
74
74
  renderForm(notificationSettingsForm(), req.csrfToken()),
75
75
  ],
76
76
  },
package/routes/plugins.js CHANGED
@@ -526,7 +526,8 @@ const plugin_store_html = (items, req) => {
526
526
  contents: div(
527
527
  { class: "d-flex justify-content-between" },
528
528
  storeNavPills(req),
529
- div(search_bar("q", req.query.q || "", { stateField: "q" })),
529
+ div(search_bar("q", req.query.q || "",
530
+ { placeHolder: req.__("Search for..."), stateField: "q" })),
530
531
  div(store_actions_dropdown(req))
531
532
  ),
532
533
  },
@@ -825,7 +826,7 @@ router.get(
825
826
  pkgjson = require(path.join(mod.location, "package.json"));
826
827
 
827
828
  if (!plugin_db) {
828
- req.flash("warning", "Module not found");
829
+ req.flash("warning", req.__("Module not found"));
829
830
  res.redirect("/plugins");
830
831
  return;
831
832
  }
@@ -1035,7 +1036,7 @@ router.post(
1035
1036
 
1036
1037
  const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
1037
1038
  if (!plugin) {
1038
- req.flash("warning", "Module not found");
1039
+ req.flash("warning", req.__("Module not found"));
1039
1040
  res.redirect("/plugins");
1040
1041
  return;
1041
1042
  }
package/routes/tables.js CHANGED
@@ -42,6 +42,7 @@ const {
42
42
  domReady,
43
43
  code,
44
44
  pre,
45
+ button,
45
46
  } = require("@saltcorn/markup/tags");
46
47
  const stringify = require("csv-stringify");
47
48
  const TableConstraint = require("@saltcorn/data/models/table_constraints");
@@ -54,8 +55,11 @@ const {
54
55
  const { getState } = require("@saltcorn/data/db/state");
55
56
  const { cardHeaderTabs } = require("@saltcorn/markup/layout_utils");
56
57
  const { tablesList } = require("./common_lists");
57
- const { InvalidConfiguration } = require("@saltcorn/data/utils");
58
- const { sleep } = require("@saltcorn/data/utils");
58
+ const {
59
+ InvalidConfiguration,
60
+ removeAllWhiteSpace,
61
+ } = require("@saltcorn/data/utils");
62
+ const { EOL } = require("os");
59
63
 
60
64
  const path = require("path");
61
65
  /**
@@ -421,6 +425,101 @@ router.post(
421
425
  })
422
426
  );
423
427
 
428
+ const indentString = (str, indent) => `${" ".repeat(indent)}${str}`;
429
+
430
+ const srcCardinality = (field) => (field.required ? "||" : "|o");
431
+
432
+ const buildTableMarkup = (table) => {
433
+ const fields = table.getFields();
434
+ const members = fields
435
+ // .filter((f) => !f.reftable_name)
436
+ .map((f) =>
437
+ indentString(`${removeAllWhiteSpace(f.type_name)} ${f.name}`, 6)
438
+ )
439
+ .join(EOL);
440
+ const keys = table
441
+ .getForeignKeys()
442
+ .map((f) =>
443
+ indentString(
444
+ `"${table.name}"${srcCardinality(f)}--|| "${f.reftable_name}" : "${
445
+ f.name
446
+ }"`,
447
+ 2
448
+ )
449
+ )
450
+ .join(EOL);
451
+ return `${keys}
452
+ "${table.name}" {${EOL}${members}${EOL} }`;
453
+ };
454
+
455
+ const buildMermaidMarkup = (tables) => {
456
+ const lines = tables.map((table) => buildTableMarkup(table)).join(EOL);
457
+ return `${indentString("erDiagram", 2)}${EOL}${lines}`;
458
+ };
459
+
460
+ const navigationPanel = () =>
461
+ div(
462
+ { class: "er-navigation-panel" },
463
+ button(
464
+ {
465
+ class: "btn btn-primary er-up",
466
+ onclick: "erHelper.translateY(-100)",
467
+ },
468
+ i({ class: "fas fa-chevron-up" })
469
+ ),
470
+ button(
471
+ {
472
+ class: "btn btn-primary er-zoom-in",
473
+ onclick: "erHelper.zoom(0.1)",
474
+ },
475
+ i({ class: "fas fa-search-plus" })
476
+ ),
477
+ button(
478
+ {
479
+ class: "btn btn-primary er-left",
480
+ onclick: "erHelper.translateX(-100)",
481
+ },
482
+ i({ class: "fas fa-chevron-left" })
483
+ ),
484
+ button(
485
+ { class: "btn btn-primary er-reset", onclick: "erHelper.reset()" },
486
+ i({ class: "fas fa-sync-alt" })
487
+ ),
488
+ button(
489
+ {
490
+ class: "btn btn-primary er-right",
491
+ onclick: "erHelper.translateX(100)",
492
+ },
493
+ i({ class: "fas fa-chevron-right" })
494
+ ),
495
+ button(
496
+ {
497
+ class: "btn btn-primary er-down",
498
+ onclick: "erHelper.translateY(100)",
499
+ },
500
+ i({ class: "fas fa-chevron-down" })
501
+ ),
502
+ button(
503
+ {
504
+ class: "btn btn-primary er-zoom-out",
505
+ onclick: "erHelper.zoom(-0.1)",
506
+ },
507
+ i({ class: "fas fa-search-minus" })
508
+ )
509
+ );
510
+
511
+ const screenshotPanel = () =>
512
+ div(
513
+ { class: "er-screenshot-panel" },
514
+ button(
515
+ {
516
+ class: "btn btn-primary",
517
+ onclick: "erHelper.takePicture()",
518
+ },
519
+ i({ class: "fas fa-camera" })
520
+ )
521
+ );
522
+
424
523
  /**
425
524
  * Show Relational Diagram (get)
426
525
  * @name get/relationship-diagram
@@ -433,34 +532,24 @@ router.get(
433
532
  isAdmin,
434
533
  error_catcher(async (req, res) => {
435
534
  const tables = await Table.find_with_external({}, { orderBy: "name" });
436
- const edges = [];
437
- for (const table of tables) {
438
- const fields = table.getFields();
439
- for (const field of fields) {
440
- if (field.reftable_name)
441
- edges.push({
442
- from: table.name,
443
- to: field.reftable_name,
444
- arrows: "to",
445
- });
446
- }
447
- }
448
- const data = {
449
- nodes: tables.map((t) => ({
450
- id: t.name,
451
- label: `<b>${t.name}</b>\n${t.fields
452
- .map((f) => `${f.name} : ${f.pretty_type}`)
453
- .join("\n")}`,
454
- title: t.description ? t.description : t.name,
455
- })),
456
- edges,
457
- };
458
535
  res.sendWrap(
459
536
  {
460
537
  title: req.__("Tables"),
461
538
  headers: [
462
539
  {
463
- script: `/static_assets/${db.connectObj.version_tag}/vis-network.min.js`,
540
+ script: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
541
+ },
542
+ {
543
+ headerTag: `
544
+ <script type="module">
545
+ mermaid.initialize({ startOnLoad: false });
546
+ await mermaid.run({
547
+ querySelector: ".mermaid",
548
+ postRenderCallback: (id) => {
549
+ $("#" + id).css("height", "calc(100vh - 250px)");
550
+ }
551
+ });
552
+ </script>`,
464
553
  },
465
554
  ],
466
555
  },
@@ -482,29 +571,19 @@ router.get(
482
571
  },
483
572
  ]),
484
573
  contents: [
485
- div({ id: "erdvis" }),
486
- script(
487
- domReady(`
488
- var container = document.getElementById('erdvis');
489
- var data = ${JSON.stringify(data)};
490
- var options = {
491
- edges: {length: 250},
492
- nodes: {
493
- font: { align: 'left', multi: "html", size: 20 },
494
- shape: "box"
495
- },
496
- physics: {
497
- // Even though it's disabled the options still apply to network.stabilize().
498
- enabled: false,
499
- solver: "repulsion",
500
- repulsion: {
501
- nodeDistance: 100 // Put more distance between the nodes.
502
- }
503
- }
504
- };
505
- var network = new vis.Network(container, data, options);
506
- network.stabilize();`)
574
+ div(
575
+ {
576
+ style: "height: calc(100vh - 250px);",
577
+ class: "overflow-scroll position-relative",
578
+ },
579
+ screenshotPanel(),
580
+ pre(
581
+ { class: "mermaid", style: "height: calc(100vh - 250px);" },
582
+ buildMermaidMarkup(tables)
583
+ ),
584
+ navigationPanel()
507
585
  ),
586
+ script({ src: "/relationship_diagram_utils.js" }),
508
587
  ],
509
588
  },
510
589
  ],
@@ -1135,7 +1214,7 @@ router.get(
1135
1214
  error_catcher(async (req, res) => {
1136
1215
  const { name } = req.params;
1137
1216
  const table = Table.findOne({ name });
1138
- const rows = await table.getRows({}, { orderBy: "id", orderDesc: true });
1217
+ const rows = await table.getRows({}, { orderBy: "id" });
1139
1218
  res.setHeader("Content-Type", "text/csv");
1140
1219
  res.setHeader("Content-Disposition", `attachment; filename="${name}.csv"`);
1141
1220
  res.setHeader("Cache-Control", "no-cache");
@@ -34,6 +34,9 @@ test("updateQueryStringParameter", () => {
34
34
  expect(removeQueryStringParameter("/foo?name=Bar&age=45", "age")).toBe(
35
35
  "/foo?name=Bar"
36
36
  );
37
+ expect(
38
+ removeQueryStringParameter("/foo?name=Baz&name=Foo&age=45", "name")
39
+ ).not.toContain("name");
37
40
  expect(
38
41
  updateQueryStringParameter("/foo", "publisher.publisher->name", "AK")
39
42
  ).toBe("/foo?publisher.publisher->name=AK");
@@ -7,9 +7,9 @@ const {
7
7
  itShouldRedirectUnauthToLogin,
8
8
  toInclude,
9
9
  toSucceed,
10
- toNotInclude,
11
10
  resetToFixtures,
12
11
  toSucceedWithImage,
12
+ respondJsonWith,
13
13
  } = require("../auth/testhelp");
14
14
  const db = require("@saltcorn/data/db");
15
15
  const fs = require("fs").promises;
@@ -18,7 +18,17 @@ const File = require("@saltcorn/data/models/file");
18
18
  const Field = require("@saltcorn/data/models/field");
19
19
  const Table = require("@saltcorn/data/models/table");
20
20
  const View = require("@saltcorn/data/models/view");
21
- const { table } = require("console");
21
+ const { existsSync } = require("fs");
22
+
23
+ const createTestFile = async (folder, name, mimetype, content) => {
24
+ if (
25
+ !existsSync(
26
+ path.join(db.connectObj.file_store, db.getTenantSchema(), folder, name)
27
+ )
28
+ ) {
29
+ await File.from_contents(name, mimetype, content, 1, 100, folder);
30
+ }
31
+ };
22
32
 
23
33
  beforeAll(async () => {
24
34
  await resetToFixtures();
@@ -47,6 +57,33 @@ beforeAll(async () => {
47
57
  1,
48
58
  100
49
59
  );
60
+
61
+ await File.new_folder(path.join("_sc_test_subfolder_one", "subsubfolder"));
62
+ await createTestFile(
63
+ "_sc_test_subfolder_one",
64
+ "foo_image.png",
65
+ "image/png",
66
+ "imagecontent"
67
+ );
68
+ await createTestFile(
69
+ "_sc_test_subfolder_one",
70
+ "bar_image.png",
71
+ "image/png",
72
+ "imagecontent"
73
+ );
74
+ await createTestFile(
75
+ path.join("_sc_test_subfolder_one", "subsubfolder"),
76
+ "bar_image.png",
77
+ "image/png",
78
+ "imagecontent"
79
+ );
80
+ await File.new_folder("_sc_test_subfolder_two");
81
+ await createTestFile(
82
+ "_sc_test_subfolder_two",
83
+ "foo_image.png",
84
+ "image/png",
85
+ "imagecontent"
86
+ );
50
87
  });
51
88
  afterAll(db.close);
52
89
 
@@ -146,6 +183,90 @@ describe("files admin", () => {
146
183
 
147
184
  .expect(toRedirect("/files?dir=."));
148
185
  });
186
+ it("search files by name", async () => {
187
+ const app = await getApp({ disableCsrf: true });
188
+ const loginCookie = await getAdminLoginCookie();
189
+ const checkFiles = (files, expecteds) =>
190
+ files.length === expecteds.length &&
191
+ expecteds.every(({ filename, location }) =>
192
+ files.find(
193
+ (file) => file.filename === filename && file.location === location
194
+ )
195
+ );
196
+ const searchTestHelper = async (dir, search, expected) => {
197
+ await request(app)
198
+ .get("/files")
199
+ .query({ dir, search })
200
+ .set("X-Requested-With", "XMLHttpRequest")
201
+ .set("Cookie", loginCookie)
202
+ .expect(
203
+ respondJsonWith(200, (data) => checkFiles(data.files, expected))
204
+ );
205
+ };
206
+
207
+ await searchTestHelper("/", "foo", [
208
+ {
209
+ filename: "foo_image.png",
210
+ location: path.join("_sc_test_subfolder_one", "foo_image.png"),
211
+ },
212
+ {
213
+ filename: "foo_image.png",
214
+ location: path.join("_sc_test_subfolder_two", "foo_image.png"),
215
+ },
216
+ ]);
217
+ await searchTestHelper("_sc_test_subfolder_two", "foo", [
218
+ {
219
+ filename: "foo_image.png",
220
+ location: path.join("_sc_test_subfolder_two", "foo_image.png"),
221
+ },
222
+ {
223
+ filename: "..",
224
+ location: "",
225
+ },
226
+ ]);
227
+ await searchTestHelper("/", "bar", [
228
+ {
229
+ filename: "bar_image.png",
230
+ location: path.join("_sc_test_subfolder_one", "bar_image.png"),
231
+ },
232
+ {
233
+ filename: "bar_image.png",
234
+ location: path.join(
235
+ "_sc_test_subfolder_one",
236
+ "subsubfolder",
237
+ "bar_image.png"
238
+ ),
239
+ },
240
+ ]);
241
+ await searchTestHelper(
242
+ path.join("_sc_test_subfolder_one", "subsubfolder"),
243
+ "foo",
244
+ [
245
+ {
246
+ filename: "..",
247
+ location: "_sc_test_subfolder_one",
248
+ },
249
+ ]
250
+ );
251
+ await searchTestHelper(
252
+ path.join("_sc_test_subfolder_one", "subsubfolder"),
253
+ "bar",
254
+ [
255
+ {
256
+ filename: "..",
257
+ location: "_sc_test_subfolder_one",
258
+ },
259
+ {
260
+ filename: "bar_image.png",
261
+ location: path.join(
262
+ "_sc_test_subfolder_one",
263
+ "subsubfolder",
264
+ "bar_image.png"
265
+ ),
266
+ },
267
+ ]
268
+ );
269
+ });
149
270
  });
150
271
  describe("files edit", () => {
151
272
  it("creates table and view", async () => {