@saltcorn/server 0.7.3-beta.7 → 0.7.4-beta.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/routes/api.js CHANGED
@@ -112,6 +112,13 @@ function accessAllowed(req, user, trigger) {
112
112
  return role <= trigger.min_role;
113
113
  }
114
114
 
115
+ const getFlashes = (req) =>
116
+ ["error", "success", "danger", "warning", "information"]
117
+ .map((type) => {
118
+ return { type, msg: req.flash(type) };
119
+ })
120
+ .filter((a) => a.msg && a.msg.length && a.msg.length > 0);
121
+
115
122
  router.post(
116
123
  "/viewQuery/:viewName/:queryName",
117
124
  error_catcher(async (req, res, next) => {
@@ -134,7 +141,7 @@ router.post(
134
141
  if (queries[queryName]) {
135
142
  const { args } = req.body;
136
143
  const resp = await queries[queryName](...args, true);
137
- res.json({ success: resp });
144
+ res.json({ success: resp, alerts: getFlashes(req) });
138
145
  } else {
139
146
  res.status(404).json({ error: req.__("Not found") });
140
147
  }
@@ -235,6 +242,7 @@ router.get(
235
242
  rows = await table.getJoinedRows(joinOpts);
236
243
  } else if (req_query && req_query !== {}) {
237
244
  const tbl_fields = await table.getFields();
245
+ readState(req_query, tbl_fields, req);
238
246
  const qstate = await stateFieldsToWhere({
239
247
  fields: tbl_fields,
240
248
  approximate: !!approximate,
@@ -73,8 +73,9 @@ const logSettingsForm = async (req) => {
73
73
  fields.push({
74
74
  name: w + "_channel",
75
75
  label: w + " channel",
76
- sublabel:
77
- req.__("Channels to create events for. Separate by comma; leave blank for all"),
76
+ sublabel: req.__(
77
+ "Channels to create events for. Separate by comma; leave blank for all"
78
+ ),
78
79
  type: "String",
79
80
  showIf: { [w]: true },
80
81
  });
@@ -82,8 +83,8 @@ const logSettingsForm = async (req) => {
82
83
  return new Form({
83
84
  action: "/eventlog/settings",
84
85
  blurb: req.__("Which events should be logged?"),
85
- submitButtonClass: "btn-outline-primary",
86
- onChange: "remove_outline(this)",
86
+ noSubmitButton: true,
87
+ onChange: "saveAndContinue(this)",
87
88
  fields,
88
89
  });
89
90
  };
@@ -169,23 +170,23 @@ router.get(
169
170
  * @returns {Form}
170
171
  */
171
172
  const customEventForm = async (req) => {
172
- return new Form({
173
- action: "/eventlog/custom/new",
174
- submitButtonClass: "btn-outline-primary",
175
- onChange: "remove_outline(this)",
176
- fields: [
177
- {
178
- name: "name",
179
- label: req.__("Event Name"),
180
- type: "String",
181
- },
182
- {
183
- name: "hasChannel",
184
- label: req.__("Has channels?"),
185
- type: "Bool",
186
- },
187
- ],
188
- });
173
+ return new Form({
174
+ action: "/eventlog/custom/new",
175
+ submitButtonClass: "btn-outline-primary",
176
+ onChange: "remove_outline(this)",
177
+ fields: [
178
+ {
179
+ name: "name",
180
+ label: req.__("Event Name"),
181
+ type: "String",
182
+ },
183
+ {
184
+ name: "hasChannel",
185
+ label: req.__("Has channels?"),
186
+ type: "Bool",
187
+ },
188
+ ],
189
+ });
189
190
  };
190
191
  /**
191
192
  * @name get/custom/new
@@ -297,7 +298,8 @@ router.post(
297
298
  } else {
298
299
  await getState().setConfig("event_log_settings", form.values);
299
300
 
300
- res.redirect(`/eventlog/settings`);
301
+ if (!req.xhr) res.redirect(`/eventlog/settings`);
302
+ else res.json({ success: "ok" });
301
303
  }
302
304
  })
303
305
  );
package/routes/fields.js CHANGED
@@ -348,13 +348,12 @@ const fieldFlow = (req) =>
348
348
  // todo sublabel
349
349
  input_type: "custom_html",
350
350
  attributes: {
351
- html: `<button type="button" id="test_formula_btn" onclick="test_formula('${
352
- table.name
353
- }', ${JSON.stringify(
354
- context.stored
355
- )})" class="btn btn-outline-secondary">${req.__(
356
- "Test"
357
- )}</button>
351
+ html: `<button type="button" id="test_formula_btn" onclick="test_formula('${table.name
352
+ }', ${JSON.stringify(
353
+ context.stored
354
+ )})" class="btn btn-outline-secondary">${req.__(
355
+ "Test"
356
+ )}</button>
358
357
  <div id="test_formula_output"></div>`,
359
358
  },
360
359
  }),
@@ -633,8 +632,7 @@ router.post(
633
632
  result = f(rows[0]);
634
633
  }
635
634
  res.send(
636
- `Result of running on row with id=${
637
- rows[0].id
635
+ `Result of running on row with id=${rows[0].id
638
636
  } is: <pre>${JSON.stringify(result)}</pre>`
639
637
  );
640
638
  } catch (e) {
@@ -788,22 +786,22 @@ router.post(
788
786
  field.type === "Key"
789
787
  ? getState().keyFieldviews
790
788
  : field.type === "File"
791
- ? getState().fileviews
792
- : field.type.fieldviews;
789
+ ? getState().fileviews
790
+ : field.type.fieldviews;
793
791
  if (!field.type || !fieldviews) {
794
792
  res.send("");
795
793
  return;
796
794
  }
797
795
  const fv = fieldviews[fieldview];
798
796
  if (!fv && field.type === "Key" && fieldview === "select")
799
- res.send("<select disabled></select>");
797
+ res.send(`<input readonly class="form-control form-select"></input>`);
800
798
  else if (!fv) res.send("");
801
799
  else if (fv.isEdit || fv.isFilter)
802
800
  res.send(
803
801
  fv.run(
804
802
  field.name,
805
803
  undefined,
806
- { disabled: true, ...configuration, ...(field.attributes || {}) },
804
+ { readonly: true, ...configuration, ...(field.attributes || {}) },
807
805
  "",
808
806
  false,
809
807
  field
package/routes/files.js CHANGED
@@ -378,9 +378,6 @@ const storage_form = async (req) => {
378
378
  ],
379
379
  action: "/files/storage",
380
380
  });
381
- form.submitButtonClass = "btn-outline-primary";
382
- form.submitLabel = req.__("Save");
383
- form.onChange = "remove_outline(this)";
384
381
  return form;
385
382
  };
386
383
 
@@ -431,8 +428,11 @@ router.post(
431
428
  });
432
429
  } else {
433
430
  await save_config_from_form(form);
434
- req.flash("success", req.__("Storage settings updated"));
435
- res.redirect("/files/storage");
431
+
432
+ if (!req.xhr) {
433
+ req.flash("success", req.__("Storage settings updated"));
434
+ res.redirect("/files/storage");
435
+ } else res.json({ success: "ok" });
436
436
  }
437
437
  })
438
438
  );
@@ -54,9 +54,9 @@ const tableCard = (tables, req) => ({
54
54
  contents:
55
55
  (tables.length <= 1
56
56
  ? p(
57
- { class: "mt-2 pe-2" },
58
- i(req.__("Tables organise data by fields and rows."))
59
- )
57
+ { class: "mt-2 pe-2" },
58
+ i(req.__("Tables organise data by fields and rows."))
59
+ )
60
60
  : "") + tableTable(tables, req),
61
61
  bodyClass: "py-0 pe-0",
62
62
  footer: div(
@@ -107,13 +107,13 @@ const viewCard = (views, req) => ({
107
107
  contents:
108
108
  (views.length <= 1
109
109
  ? p(
110
- { class: "mt-2 pe-2" },
111
- i(
112
- req.__(
113
- "Views display data from tables. A view is a view template applied to a table, with configuration."
114
- )
110
+ { class: "mt-2 pe-2" },
111
+ i(
112
+ req.__(
113
+ "Views display data from tables. A view is a view pattern applied to a table, with configuration."
115
114
  )
116
115
  )
116
+ )
117
117
  : "") +
118
118
  (views.length > 0 ? viewTable(views, req) : p(req.__("No views"))),
119
119
 
@@ -160,13 +160,13 @@ const pageCard = (pages, req) => ({
160
160
  contents:
161
161
  (pages.length <= 1
162
162
  ? p(
163
- { class: "mt-2 pe-2" },
164
- i(
165
- req.__(
166
- "Pages are the web pages of your application built with a drag-and-drop builder. They have static content, and by embedding views, dynamic content."
167
- )
163
+ { class: "mt-2 pe-2" },
164
+ i(
165
+ req.__(
166
+ "Pages are the web pages of your application built with a drag-and-drop builder. They have static content, and by embedding views, dynamic content."
168
167
  )
169
168
  )
169
+ )
170
170
  : "") +
171
171
  (pages.length > 0
172
172
  ? pageTable(pages, req)
@@ -191,16 +191,16 @@ const filesTab = async (req) => {
191
191
  files.length === 0
192
192
  ? p(req.__("No files"))
193
193
  : mkTable(
194
- [
195
- {
196
- label: req.__("Filename"),
197
- key: (r) => link(`/files/serve/${r.id}`, r.filename),
198
- },
199
- { label: req.__("Size (KiB)"), key: "size_kb", align: "right" },
200
- { label: req.__("Media type"), key: (r) => r.mimetype },
201
- ],
202
- files
203
- ),
194
+ [
195
+ {
196
+ label: req.__("Filename"),
197
+ key: (r) => link(`/files/serve/${r.id}`, r.filename),
198
+ },
199
+ { label: req.__("Size (KiB)"), key: "size_kb", align: "right" },
200
+ { label: req.__("Media type"), key: (r) => r.mimetype },
201
+ ],
202
+ files
203
+ ),
204
204
  fileUploadForm(req)
205
205
  );
206
206
  };
@@ -244,30 +244,30 @@ const actionsTab = async (req, triggers) => {
244
244
  return div(
245
245
  { class: "pb-3" },
246
246
  triggers.length <= 1 &&
247
- p(
248
- { class: "mt-2 pe-2" },
249
- i(req.__("Triggers run actions in response to events."))
250
- ),
247
+ p(
248
+ { class: "mt-2 pe-2" },
249
+ i(req.__("Triggers run actions in response to events."))
250
+ ),
251
251
  triggers.length === 0
252
252
  ? p(req.__("No triggers"))
253
253
  : mkTable(
254
- [
255
- { label: req.__("Name"), key: "name" },
256
- { label: req.__("Action"), key: "action" },
257
- {
258
- label: req.__("Table or Channel"),
259
- key: (r) => r.table_name || r.channel,
260
- },
261
- {
262
- label: req.__("When"),
263
- key: (a) =>
264
- a.when_trigger === "API call"
265
- ? `API: ${base_url}api/action/${a.name}`
266
- : a.when_trigger,
267
- },
268
- ],
269
- triggers
270
- ),
254
+ [
255
+ { label: req.__("Name"), key: "name" },
256
+ { label: req.__("Action"), key: "action" },
257
+ {
258
+ label: req.__("Table or Channel"),
259
+ key: (r) => r.table_name || r.channel,
260
+ },
261
+ {
262
+ label: req.__("When"),
263
+ key: (a) =>
264
+ a.when_trigger === "API call"
265
+ ? `API: ${base_url}api/action/${a.name}`
266
+ : a.when_trigger,
267
+ },
268
+ ],
269
+ triggers
270
+ ),
271
271
  a(
272
272
  { href: "/actions/new", class: "btn btn-secondary my-3" },
273
273
  req.__("Add trigger")
@@ -385,15 +385,15 @@ const welcome_page = async (req) => {
385
385
  tabContents:
386
386
  triggers.length > 0
387
387
  ? {
388
- Triggers: await actionsTab(req, triggers),
389
- Files: await filesTab(req),
390
- Packs: packTab(req, packlist),
391
- }
388
+ Triggers: await actionsTab(req, triggers),
389
+ Files: await filesTab(req),
390
+ Packs: packTab(req, packlist),
391
+ }
392
392
  : {
393
- Packs: packTab(req, packlist),
394
- Triggers: await actionsTab(req, triggers),
395
- Files: await filesTab(req),
396
- },
393
+ Packs: packTab(req, packlist),
394
+ Triggers: await actionsTab(req, triggers),
395
+ Files: await filesTab(req),
396
+ },
397
397
  },
398
398
  {
399
399
  type: "card",
@@ -403,13 +403,13 @@ const welcome_page = async (req) => {
403
403
  tabContents:
404
404
  users.length > 4
405
405
  ? {
406
- Users: await usersTab(req, users, roleMap),
407
- Help: helpCard(req),
408
- }
406
+ Users: await usersTab(req, users, roleMap),
407
+ Help: helpCard(req),
408
+ }
409
409
  : {
410
- Help: helpCard(req),
411
- Users: await usersTab(req, users, roleMap),
412
- },
410
+ Help: helpCard(req),
411
+ Users: await usersTab(req, users, roleMap),
412
+ },
413
413
  },
414
414
  ],
415
415
  },
@@ -440,8 +440,8 @@ const no_views_logged_in = async (req, res) => {
440
440
  packagejson.version,
441
441
  latest
442
442
  ) +
443
- " " +
444
- a({ href: "/admin/system" }, req.__("Upgrade here"))
443
+ " " +
444
+ a({ href: "/admin/system" }, req.__("Upgrade here"))
445
445
  );
446
446
 
447
447
  res.sendWrap(req.__("Hello"), await welcome_page(req));
@@ -48,8 +48,8 @@ router.get(
48
48
  const languageForm = (req) =>
49
49
  new Form({
50
50
  action: "/site-structure/localizer/save-lang",
51
- submitButtonClass: "btn-outline-primary",
52
- onChange: "remove_outline(this)",
51
+ onChange: "saveAndContinue(this)",
52
+ noSubmitButton: true,
53
53
  fields: [
54
54
  {
55
55
  name: "name",
@@ -270,7 +270,10 @@ router.post(
270
270
  ...cfgLangs,
271
271
  [lang.locale]: lang,
272
272
  });
273
- res.redirect(`/site-structure/localizer/edit/${lang.locale}`);
273
+
274
+ if (!req.xhr)
275
+ res.redirect(`/site-structure/localizer/edit/${lang.locale}`);
276
+ else res.json({ success: "ok" });
274
277
  }
275
278
  })
276
279
  );
package/routes/menu.js CHANGED
@@ -22,6 +22,9 @@ const { renderForm } = require("@saltcorn/markup");
22
22
  const { script, domReady, div, ul } = require("@saltcorn/markup/tags");
23
23
  const { send_infoarch_page } = require("../markup/admin.js");
24
24
  const Table = require("@saltcorn/data/models/table");
25
+ const Trigger = require("@saltcorn/data/models/trigger");
26
+ const { run_action_column } = require("@saltcorn/data/plugin-helper");
27
+
25
28
 
26
29
  /**
27
30
  * @type {object}
@@ -61,6 +64,18 @@ const menuForm = async (req) => {
61
64
  dynSectionFieldOptions[table.name].push(field.name);
62
65
  }
63
66
  }
67
+ const stateActions = getState().actions;
68
+ const actions = [
69
+ ...Object.entries(stateActions)
70
+ .filter(([k, v]) => !v.requireRow && !v.disableInBuilder)
71
+ .map(([k, v]) => k),
72
+ ];
73
+ const triggers = await Trigger.find({
74
+ when_trigger: { or: ["API call", "Never"] },
75
+ });
76
+ triggers.forEach((tr) => {
77
+ actions.push(tr.name);
78
+ });
64
79
 
65
80
  return new Form({
66
81
  action: "/menu/",
@@ -92,6 +107,7 @@ const menuForm = async (req) => {
92
107
  "Dynamic",
93
108
  "Search",
94
109
  "Separator",
110
+ "Action"
95
111
  ],
96
112
  },
97
113
  {
@@ -101,7 +117,7 @@ const menuForm = async (req) => {
101
117
  input_type: "text",
102
118
  required: true,
103
119
  showIf: {
104
- type: ["View", "Page", "Link", "Header", "Dynamic", "Search"],
120
+ type: ["View", "Page", "Link", "Header", "Dynamic", "Search", "Action"],
105
121
  },
106
122
  },
107
123
  {
@@ -111,7 +127,7 @@ const menuForm = async (req) => {
111
127
  attributes: {
112
128
  html: `<button type="button" id="myEditor_icon" class="btn btn-outline-secondary"></button>`,
113
129
  },
114
- showIf: { type: ["View", "Page", "Link", "Header"] },
130
+ showIf: { type: ["View", "Page", "Link", "Header", "Action"] },
115
131
  },
116
132
  {
117
133
  name: "icon",
@@ -149,6 +165,17 @@ const menuForm = async (req) => {
149
165
  attributes: { options: views.map((r) => r.select_option) },
150
166
  showIf: { type: "View" },
151
167
  },
168
+ {
169
+ name: "action_name",
170
+ label: req.__("Action"),
171
+ type: "String",
172
+ class: "item-menu",
173
+ required: true,
174
+ attributes: {
175
+ options: actions,
176
+ },
177
+ showIf: { type: "Action" },
178
+ },
152
179
  {
153
180
  name: "dyn_table",
154
181
  label: req.__("Table"),
@@ -217,7 +244,7 @@ const menuForm = async (req) => {
217
244
  class: "item-menu",
218
245
  type: "String",
219
246
  required: true,
220
- showIf: { type: ["View", "Page", "Link", "Header", "Dynamic"] },
247
+ showIf: { type: ["View", "Page", "Link", "Header", "Dynamic", "Action"] },
221
248
  attributes: {
222
249
  options: [
223
250
  { name: "", label: "Link" },
@@ -239,7 +266,7 @@ const menuForm = async (req) => {
239
266
  {
240
267
  name: "location",
241
268
  label: req.__("Location"),
242
- showIf: { type: ["View", "Page", "Link", "Header", "Dynamic"] },
269
+ showIf: { type: ["View", "Page", "Link", "Header", "Dynamic", "Action"] },
243
270
  sublabel: req.__("Not all themes support all locations"),
244
271
  class: "item-menu",
245
272
  type: "String",
@@ -404,3 +431,37 @@ router.post(
404
431
  res.json({ success: true });
405
432
  })
406
433
  );
434
+
435
+ router.post(
436
+ "/runaction/:name",
437
+ error_catcher(async (req, res) => {
438
+ const { name } = req.params;
439
+ const role = (req.user || {}).role_id || 10;
440
+ const state = getState();
441
+ const menu_items = state.getConfig("menu_items");
442
+ let menu_item;
443
+ const search = items =>
444
+ items
445
+ .filter((item) => role <= +item.min_role)
446
+ .forEach(item => {
447
+ if (item.type === "Action" && item.action_name === name)
448
+ menu_item = item;
449
+ else if (item.subitems)
450
+ search(item.subitems);
451
+ })
452
+ search(menu_items);
453
+ if (menu_item)
454
+ try {
455
+ const result = await run_action_column({
456
+ col: menu_item,
457
+ referrer: req.get("Referrer"),
458
+ req,
459
+ });
460
+ res.json({ success: "ok", ...(result || {}) });
461
+ } catch (e) {
462
+ res.status(400).json({ error: e.message || e });
463
+ }
464
+
465
+ else res.status(404).json({ error: "Action not found" });
466
+ })
467
+ );
package/routes/packs.js CHANGED
@@ -116,7 +116,7 @@ router.get(
116
116
  type: "breadcrumbs",
117
117
  crumbs: [
118
118
  { text: req.__("Settings") },
119
- { text: req.__("Plugins"), href: "/plugins" },
119
+ { text: req.__("Modules"), href: "/plugins" },
120
120
  { text: req.__("Create pack") },
121
121
  ],
122
122
  },
@@ -184,7 +184,7 @@ router.post(
184
184
  type: "breadcrumbs",
185
185
  crumbs: [
186
186
  { text: req.__("Settings") },
187
- { text: req.__("Plugins"), href: "/plugins" },
187
+ { text: req.__("Modules"), href: "/plugins" },
188
188
  { text: req.__("Create pack") },
189
189
  ],
190
190
  },
@@ -242,7 +242,7 @@ router.get(
242
242
  type: "breadcrumbs",
243
243
  crumbs: [
244
244
  { text: req.__("Settings") },
245
- { text: req.__("Plugins"), href: "/plugins" },
245
+ { text: req.__("Modules"), href: "/plugins" },
246
246
  { text: req.__("Install pack") },
247
247
  ],
248
248
  },
@@ -293,7 +293,7 @@ router.post(
293
293
  type: "breadcrumbs",
294
294
  crumbs: [
295
295
  { text: req.__("Settings") },
296
- { text: req.__("Plugins"), href: "/plugins" },
296
+ { text: req.__("Modules"), href: "/plugins" },
297
297
  { text: req.__("Install pack") },
298
298
  ],
299
299
  },
package/routes/page.js CHANGED
@@ -36,6 +36,8 @@ router.get(
36
36
  "/:pagename",
37
37
  error_catcher(async (req, res) => {
38
38
  const { pagename } = req.params;
39
+ const state = getState();
40
+ state.log(3, `Route /page/${pagename} user=${req.user?.id}`);
39
41
 
40
42
  const role = req.user && req.user.id ? req.user.role_id : 10;
41
43
  const db_page = await Page.findOne({ name: pagename });
@@ -56,10 +58,12 @@ router.get(
56
58
  contents,
57
59
  })
58
60
  );
59
- } else
61
+ } else {
62
+ state.log(2, `Page $pagename} not found or not authorized`);
60
63
  res
61
64
  .status(404)
62
65
  .sendWrap(`${pagename} page`, req.__("Page %s not found", pagename));
66
+ }
63
67
  })
64
68
  );
65
69
 
@@ -90,6 +90,13 @@ const page_dropdown = (page, req) =>
90
90
  '<i class="far fa-copy"></i>&nbsp;' + req.__("Duplicate"),
91
91
  req
92
92
  ),
93
+ a(
94
+ {
95
+ class: "dropdown-item",
96
+ href: `javascript:ajax_modal('/admin/snapshot-restore/page/${page.name}')`,
97
+ },
98
+ '<i class="fas fa-undo-alt"></i>&nbsp;' + req.__("Restore")
99
+ ),
93
100
  div({ class: "dropdown-divider" }),
94
101
  post_dropdown_item(
95
102
  `/pageedit/delete/${page.id}`,
@@ -180,7 +187,8 @@ const pageBuilderData = async (req, context) => {
180
187
  const fixed_state_fields = {};
181
188
  for (const view of views) {
182
189
  fixed_state_fields[view.name] = [];
183
- const table = Table.findOne({ id: view.table_id });
190
+ const table = Table.findOne(view.table_id || view.exttable_name);
191
+
184
192
  const fs = await view.get_state_fields();
185
193
  for (const frec of fs) {
186
194
  const f = new Field(frec);