@saltcorn/server 0.9.1-beta.5 → 0.9.1-beta.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/auth/routes.js CHANGED
@@ -877,17 +877,17 @@ router.post(
877
877
 
878
878
  const unsuitableEmailPassword = async (urecord) => {
879
879
  const { email, password, passwordRepeat } = urecord;
880
- if (!email || !password) {
880
+ if (email == "" || !password) {
881
881
  req.flash("danger", req.__("E-mail and password required"));
882
882
  res.redirect("/auth/signup");
883
883
  return true;
884
884
  }
885
- if (email.length > 127) {
885
+ if (email && email.length > 127) {
886
886
  req.flash("danger", req.__("E-mail too long"));
887
887
  res.redirect("/auth/signup");
888
888
  return true;
889
889
  }
890
- if (!User.valid_email(email)) {
890
+ if (email && !User.valid_email(email)) {
891
891
  req.flash("danger", req.__("Not a valid e-mail address"));
892
892
  res.redirect("/auth/signup");
893
893
  return true;
@@ -905,9 +905,7 @@ router.post(
905
905
  res.redirect("/auth/signup");
906
906
  return true;
907
907
  }
908
-
909
- const us = await User.find({ email });
910
- if (us.length > 0) {
908
+ if (await User.matches_existing_user(urecord)) {
911
909
  req.flash("danger", req.__("Account already exists"));
912
910
  res.redirect("/auth/signup");
913
911
  return true;
@@ -16,6 +16,13 @@ outbound emails would have a trigger with When = Insert and Action = send_email
16
16
  **Update**: run this action when changes are made to an existing row. The old row can
17
17
  be accessed with the `old_row` variable.
18
18
 
19
+ **Validate**: run before inserts or updates. If the action returns `error` (for example,
20
+ `run_js_code` code: `return {error: "Invalid row"}`), the insert/update is aborted. If the
21
+ trigger returns `set_fields` (for example, `run_js_code` code: `return {set_fields: {full_name: "Unknown"}}`)
22
+ these values are inserted in row.
23
+
24
+ Guaranteed to run before any Insert or Update triggers
25
+
19
26
  **Delete**: run this action when a row is deleted
20
27
 
21
28
  ## Periodic events
@@ -87,20 +87,7 @@ Example: `return { error: "Invalid command!" }`
87
87
 
88
88
  If this is triggered by an Edit view with the SubmitWithAjax,
89
89
  halt navigation and stay on page. This can be used for complex validation logic,
90
- When added as an Insert or Update trigger. If you delete the inserted row, You
91
- may also need to clear the returned id in order to allow the user to continue editing.
92
-
93
- Example:
94
-
95
- ```
96
- if(amount>cash_on_hand) {
97
- await table.deleteRows({ id })
98
- return {
99
- error: "Invalid order!",
100
- id: null
101
- }
102
- }
103
- ```
90
+ when added as a Validate trigger.
104
91
 
105
92
  #### `goto`
106
93
 
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.1-beta.5",
3
+ "version": "0.9.1-beta.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
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "0.9.1-beta.5",
11
- "@saltcorn/builder": "0.9.1-beta.5",
12
- "@saltcorn/data": "0.9.1-beta.5",
13
- "@saltcorn/admin-models": "0.9.1-beta.5",
14
- "@saltcorn/filemanager": "0.9.1-beta.5",
15
- "@saltcorn/markup": "0.9.1-beta.5",
16
- "@saltcorn/sbadmin2": "0.9.1-beta.5",
10
+ "@saltcorn/base-plugin": "0.9.1-beta.7",
11
+ "@saltcorn/builder": "0.9.1-beta.7",
12
+ "@saltcorn/data": "0.9.1-beta.7",
13
+ "@saltcorn/admin-models": "0.9.1-beta.7",
14
+ "@saltcorn/filemanager": "0.9.1-beta.7",
15
+ "@saltcorn/markup": "0.9.1-beta.7",
16
+ "@saltcorn/sbadmin2": "0.9.1-beta.7",
17
17
  "@socket.io/cluster-adapter": "^0.2.1",
18
18
  "@socket.io/sticky": "^1.0.1",
19
19
  "adm-zip": "0.5.10",
@@ -360,7 +360,7 @@ function submitWithAjax(e) {
360
360
  saveAndContinue(e, (res) => {
361
361
  if (res && res.responseJSON && res.responseJSON.url_when_done)
362
362
  window.location.href = res.responseJSON.url_when_done;
363
- if (res && res.responseJSON && res.responseJSON.error)
363
+ if (res && res.responseJSON && res.responseJSON.error && res.status < 300)
364
364
  notifyAlert({ type: "danger", text: res.responseJSON.error });
365
365
  });
366
366
  }
package/routes/actions.js CHANGED
@@ -145,7 +145,7 @@ const triggerForm = async (req, trigger) => {
145
145
  .filter(([k, v]) => v.hasChannel)
146
146
  .map(([k, v]) => k);
147
147
  const allActions = actions.map((t) => t.name);
148
- const table_triggers = ["Insert", "Update", "Delete"];
148
+ const table_triggers = ["Insert", "Update", "Delete", "Validate"];
149
149
  const action_options = {};
150
150
  const actionsNotRequiringRow = actions
151
151
  .filter((a) => !a.requireRow)
@@ -344,11 +344,7 @@ const getPageList = (rows, roles, req, { tagId, domId, showList } = {}) => {
344
344
  },
345
345
  {
346
346
  label: req.__("Edit"),
347
- key: (r) =>
348
- link(
349
- `/pageedit/${!r.html_file ? "edit" : "edit-properties"}/${r.name}`,
350
- req.__(!r.html_file ? "Edit" : "Edit properties")
351
- ),
347
+ key: (r) => link(`/pageedit/edit/${r.name}`, req.__("Edit")),
352
348
  },
353
349
  !tagId
354
350
  ? {
package/routes/page.js CHANGED
@@ -16,6 +16,7 @@ const {
16
16
  isAdmin,
17
17
  sendHtmlFile,
18
18
  } = require("../routes/utils.js");
19
+ const { isTest } = require("@saltcorn/data/utils");
19
20
  const { add_edit_bar } = require("../markup/admin.js");
20
21
  const { traverseSync } = require("@saltcorn/data/models/layout");
21
22
  const { run_action_column } = require("@saltcorn/data/plugin-helper");
@@ -52,12 +53,13 @@ router.get(
52
53
  const title = scan_for_page_title(contents, db_page.title);
53
54
  const tock = new Date();
54
55
  const ms = tock.getTime() - tic.getTime();
55
- Trigger.emitEvent("PageLoad", null, req.user, {
56
- text: req.__("Page '%s' was loaded", pagename),
57
- type: "page",
58
- name: pagename,
59
- render_time: ms,
60
- });
56
+ if (!isTest())
57
+ Trigger.emitEvent("PageLoad", null, req.user, {
58
+ text: req.__("Page '%s' was loaded", pagename),
59
+ type: "page",
60
+ name: pagename,
61
+ render_time: ms,
62
+ });
61
63
  if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
62
64
  else
63
65
  res.sendWrap(
@@ -9,7 +9,7 @@ const View = require("@saltcorn/data/models/view");
9
9
  const Field = require("@saltcorn/data/models/field");
10
10
  const Table = require("@saltcorn/data/models/table");
11
11
  const Page = require("@saltcorn/data/models/page");
12
- const { div, a } = require("@saltcorn/markup/tags");
12
+ const { div, a, iframe, script } = require("@saltcorn/markup/tags");
13
13
  const { getState } = require("@saltcorn/data/db/state");
14
14
  const User = require("@saltcorn/data/models/user");
15
15
  const Workflow = require("@saltcorn/data/models/workflow");
@@ -41,6 +41,7 @@ const {
41
41
  const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
42
42
  const Library = require("@saltcorn/data/models/library");
43
43
  const path = require("path");
44
+ const fsp = require("fs").promises;
44
45
 
45
46
  /**
46
47
  * @type {object}
@@ -431,6 +432,116 @@ router.post(
431
432
  );
432
433
 
433
434
  /**
435
+ * open the builder
436
+ * @param {*} req
437
+ * @param {*} res
438
+ * @param {*} page
439
+ */
440
+ const getEditNormalPage = async (req, res, page) => {
441
+ // set fixed states in page directly for legacy builds
442
+ traverseSync(page.layout, {
443
+ view(s) {
444
+ if (s.state === "fixed" && !s.configuration) {
445
+ const fs = page.fixed_states[s.name];
446
+ if (fs) s.configuration = fs;
447
+ }
448
+ },
449
+ });
450
+ const options = await pageBuilderData(req, page);
451
+ const builderData = {
452
+ options,
453
+ context: page,
454
+ layout: page.layout,
455
+ mode: "page",
456
+ version_tag: db.connectObj.version_tag,
457
+ };
458
+ res.sendWrap(
459
+ req.__(`%s configuration`, page.name),
460
+ wrap(renderBuilder(builderData, req.csrfToken()), true, req, page)
461
+ );
462
+ };
463
+
464
+ /**
465
+ * open a file editor with an iframe preview
466
+ * @param {*} req
467
+ * @param {*} res
468
+ * @param {*} page
469
+ */
470
+ const getEditPageWithHtmlFile = async (req, res, page) => {
471
+ const htmlFile = page.html_file;
472
+ const iframeId = "page_preview_iframe";
473
+ const updateBttnId = "addnUpdBtn";
474
+ const file = await File.findOne(htmlFile);
475
+ if (!file) {
476
+ req.flash("error", req.__("File not found"));
477
+ return res.redirect(`/pageedit`);
478
+ }
479
+ const editForm = new Form({
480
+ action: `/pageedit/edit/${encodeURIComponent(page.name)}`,
481
+ fields: [
482
+ {
483
+ name: "code",
484
+ form_name: "code",
485
+ label: "Code",
486
+ input_type: "code",
487
+ attributes: { mode: "text/html" },
488
+ validator(s) {
489
+ return true;
490
+ },
491
+ },
492
+ ],
493
+ values: {
494
+ code: await fsp.readFile(file.location, "utf8"),
495
+ },
496
+ onChange: `document.getElementById('${updateBttnId}').disabled = false;`,
497
+ additionalButtons: [
498
+ {
499
+ label: req.__("Update"),
500
+ id: updateBttnId,
501
+ class: "btn btn-primary",
502
+ onclick: `saveAndContinue(this, () => {
503
+ document.getElementById('${iframeId}').contentWindow.location.reload();
504
+ document.getElementById('${updateBttnId}').disabled = true;
505
+ })`,
506
+ disabled: true,
507
+ },
508
+ ],
509
+ submitLabel: req.__("Finish") + " &raquo;",
510
+ });
511
+ res.sendWrap(req.__("Edit %s", page.title), {
512
+ above: [
513
+ {
514
+ type: "card",
515
+ title: "Edit",
516
+ titleAjaxIndicator: true,
517
+ contents: [renderForm(editForm, req.csrfToken())],
518
+ },
519
+ {
520
+ type: "card",
521
+ title: "Preview",
522
+ contents: [
523
+ iframe({
524
+ id: iframeId,
525
+ src: `/files/serve/${encodeURIComponent(htmlFile)}`,
526
+ }),
527
+ script(`
528
+ const iframe = document.getElementById("${iframeId}");
529
+ iframe.onload = () => {
530
+ const _iframe = document.getElementById("${iframeId}");
531
+ if (_iframe.contentWindow.document.body) {
532
+ _iframe.width = _iframe.contentWindow.document.body.scrollWidth;
533
+ _iframe.height = _iframe.contentWindow.document.body.scrollHeight;
534
+ }
535
+ }`),
536
+ ],
537
+ },
538
+ ],
539
+ });
540
+ };
541
+
542
+ /**
543
+ * for normal pages, open the builder
544
+ * for pages with a fixed html file, open a file editor with an iframe preview
434
545
  * @name get/edit/:pagename
435
546
  * @function
436
547
  * @memberof module:routes/pageedit~pageeditRouter
@@ -446,27 +557,8 @@ router.get(
446
557
  req.flash("error", req.__(`Page %s not found`, pagename));
447
558
  res.redirect(`/pageedit`);
448
559
  } else {
449
- // set fixed states in page directly for legacy builds
450
- traverseSync(page.layout, {
451
- view(s) {
452
- if (s.state === "fixed" && !s.configuration) {
453
- const fs = page.fixed_states[s.name];
454
- if (fs) s.configuration = fs;
455
- }
456
- },
457
- });
458
- const options = await pageBuilderData(req, page);
459
- const builderData = {
460
- options,
461
- context: page,
462
- layout: page.layout,
463
- mode: "page",
464
- version_tag: db.connectObj.version_tag,
465
- };
466
- res.sendWrap(
467
- req.__(`%s configuration`, page.name),
468
- wrap(renderBuilder(builderData, req.csrfToken()), true, req, page)
469
- );
560
+ if (!page.html_file) await getEditNormalPage(req, res, page);
561
+ else await getEditPageWithHtmlFile(req, res, page);
470
562
  }
471
563
  })
472
564
  );
@@ -495,13 +587,33 @@ router.post(
495
587
  await Page.update(page.id, {
496
588
  layout: decodeURIComponent(req.body.layout),
497
589
  });
498
-
499
590
  req.flash("success", req.__(`Page %s saved`, pagename));
500
591
  res.redirect(redirectTarget);
592
+ } else if (req.body.code) {
593
+ try {
594
+ if (!page.html_file) throw new Error(req.__("File not found"));
595
+ const file = await File.findOne(page.html_file);
596
+ if (!file) throw new Error(req.__("File not found"));
597
+ await fsp.writeFile(file.location, req.body.code);
598
+ if (!req.xhr) {
599
+ req.flash("success", req.__(`Page %s saved`, pagename));
600
+ res.redirect(redirectTarget);
601
+ } else res.json({ okay: true });
602
+ } catch (error) {
603
+ getState().log(2, `POST /edit/${pagename}: '${error.message}'`);
604
+ req.flash(
605
+ "error",
606
+ `${req.__("Error")}: ${error.message || req.__("An error occurred")}`
607
+ );
608
+ if (!req.xhr) res.redirect(redirectTarget);
609
+ else res.json({ error: error.message });
610
+ }
501
611
  } else {
612
+ getState().log(2, `POST /edit/${pagename}: '${req.body}'`);
502
613
  req.flash("error", req.__(`Error processing page`));
503
614
  res.redirect(redirectTarget);
504
615
  }
616
+ getState().log(5, `POST /edit/${pagename}: Success`);
505
617
  })
506
618
  );
507
619
 
package/routes/view.js CHANGED
@@ -18,7 +18,7 @@ const {
18
18
  setTenant,
19
19
  } = require("../routes/utils.js");
20
20
  const { add_edit_bar } = require("../markup/admin.js");
21
- const { InvalidConfiguration } = require("@saltcorn/data/utils");
21
+ const { InvalidConfiguration, isTest } = require("@saltcorn/data/utils");
22
22
  const { getState } = require("@saltcorn/data/db/state");
23
23
 
24
24
  /**
@@ -88,12 +88,13 @@ router.get(
88
88
  res.set("SaltcornModalLinkOut", `true`);
89
89
  const tock = new Date();
90
90
  const ms = tock.getTime() - tic.getTime();
91
- Trigger.emitEvent("PageLoad", null, req.user, {
92
- text: req.__("View '%s' was loaded", viewname),
93
- type: "view",
94
- name: viewname,
95
- render_time: ms,
96
- });
91
+ if (!isTest())
92
+ Trigger.emitEvent("PageLoad", null, req.user, {
93
+ text: req.__("View '%s' was loaded", viewname),
94
+ type: "view",
95
+ name: viewname,
96
+ render_time: ms,
97
+ });
97
98
  if (typeof contents === "object" && contents.goto)
98
99
  res.redirect(contents.goto);
99
100
  else
@@ -16,6 +16,7 @@ const Page = require("@saltcorn/data/models/page");
16
16
  const File = require("@saltcorn/data/models/file");
17
17
  const { existsSync } = require("fs");
18
18
  const { join } = require("path");
19
+ const fs = require("fs");
19
20
 
20
21
  let htmlFile = null;
21
22
 
@@ -26,24 +27,20 @@ const prepHtmlFiles = async () => {
26
27
  db.getTenantSchema(),
27
28
  folder
28
29
  );
30
+ const html = `<html><head><title>Landing page</title></head><body><h1>${content}</h1></body></html>`;
29
31
  if (!existsSync(scFolder)) await File.new_folder(folder);
30
32
  if (!existsSync(join(scFolder, name))) {
31
- return await File.from_contents(
32
- name,
33
- "text/html",
34
- `<html><head><title>Landing page</title></head><body><h1>${content}</h1></body></html>`,
35
- 1,
36
- 1,
37
- folder
38
- );
33
+ return await File.from_contents(name, "text/html", html, 1, 1, folder);
39
34
  } else {
40
35
  const file = await File.from_file_on_disk(name, scFolder);
36
+ fs.writeFileSync(file.location, html);
41
37
  file.location = File.absPathToServePath(file.location);
42
38
  return file;
43
39
  }
44
40
  };
45
41
  htmlFile = await createFile("/", "fixed_page.html", "Land here");
46
42
  await createFile("/subfolder", "fixed_page2.html", "Or Land here");
43
+ await createFile("/", "test.html", "page with fixed html");
47
44
  };
48
45
 
49
46
  beforeAll(async () => {
@@ -135,6 +132,11 @@ describe("page create", () => {
135
132
 
136
133
  it("does not find the html file for staff or public", async () => {
137
134
  const app = await getApp({ disableCsrf: true });
135
+ await request(app)
136
+ .post(`/files/setrole/fixed_page.html`)
137
+ .set("Cookie", await getAdminLoginCookie())
138
+ .send("role=1")
139
+ .expect(toRedirect("/files?dir=."));
138
140
  const loginCookie = await getStaffLoginCookie();
139
141
  await request(app)
140
142
  .get("/page/new_page_with_html_file")
@@ -247,6 +249,33 @@ describe("pageedit", () => {
247
249
  .set("Cookie", loginCookie)
248
250
  .expect(toInclude("<script>builder.renderBuilder"));
249
251
  });
252
+ it("show editor with html file", async () => {
253
+ const app = await getApp({ disableCsrf: true });
254
+ const loginCookie = await getAdminLoginCookie();
255
+ await request(app)
256
+ .get("/pageedit/edit/page_with_html_file")
257
+ .set("Cookie", loginCookie)
258
+ .expect(toInclude(`<textarea mode="text/html"`));
259
+ });
260
+ it("edit editor with html file", async () => {
261
+ const app = await getApp({ disableCsrf: true });
262
+ const loginCookie = await getAdminLoginCookie();
263
+ const newHtml =
264
+ "<html><head><title>title</title></head><body><h1>new html</h1></body</html>";
265
+ await request(app)
266
+ .get("/page/page_with_html_file")
267
+ .set("Cookie", loginCookie)
268
+ .expect(toInclude("page with fixed html"));
269
+ await request(app)
270
+ .post("/pageedit/edit/page_with_html_file")
271
+ .set("Cookie", loginCookie)
272
+ .send("code=" + encodeURIComponent(newHtml))
273
+ .expect(toRedirect("/pageedit"));
274
+ await request(app)
275
+ .get("/page/page_with_html_file")
276
+ .set("Cookie", loginCookie)
277
+ .expect(toInclude("new html"));
278
+ });
250
279
 
251
280
  it("sets root page", async () => {
252
281
  const app = await getApp({ disableCsrf: true });