@saltcorn/server 0.7.3-beta.3 → 0.7.3

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/markup/admin.js CHANGED
@@ -124,6 +124,7 @@ const send_settings_page = ({
124
124
  : [
125
125
  {
126
126
  type: "card",
127
+ class: "mt-0",
127
128
  contents: div(
128
129
  { class: "d-flex" },
129
130
  ul(
@@ -344,7 +345,7 @@ const flash_restart = (req) => {
344
345
  * @param {*} opts.formArgs
345
346
  * @returns {Promise<Form>}
346
347
  */
347
- const config_fields_form = async ({ field_names, req, ...formArgs }) => {
348
+ const config_fields_form = async ({ field_names, req, action, ...formArgs }) => {
348
349
  const values = {};
349
350
  const state = getState();
350
351
  const fields = [];
@@ -395,8 +396,9 @@ const config_fields_form = async ({ field_names, req, ...formArgs }) => {
395
396
  const form = new Form({
396
397
  fields,
397
398
  values,
398
- submitButtonClass: "btn-outline-primary",
399
- onChange: "remove_outline(this)",
399
+ action,
400
+ noSubmitButton: true,
401
+ onChange: `saveAndContinue(this)`,
400
402
  ...formArgs,
401
403
  });
402
404
  await form.fill_fkey_options();
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.7.3-beta.3",
3
+ "version": "0.7.3",
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.7.3-beta.3",
10
- "@saltcorn/builder": "0.7.3-beta.3",
11
- "@saltcorn/data": "0.7.3-beta.3",
12
- "@saltcorn/admin-models": "0.7.3-beta.3",
13
- "@saltcorn/markup": "0.7.3-beta.3",
14
- "@saltcorn/sbadmin2": "0.7.3-beta.3",
9
+ "@saltcorn/base-plugin": "0.7.3",
10
+ "@saltcorn/builder": "0.7.3",
11
+ "@saltcorn/data": "0.7.3",
12
+ "@saltcorn/admin-models": "0.7.3",
13
+ "@saltcorn/markup": "0.7.3",
14
+ "@saltcorn/sbadmin2": "0.7.3",
15
15
  "@socket.io/cluster-adapter": "^0.1.0",
16
16
  "@socket.io/sticky": "^1.0.1",
17
17
  "aws-sdk": "^2.1037.0",
@@ -48,7 +48,7 @@
48
48
  "pg": "^8.2.1",
49
49
  "pluralize": "^8.0.0",
50
50
  "qrcode": "1.5.0",
51
- "resize-with-sharp-or-jimp": "0.1.4",
51
+ "resize-with-sharp-or-jimp": "0.1.5",
52
52
  "socket.io": "4.2.0",
53
53
  "thirty-two": "1.0.2",
54
54
  "tmp-promise": "^3.0.2",
@@ -76,6 +76,8 @@ function isoDateTimeFormatter(cell, formatterParams, onRendered) {
76
76
  function isoDateFormatter(cell, formatterParams, onRendered) {
77
77
  const val = cell.getValue();
78
78
  if (!val) return "";
79
+ if (formatterParams && formatterParams.format)
80
+ return moment(val).format(formatterParams.format);
79
81
 
80
82
  return new Date(val).toLocaleDateString(window.detected_locale || "en");
81
83
  }
@@ -144,6 +146,10 @@ function add_tabulator_row() {
144
146
  }
145
147
 
146
148
  function delete_tabulator_row(e, cell) {
149
+ const def = cell.getColumn().getDefinition();
150
+ if (def && def.formatterParams && def.formatterParams.confirm) {
151
+ if (!confirm("Are you sure you want to delete this row?")) return;
152
+ }
147
153
  const row = cell.getRow().getData();
148
154
  if (!row.id) {
149
155
  cell.getRow().delete();
@@ -117,7 +117,7 @@ function MenuEditor(e, t) {
117
117
  (n.prev("div").children(".sortableListsOpener").first().remove(),
118
118
  n.remove()),
119
119
  MenuEditor.updateButtons(s);
120
- l.onUpdate();
120
+ l.onUpdate();
121
121
  }
122
122
  }),
123
123
  $(document).on("click", ".btnEdit", function (e) {
@@ -132,6 +132,9 @@ function MenuEditor(e, t) {
132
132
  "checked",
133
133
  true
134
134
  );
135
+ } else if (el.prop("tagName") == "SELECT") {
136
+ el.val(t);
137
+ el.attr("data-selected", t);
135
138
  } else el.val(t);
136
139
  }),
137
140
  i.find(".item-menu").first().focus(),
@@ -139,6 +142,16 @@ function MenuEditor(e, t) {
139
142
  ? c.iconpicker("setIcon", t.icon)
140
143
  : c.iconpicker("setIcon", "empty");
141
144
  r.removeAttr("disabled");
145
+ i.find("[name]").each(function () {
146
+ try {
147
+ const el = $(this);
148
+ if (typeof t[el.attr("name")] === "undefined") {
149
+ el.val("");
150
+ }
151
+ } catch (e) {
152
+ console.warn(e);
153
+ }
154
+ });
142
155
  })((n = $(this).closest("li")));
143
156
  l.onUpdate();
144
157
  }),
@@ -164,7 +177,7 @@ function MenuEditor(e, t) {
164
177
  t.remove()),
165
178
  MenuEditor.updateButtons(s),
166
179
  s.updateLevels();
167
- l.onUpdate();
180
+ l.onUpdate();
168
181
  }),
169
182
  s.on("click", ".btnIn", function (e) {
170
183
  e.preventDefault();
@@ -63,8 +63,8 @@ function apply_showif() {
63
63
  .val();
64
64
 
65
65
  var options = data[1][val];
66
- var current = e.attr("data-selected");
67
- //console.log({ val, options, current, data });
66
+ var current = e.attr("data-selected") || e.val();
67
+ //console.log({ field: e.attr("name"), target: data[0], val, current });
68
68
  e.empty();
69
69
  (options || []).forEach((o) => {
70
70
  if (
@@ -87,6 +87,39 @@ function apply_showif() {
87
87
  e.attr("data-selected", ec.target.value);
88
88
  });
89
89
  });
90
+ $("[data-fetch-options]").each(function (ix, element) {
91
+ const e = $(element);
92
+ const rec = get_form_record(e);
93
+ const dynwhere = JSON.parse(
94
+ decodeURIComponent(e.attr("data-fetch-options"))
95
+ );
96
+ //console.log(dynwhere);
97
+ const qs = Object.entries(dynwhere.whereParsed)
98
+ .map(([k, v]) => `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`)
99
+ .join("&");
100
+ var current = e.attr("data-selected");
101
+ e.change(function (ec) {
102
+ e.attr("data-selected", ec.target.value);
103
+ });
104
+ $.ajax(`/api/${dynwhere.table}?${qs}`).then((resp) => {
105
+ if (resp.success) {
106
+ e.empty();
107
+ if (!dynwhere.required) e.append($(`<option></option>`));
108
+ resp.success.forEach((r) => {
109
+ e.append(
110
+ $(
111
+ `<option ${
112
+ `${current}` === `${r[dynwhere.refname]}` ? "selected" : ""
113
+ } value="${r[dynwhere.refname]}">${
114
+ r[dynwhere.summary_field]
115
+ }</option>`
116
+ )
117
+ );
118
+ });
119
+ }
120
+ });
121
+ });
122
+
90
123
  $("[data-source-url]").each(function (ix, element) {
91
124
  const e = $(element);
92
125
  const rec = get_form_record(e);
@@ -469,7 +502,7 @@ function common_done(res, isWeb = true) {
469
502
  }
470
503
  }
471
504
 
472
- const repeaterCopyValuesToForm = (form, editor) => {
505
+ const repeaterCopyValuesToForm = (form, editor, noTriggerChange) => {
473
506
  const vs = JSON.parse(editor.getString());
474
507
 
475
508
  const setVal = (k, ix, v) => {
@@ -500,6 +533,7 @@ const repeaterCopyValuesToForm = (form, editor) => {
500
533
  if (typeof ix !== "number" || isNaN(ix)) return;
501
534
  if (ix >= vs.length) $(this).remove();
502
535
  });
536
+ !noTriggerChange && form.trigger("change");
503
537
  };
504
538
  function align_dropdown(id) {
505
539
  setTimeout(() => {
@@ -631,4 +665,3 @@ function cancel_form(form) {
631
665
  $(form).append(`<input type="hidden" name="_cancel" value="on">`);
632
666
  $(form).submit();
633
667
  }
634
-
@@ -302,4 +302,21 @@ section.range-slider input[type="range"]::-moz-focus-outer {
302
302
  table.table-inner-grid, table.table-inner-grid th, table.table-inner-grid td {
303
303
  border: 1px solid black;
304
304
  border-collapse: collapse;
305
+ }
306
+
307
+ /* https://codepen.io/pezmotion/pen/RQERdm */
308
+
309
+ .editStarRating {
310
+ direction: rtl;
311
+ unicode-bidi: bidi-override;
312
+ color: #ddd;
313
+ }
314
+ .editStarRating input {
315
+ display: none;
316
+ }
317
+ .editStarRating label:hover,
318
+ .editStarRating label:hover ~ label,
319
+ .editStarRating input:checked + label,
320
+ .editStarRating input:checked + label ~ label {
321
+ color: #ffc107;
305
322
  }
@@ -169,7 +169,7 @@ function close_saltcorn_modal() {
169
169
  if (modal) modal.dispose();
170
170
  }
171
171
 
172
- function ajax_modal(url, opts = {}) {
172
+ function ensure_modal_exists_and_closed() {
173
173
  if ($("#scmodal").length === 0) {
174
174
  $("body").append(`<div id="scmodal", class="modal">
175
175
  <div class="modal-dialog">
@@ -188,6 +188,19 @@ function ajax_modal(url, opts = {}) {
188
188
  } else if ($("#scmodal").hasClass("show")) {
189
189
  close_saltcorn_modal();
190
190
  }
191
+ }
192
+
193
+ function expand_thumbnail(img_id, filename) {
194
+ ensure_modal_exists_and_closed();
195
+ $("#scmodal .modal-body").html(
196
+ `<img src="/files/serve/${img_id}" style="width: 100%">`
197
+ );
198
+ $("#scmodal .modal-title").html(decodeURIComponent(filename));
199
+ new bootstrap.Modal($("#scmodal")).show();
200
+ }
201
+
202
+ function ajax_modal(url, opts = {}) {
203
+ ensure_modal_exists_and_closed();
191
204
  if (opts.submitReload === false) $("#scmodal").addClass("no-submit-reload");
192
205
  else $("#scmodal").removeClass("no-submit-reload");
193
206
  $.ajax(url, {
@@ -200,6 +213,7 @@ function ajax_modal(url, opts = {}) {
200
213
  (opts.onOpen || function () {})(res);
201
214
  $("#scmodal").on("hidden.bs.modal", function (e) {
202
215
  (opts.onClose || function () {})(res);
216
+ $("body").css("overflow", "");
203
217
  });
204
218
  },
205
219
  });
@@ -235,7 +249,7 @@ function saveAndContinue(e, k) {
235
249
  return false;
236
250
  }
237
251
 
238
- function applyViewConfig(e, url) {
252
+ function applyViewConfig(e, url, k) {
239
253
  var form = $(e).closest("form");
240
254
  var form_data = form.serializeArray();
241
255
  const cfg = {};
@@ -251,6 +265,9 @@ function applyViewConfig(e, url) {
251
265
  },
252
266
  data: JSON.stringify(cfg),
253
267
  error: function (request) {},
268
+ success: function (res) {
269
+ k && k(res);
270
+ },
254
271
  });
255
272
 
256
273
  return false;
@@ -296,7 +313,11 @@ function ajax_post(url, args) {
296
313
  "CSRF-Token": _sc_globalCsrf,
297
314
  },
298
315
  ...(args || {}),
299
- }).done(ajax_done);
316
+ })
317
+ .done(ajax_done)
318
+ .fail((e) =>
319
+ ajax_done(e.responseJSON || { error: "Unknown error: " + e.responseText })
320
+ );
300
321
  }
301
322
  function ajax_post_btn(e, reload_on_done, reload_delay) {
302
323
  var form = $(e).closest("form");
package/routes/admin.js CHANGED
@@ -43,6 +43,11 @@ const {
43
43
  option,
44
44
  fieldset,
45
45
  legend,
46
+ ul,
47
+ li,
48
+ ol,
49
+ script,
50
+ domReady,
46
51
  } = require("@saltcorn/markup/tags");
47
52
  const db = require("@saltcorn/data/db");
48
53
  const {
@@ -83,6 +88,7 @@ const {
83
88
  const moment = require("moment");
84
89
  const View = require("@saltcorn/data/models/view");
85
90
  const { getConfigFile } = require("@saltcorn/data/db/connect");
91
+ const os = require("os");
86
92
 
87
93
  /**
88
94
  * @type {object}
@@ -137,10 +143,6 @@ const email_form = async (req) => {
137
143
  ],
138
144
  action: "/admin/email",
139
145
  });
140
- form.submitButtonClass = "btn-outline-primary";
141
- form.submitLabel = req.__("Save");
142
- form.onChange =
143
- "remove_outline(this);$('#testemail').attr('href','#').removeClass('btn-primary').addClass('btn-outline-primary')";
144
146
  return form;
145
147
  };
146
148
 
@@ -211,8 +213,10 @@ router.post(
211
213
  flash_restart_if_required(form, req);
212
214
  await save_config_from_form(form);
213
215
 
214
- req.flash("success", req.__("Site identity settings updated"));
215
- res.redirect("/admin");
216
+ if (!req.xhr) {
217
+ req.flash("success", req.__("Site identity settings updated"));
218
+ res.redirect("/admin");
219
+ } else res.json({ success: "ok" });
216
220
  }
217
221
  })
218
222
  );
@@ -305,7 +309,8 @@ router.post(
305
309
  } else {
306
310
  await save_config_from_form(form);
307
311
  req.flash("success", req.__("Email settings updated"));
308
- res.redirect("/admin/email");
312
+ if (!req.xhr) res.redirect("/admin/email");
313
+ else res.json({ success: "ok" });
309
314
  }
310
315
  })
311
316
  );
@@ -370,7 +375,18 @@ router.get(
370
375
  ? {
371
376
  type: "card",
372
377
  title: req.__("Automated backup"),
373
- contents: div(renderForm(backupForm, req.csrfToken())),
378
+ contents: div(
379
+ renderForm(backupForm, req.csrfToken()),
380
+ a(
381
+ { href: "/admin/auto-backup-list" },
382
+ "Restore/download automated backups &raquo;"
383
+ ),
384
+ script(
385
+ domReady(
386
+ `$('#btnBackupNow').prop('disabled', $('#inputauto_backup_frequency').val()==='Never');`
387
+ )
388
+ )
389
+ ),
374
390
  }
375
391
  : { type: "blank", contents: "" },
376
392
  ],
@@ -379,6 +395,93 @@ router.get(
379
395
  })
380
396
  );
381
397
 
398
+ /**
399
+ * @name get/backup
400
+ * @function
401
+ * @memberof module:routes/admin~routes/adminRouter
402
+ */
403
+ router.get(
404
+ "/auto-backup-list",
405
+ isAdmin,
406
+ error_catcher(async (req, res) => {
407
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
408
+ if (!isRoot) {
409
+ res.redirect("/admin/backup");
410
+ return;
411
+ }
412
+ const auto_backup_directory = getState().getConfig("auto_backup_directory");
413
+ const fileNms = await fs.promises.readdir(auto_backup_directory);
414
+ const backupFiles = fileNms.filter(
415
+ (fnm) => fnm.startsWith("sc-backup") && fnm.endsWith(".zip")
416
+ );
417
+ send_admin_page({
418
+ res,
419
+ req,
420
+ active_sub: "Backup",
421
+ contents: {
422
+ above: [
423
+ {
424
+ type: "card",
425
+ title: req.__("Download automated backup"),
426
+ contents: div(
427
+ ul(
428
+ backupFiles.map((fnm) =>
429
+ li(
430
+ a(
431
+ {
432
+ href: `/admin/auto-backup-download/${encodeURIComponent(
433
+ fnm
434
+ )}`,
435
+ },
436
+ fnm
437
+ )
438
+ )
439
+ )
440
+ )
441
+ ),
442
+ },
443
+ {
444
+ type: "card",
445
+ title: req.__("Restoring automated backup"),
446
+ contents: div(
447
+ ol(
448
+ li("Download one of the backups above"),
449
+ li(
450
+ a({ href: "/admin/clear-all" }, "Clear this application"),
451
+ " ",
452
+ "(tick all boxes)"
453
+ ),
454
+ li(
455
+ "When prompted to create the first user, click the link to restore a backup"
456
+ ),
457
+ li("Select the downloaded backup file")
458
+ )
459
+ ),
460
+ },
461
+ ],
462
+ },
463
+ });
464
+ })
465
+ );
466
+
467
+ router.get(
468
+ "/auto-backup-download/:filename",
469
+ isAdmin,
470
+ error_catcher(async (req, res) => {
471
+ const { filename } = req.params;
472
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
473
+ if (
474
+ !isRoot ||
475
+ !(filename.startsWith("sc-backup") && filename.endsWith(".zip"))
476
+ ) {
477
+ res.redirect("/admin/backup");
478
+ return;
479
+ }
480
+ const auto_backup_directory = getState().getConfig("auto_backup_directory");
481
+ res.download(path.join(auto_backup_directory, filename), filename);
482
+ })
483
+ );
484
+
382
485
  /**
383
486
  * Auto backup Form
384
487
  * @param {object} req
@@ -387,9 +490,8 @@ router.get(
387
490
  const autoBackupForm = (req) =>
388
491
  new Form({
389
492
  action: "/admin/set-auto-backup",
390
- submitButtonClass: "btn-outline-primary",
391
- onChange: "remove_outline(this)",
392
- submitLabel: "Save settings",
493
+ onChange: `saveAndContinue(this);$('#btnBackupNow').prop('disabled', $('#inputauto_backup_frequency').val()==='Never');`,
494
+ noSubmitButton: true,
393
495
  additionalButtons: [
394
496
  {
395
497
  label: "Backup now",
@@ -458,7 +560,8 @@ router.post(
458
560
  } else {
459
561
  await save_config_from_form(form);
460
562
  req.flash("success", req.__("Backup settings updated"));
461
- res.redirect("/admin/backup");
563
+ if (!req.xhr) res.redirect("/admin/backup");
564
+ else res.json({ success: "ok" });
462
565
  }
463
566
  })
464
567
  );
@@ -1141,22 +1244,30 @@ router.post(
1141
1244
  "error",
1142
1245
  req.__("Please select at least one platform (android or iOS).")
1143
1246
  );
1144
- return res.redirect("/admin/system");
1247
+ return res.redirect("/admin/build-mobile-app");
1145
1248
  }
1146
1249
  if (!androidPlatform && useDocker) {
1147
1250
  req.flash("error", req.__("Only the android build supports docker."));
1148
- return res.redirect("/admin/system");
1251
+ return res.redirect("/admin/build-mobile-app");
1149
1252
  }
1150
1253
  if (appFile && !appFile.endsWith(".apk")) appFile = `${appFile}.apk`;
1151
1254
  const appOut = path.join(__dirname, "..", "mobile-app-out");
1152
- const spawnParams = ["build-app", "-v", entryView, "-c", appOut];
1255
+ const spawnParams = [
1256
+ "build-app",
1257
+ "-v",
1258
+ entryView,
1259
+ "-c",
1260
+ appOut,
1261
+ "-b",
1262
+ `${os.userInfo().homedir}/mobile_app_build`,
1263
+ ];
1153
1264
  if (useDocker) spawnParams.push("-d");
1154
1265
  if (androidPlatform) spawnParams.push("-p", "android");
1155
1266
  if (iOSPlatform) spawnParams.push("-p", "ios");
1156
1267
  if (appFile) spawnParams.push("-a", appFile);
1157
1268
  if (serverURL) spawnParams.push("-s", serverURL);
1158
1269
  const child = spawn("saltcorn", spawnParams, {
1159
- stdio: ["ignore", "pipe", process.stderr],
1270
+ stdio: ["ignore", "pipe", "pipe"],
1160
1271
  cwd: ".",
1161
1272
  });
1162
1273
  const childOutputs = [];
@@ -1164,8 +1275,12 @@ router.post(
1164
1275
  // console.log(data.toString());
1165
1276
  childOutputs.push(data.toString());
1166
1277
  });
1167
- child.on("exit", async function (code, signal) {
1168
- if (code === 0) {
1278
+ child.stderr.on("data", (data) => {
1279
+ // console.log(data.toString());
1280
+ childOutputs.push(data.toString());
1281
+ });
1282
+ child.on("exit", async function (exitCode, signal) {
1283
+ if (exitCode === 0) {
1169
1284
  const file = await File.from_existing_file(
1170
1285
  appOut,
1171
1286
  appFile ? appFile : "app-debug.apk",
@@ -1187,9 +1302,11 @@ router.post(
1187
1302
  {
1188
1303
  type: "card",
1189
1304
  title: req.__("Build Result"),
1190
- contents: div("Unable to build the app"),
1305
+ contents: div(
1306
+ "Unable to build the app:",
1307
+ pre(code(childOutputs.join("<br/>")))
1308
+ ),
1191
1309
  },
1192
- childOutputs.join("<br/>"),
1193
1310
  ],
1194
1311
  });
1195
1312
  });
@@ -1201,9 +1318,12 @@ router.post(
1201
1318
  {
1202
1319
  type: "card",
1203
1320
  title: req.__("Build Result"),
1204
- contents: div("Unable to build the app"),
1321
+ contents: div(
1322
+ p("Unable to build the app:"),
1323
+ pre(code(message)),
1324
+ pre(code(stack))
1325
+ ),
1205
1326
  },
1206
- `${message} <br/> ${stack}`,
1207
1327
  ],
1208
1328
  });
1209
1329
  });
@@ -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
@@ -488,6 +488,7 @@ router.get(
488
488
  },
489
489
  {
490
490
  type: "card",
491
+ class: "mt-0",
491
492
  title: wizardCardTitle(field.label, wf, wfres),
492
493
  contents: renderForm(wfres.renderForm, req.csrfToken()),
493
494
  },
@@ -524,6 +525,7 @@ router.get(
524
525
  },
525
526
  {
526
527
  type: "card",
528
+ class: "mt-0",
527
529
  title: wizardCardTitle(req.__(`New field`), wf, wfres),
528
530
  contents: renderForm(wfres.renderForm, req.csrfToken()),
529
531
  },
@@ -589,6 +591,7 @@ router.post(
589
591
  },
590
592
  {
591
593
  type: "card",
594
+ class: "mt-0",
592
595
  title: wizardCardTitle(
593
596
  wfres.context.label || req.__("New field"),
594
597
  wf,