@saltcorn/server 0.9.0-beta.9 → 0.9.1-beta.0

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/locales/fr.json CHANGED
@@ -286,5 +286,11 @@
286
286
  "Users and security": "Users and security",
287
287
  "Site structure": "Site structure",
288
288
  "Events": "Events",
289
- "Are you sure?": "Are you sure?"
289
+ "Are you sure?": "Are you sure?",
290
+ "API token": "API token",
291
+ "No API token issued": "No API token issued",
292
+ "Generate": "Generate",
293
+ "Two-factor authentication": "Two-factor authentication",
294
+ "Two-factor authentication is disabled": "Two-factor authentication is disabled",
295
+ "Enable 2FA": "Enable 2FA"
290
296
  }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.0-beta.9",
3
+ "version": "0.9.1-beta.0",
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.9.0-beta.9",
10
- "@saltcorn/builder": "0.9.0-beta.9",
11
- "@saltcorn/data": "0.9.0-beta.9",
12
- "@saltcorn/admin-models": "0.9.0-beta.9",
13
- "@saltcorn/filemanager": "0.9.0-beta.9",
14
- "@saltcorn/markup": "0.9.0-beta.9",
15
- "@saltcorn/sbadmin2": "0.9.0-beta.9",
9
+ "@saltcorn/base-plugin": "0.9.1-beta.0",
10
+ "@saltcorn/builder": "0.9.1-beta.0",
11
+ "@saltcorn/data": "0.9.1-beta.0",
12
+ "@saltcorn/admin-models": "0.9.1-beta.0",
13
+ "@saltcorn/filemanager": "0.9.1-beta.0",
14
+ "@saltcorn/markup": "0.9.1-beta.0",
15
+ "@saltcorn/sbadmin2": "0.9.1-beta.0",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -128,10 +128,13 @@ function apply_showif() {
128
128
  const dynwhere = JSON.parse(
129
129
  decodeURIComponent(e.attr("data-fetch-options"))
130
130
  );
131
- //console.log("dynwhere", dynwhere);
132
- const qss = Object.entries(dynwhere.whereParsed).map(
133
- ([k, v]) => `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`
134
- );
131
+ if (window._sc_loglevel > 4) console.log("dynwhere", dynwhere);
132
+ const kvToQs = ([k, v]) => {
133
+ return k === "or" && Array.isArray(v)
134
+ ? v.map((v1) => Object.entries(v1).map(kvToQs).join("&")).join("&")
135
+ : `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`;
136
+ };
137
+ const qss = Object.entries(dynwhere.whereParsed).map(kvToQs);
135
138
  if (dynwhere.dereference) {
136
139
  if (Array.isArray(dynwhere.dereference))
137
140
  qss.push(...dynwhere.dereference.map((d) => `dereference=${d}`));
@@ -151,7 +154,8 @@ function apply_showif() {
151
154
  e.empty();
152
155
  e.prop("data-fetch-options-current-set", qs);
153
156
  const toAppend = [];
154
- if (!dynwhere.required) toAppend.push(`<option></option>`);
157
+ if (!dynwhere.required)
158
+ toAppend.push({ label: dynwhere.neutral_label || "" });
155
159
  let currentDataOption = undefined;
156
160
  const dataOptions = [];
157
161
  //console.log(success);
@@ -170,12 +174,28 @@ function apply_showif() {
170
174
  const selected = `${current}` === `${r[dynwhere.refname]}`;
171
175
  dataOptions.push({ text: label, value });
172
176
  if (selected) currentDataOption = value;
173
- const html = `<option ${
174
- selected ? "selected" : ""
175
- } value="${value}">${label}</option>`;
176
- toAppend.push(html);
177
+ toAppend.push({ selected, value, label });
177
178
  });
178
- e.html(toAppend.join(""));
179
+ toAppend.sort((a, b) =>
180
+ a.label === dynwhere.neutral_label
181
+ ? -1
182
+ : b.label === dynwhere.neutral_label
183
+ ? 1
184
+ : (a.label?.toLowerCase?.() || a.label) >
185
+ (b.label?.toLowerCase?.() || b.label)
186
+ ? 1
187
+ : -1
188
+ );
189
+ e.html(
190
+ toAppend
191
+ .map(
192
+ ({ label, value, selected }) =>
193
+ `<option${selected ? ` selected` : ""}${
194
+ value ? ` value="${value}"` : ""
195
+ }>${label || ""}</option>`
196
+ )
197
+ .join("")
198
+ );
179
199
 
180
200
  //TODO: also sort inserted HTML options
181
201
  dataOptions.sort((a, b) =>
@@ -205,6 +225,9 @@ function apply_showif() {
205
225
  });
206
226
  $.ajax(`/api/${dynwhere.table}?${qs}`).then((resp) => {
207
227
  if (resp.success) {
228
+ if (window._sc_loglevel > 4)
229
+ console.log("dynwhere fetch", qs, resp.success);
230
+
208
231
  activate(resp.success, qs);
209
232
  const cacheNow = e.prop("data-fetch-options-cache") || {};
210
233
  e.prop("data-fetch-options-cache", {
@@ -1016,7 +1039,14 @@ function common_done(res, viewname, isWeb = true) {
1016
1039
  if (res.notify) handle(res.notify, notifyAlert);
1017
1040
  if (res.error)
1018
1041
  handle(res.error, (text) => notifyAlert({ type: "danger", text: text }));
1019
- if (res.eval_js) handle(res.eval_js, eval);
1042
+
1043
+ if (res.eval_js && res.row && res.field_names) {
1044
+ const f = new Function(`row, {${res.field_names}}`, res.eval_js);
1045
+ const evalres = f(res.row, res.row);
1046
+ if (evalres) common_done(evalres, viewname, isWeb);
1047
+ } else if (res.eval_js) {
1048
+ handle(res.eval_js, eval);
1049
+ }
1020
1050
 
1021
1051
  if (res.reload_page) {
1022
1052
  (isWeb ? location : parent).reload(); //TODO notify to cookie if reload or goto
@@ -572,6 +572,20 @@ function ajax_post_btn(e, reload_on_done, reload_delay) {
572
572
 
573
573
  return false;
574
574
  }
575
+
576
+ function api_action_call(name, body) {
577
+ $.ajax(`/api/action/${name}`, {
578
+ type: "POST",
579
+ headers: {
580
+ "CSRF-Token": _sc_globalCsrf,
581
+ },
582
+ data: body,
583
+ success: function (res) {
584
+ common_done(res.data);
585
+ },
586
+ });
587
+ }
588
+
575
589
  function make_unique_field(
576
590
  id,
577
591
  table_id,
package/routes/actions.js CHANGED
@@ -390,6 +390,16 @@ router.get(
390
390
  return;
391
391
  }
392
392
  const action = getState().actions[trigger.action];
393
+ // get table related to trigger
394
+ const table = trigger.table_id
395
+ ? Table.findOne({ id: trigger.table_id })
396
+ : null;
397
+
398
+ const subtitle = span(
399
+ { class: "ms-3" },
400
+ trigger.action,
401
+ table ? ` on ` + a({ href: `/table/${table.name}` }, table.name) : ""
402
+ );
393
403
  if (!action) {
394
404
  req.flash("warning", req.__("Action not found"));
395
405
  res.redirect(`/actions/`);
@@ -420,6 +430,7 @@ router.get(
420
430
  type: "card",
421
431
  titleAjaxIndicator: true,
422
432
  title: req.__("Configure trigger %s", trigger.name),
433
+ subtitle,
423
434
  contents: {
424
435
  widths: [8, 4],
425
436
  besides: [
@@ -465,10 +476,6 @@ router.get(
465
476
  req.flash("warning", req.__("Action not configurable"));
466
477
  res.redirect(`/actions/`);
467
478
  } else {
468
- // get table related to trigger
469
- const table = trigger.table_id
470
- ? Table.findOne({ id: trigger.table_id })
471
- : null;
472
479
  // get configuration fields
473
480
  const cfgFields = await getActionConfigFields(action, table);
474
481
  // create form
@@ -491,6 +498,7 @@ router.get(
491
498
  type: "card",
492
499
  titleAjaxIndicator: true,
493
500
  title: req.__("Configure trigger %s", trigger.name),
501
+ subtitle,
494
502
  contents: renderForm(form, req.csrfToken()),
495
503
  },
496
504
  });
@@ -607,8 +615,10 @@ router.get(
607
615
  table = Table.findOne({ id: trigger.table_id });
608
616
  row = await table.getRow({});
609
617
  }
618
+ let runres;
619
+
610
620
  try {
611
- await trigger.runWithoutRow({
621
+ runres = await trigger.runWithoutRow({
612
622
  console: fakeConsole,
613
623
  table,
614
624
  row,
@@ -627,7 +637,9 @@ router.get(
627
637
  req.__(
628
638
  "Action %s run successfully with no console output",
629
639
  trigger.action
630
- )
640
+ ) + runres
641
+ ? script(domReady(`common_done(${JSON.stringify(runres)})`))
642
+ : ""
631
643
  );
632
644
  res.redirect(`/actions/`);
633
645
  } else {
@@ -641,7 +653,9 @@ router.get(
641
653
  title: req.__("Test run output"),
642
654
  contents: div(
643
655
  div({ class: "testrunoutput" }, output),
644
-
656
+ runres
657
+ ? script(domReady(`common_done(${JSON.stringify(runres)})`))
658
+ : "",
645
659
  a(
646
660
  { href: `/actions`, class: "mt-4 btn btn-primary me-1" },
647
661
  "&laquo;&nbsp;" + req.__("back to actions")
package/routes/api.js CHANGED
@@ -332,7 +332,7 @@ router.get(
332
332
  * @function
333
333
  * @memberof module:routes/api~apiRouter
334
334
  */
335
- router.post(
335
+ router.all(
336
336
  "/action/:actionname/",
337
337
  error_catcher(async (req, res, next) => {
338
338
  const { actionname } = req.params;
@@ -361,7 +361,7 @@ router.post(
361
361
  const resp = await action.run({
362
362
  configuration: trigger.configuration,
363
363
  body: req.body,
364
- row: req.body,
364
+ row: req.method === "GET" ? req.query : req.body,
365
365
  req,
366
366
  user: user || req.user,
367
367
  });
@@ -9,7 +9,7 @@ const {
9
9
  post_dropdown_item,
10
10
  } = require("@saltcorn/markup");
11
11
  const { get_base_url } = require("./utils.js");
12
- const { h4, p, div, a, input, text } = require("@saltcorn/markup/tags");
12
+ const { h4, p, div, a, i, input, text } = require("@saltcorn/markup/tags");
13
13
 
14
14
  /**
15
15
  * @param {string} col
@@ -385,10 +385,16 @@ const getTriggerList = (triggers, req, { tagId, domId, showList } = {}) => {
385
385
  },
386
386
  {
387
387
  label: req.__("When"),
388
- key: (a) =>
389
- a.when_trigger === "API call"
390
- ? `API: ${base_url}api/action/${a.name}`
391
- : a.when_trigger,
388
+ key: (act) =>
389
+ act.when_trigger +
390
+ (act.when_trigger === "API call"
391
+ ? a(
392
+ {
393
+ href: `javascript:ajax_modal('/admin/help/API%20actions?name=${act.name}')`,
394
+ },
395
+ i({ class: "fas fa-question-circle ms-1" })
396
+ )
397
+ : ""),
392
398
  },
393
399
  {
394
400
  label: req.__("Test run"),
@@ -262,10 +262,16 @@ const actionsTab = async (req, triggers) => {
262
262
  },
263
263
  {
264
264
  label: req.__("When"),
265
- key: (a) =>
266
- a.when_trigger === "API call"
267
- ? `API: ${base_url}api/action/${a.name}`
268
- : a.when_trigger,
265
+ key: (act) =>
266
+ act.when_trigger +
267
+ (act.when_trigger === "API call"
268
+ ? a(
269
+ {
270
+ href: `javascript:ajax_modal('/admin/help/API%20actions?name=${act.name}')`,
271
+ },
272
+ i({ class: "fas fa-question-circle ms-1" })
273
+ )
274
+ : ""),
269
275
  },
270
276
  ],
271
277
  triggers
package/routes/page.js CHANGED
@@ -61,6 +61,7 @@ router.get(
61
61
  title,
62
62
  description: db_page.description,
63
63
  bodyClass: "page_" + db.sqlsanitize(pagename),
64
+ no_menu: db_page.attributes?.no_menu,
64
65
  } || `${pagename} page`,
65
66
  add_edit_bar({
66
67
  role,
@@ -92,6 +92,12 @@ const pagePropertiesForm = async (req, isNew) => {
92
92
  input_type: "select",
93
93
  options: roles.map((r) => ({ value: r.id, label: r.role })),
94
94
  },
95
+ {
96
+ name: "no_menu",
97
+ label: req.__("No menu"),
98
+ sublabel: req.__("Omit the menu from this page"),
99
+ type: "Bool",
100
+ },
95
101
  ],
96
102
  });
97
103
  return form;
@@ -283,7 +289,7 @@ const wrap = (contents, noCard, req, page) => ({
283
289
  crumbs: [
284
290
  { text: req.__("Pages"), href: "/pageedit" },
285
291
  page
286
- ? { href: `/pageedit/edit/${page.name}`, text: page.name }
292
+ ? { href: `/page/${page.name}`, text: page.name }
287
293
  : { text: req.__("New") },
288
294
  ],
289
295
  },
@@ -315,6 +321,7 @@ router.get(
315
321
  const form = await pagePropertiesForm(req);
316
322
  form.hidden("id");
317
323
  form.values = page;
324
+ form.values.no_menu = page.attributes?.no_menu;
318
325
  res.sendWrap(
319
326
  req.__(`Page attributes`),
320
327
  wrap(renderForm(form, req.csrfToken()), false, req, page)
@@ -360,9 +367,9 @@ router.post(
360
367
  wrap(renderForm(form, req.csrfToken()), false, req)
361
368
  );
362
369
  } else {
363
- const { id, columns, ...pageRow } = form.values;
370
+ const { id, columns, no_menu, ...pageRow } = form.values;
364
371
  pageRow.min_role = +pageRow.min_role;
365
-
372
+ pageRow.attributes = { no_menu };
366
373
  if (+id) {
367
374
  await Page.update(+id, pageRow);
368
375
  res.redirect(`/pageedit/`);
@@ -638,8 +638,8 @@ router.post(
638
638
  newcfg = {
639
639
  ...view.configuration,
640
640
  [step.contextField]: {
641
- ...view.configuration?.[step.contextField],
642
- ...context,
641
+ ...(view.configuration?.[step.contextField] || {}),
642
+ ...(context?.[step.contextField] || {}),
643
643
  },
644
644
  };
645
645
  else newcfg = { ...view.configuration, ...context };
package/tests/api.test.js CHANGED
@@ -1,6 +1,9 @@
1
1
  const request = require("supertest");
2
2
  const getApp = require("../app");
3
3
  const Table = require("@saltcorn/data/models/table");
4
+ const Trigger = require("@saltcorn/data/models/trigger");
5
+
6
+ const Field = require("@saltcorn/data/models/field");
4
7
  const {
5
8
  getStaffLoginCookie,
6
9
  getAdminLoginCookie,
@@ -9,6 +12,7 @@ const {
9
12
  succeedJsonWith,
10
13
  notAuthorized,
11
14
  toRedirect,
15
+ succeedJsonWithWholeBody,
12
16
  } = require("../auth/testhelp");
13
17
  const db = require("@saltcorn/data/db");
14
18
  const User = require("@saltcorn/data/models/user");
@@ -322,3 +326,66 @@ describe("API authentication", () => {
322
326
  .expect(succeedJsonWith((rows) => rows.length == 2));
323
327
  });
324
328
  });
329
+
330
+ describe("API action", () => {
331
+ it("should set up trigger", async () => {
332
+ const table = await Table.create("triggercounter");
333
+ await Field.create({
334
+ table,
335
+ name: "thing",
336
+ label: "TheThing",
337
+ type: "String",
338
+ });
339
+ await Trigger.create({
340
+ action: "run_js_code",
341
+ when_trigger: "API call",
342
+ name: "mywebhook",
343
+ min_role: 100,
344
+ configuration: {
345
+ code: `
346
+ const table = Table.findOne({ name: "triggercounter" });
347
+ await table.insertRow({ thing: row?.thing || "no body" });
348
+ return {studio: 54}
349
+ `,
350
+ },
351
+ });
352
+ });
353
+ it("should POST to trigger", async () => {
354
+ const app = await getApp({ disableCsrf: true });
355
+ await request(app)
356
+ .post("/api/action/mywebhook")
357
+ .send({
358
+ thing: "inthebody",
359
+ })
360
+ .set("Content-Type", "application/json")
361
+ .set("Accept", "application/json")
362
+ .expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
363
+ const table = Table.findOne({ name: "triggercounter" });
364
+ const counts = await table.getRows({});
365
+ expect(counts.map((c) => c.thing)).toContain("inthebody");
366
+ expect(counts.map((c) => c.thing)).not.toContain("no body");
367
+ });
368
+ it("should GET with query to trigger", async () => {
369
+ const app = await getApp({ disableCsrf: true });
370
+ await request(app)
371
+ .get("/api/action/mywebhook?thing=inthequery")
372
+ .set("Content-Type", "application/json")
373
+ .set("Accept", "application/json")
374
+ .expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
375
+ const table = Table.findOne({ name: "triggercounter" });
376
+ const counts = await table.getRows({});
377
+ expect(counts.map((c) => c.thing)).toContain("inthequery");
378
+ expect(counts.map((c) => c.thing)).not.toContain("no body");
379
+ });
380
+ it("should GET to trigger", async () => {
381
+ const app = await getApp({ disableCsrf: true });
382
+ await request(app)
383
+ .get("/api/action/mywebhook")
384
+ .set("Content-Type", "application/json")
385
+ .set("Accept", "application/json")
386
+ .expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
387
+ const table = Table.findOne({ name: "triggercounter" });
388
+ const counts = await table.getRows({});
389
+ expect(counts.map((c) => c.thing)).toContain("no body");
390
+ });
391
+ });