@saltcorn/server 0.7.3-beta.3 → 0.7.3-beta.6

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/app.js CHANGED
@@ -227,7 +227,11 @@ const getApp = async (opts = {}) => {
227
227
  passport.use(
228
228
  new JwtStrategy(jwtOpts, (jwt_payload, done) => {
229
229
  User.findOne({ email: jwt_payload.sub }).then((u) => {
230
- if (u) {
230
+ if (
231
+ u &&
232
+ u.last_mobile_login &&
233
+ u.last_mobile_login <= jwt_payload.iat
234
+ ) {
231
235
  return done(null, {
232
236
  email: u.email,
233
237
  id: u.id,
package/auth/routes.js CHANGED
@@ -203,6 +203,7 @@ const loginWithJwt = async (req, res) => {
203
203
  const { email, password } = req.query;
204
204
  const user = await User.findOne({ email });
205
205
  if (user && user.checkPassword(password)) {
206
+ const now = new Date().valueOf();
206
207
  const jwt_secret = db.connectObj.jwt_secret;
207
208
  const token = jwt.sign(
208
209
  {
@@ -210,9 +211,11 @@ const loginWithJwt = async (req, res) => {
210
211
  role_id: user.role_id,
211
212
  iss: "saltcorn@saltcorn",
212
213
  aud: "saltcorn-mobile-app",
214
+ iat: now,
213
215
  },
214
216
  jwt_secret
215
217
  );
218
+ if (!user.last_mobile_login) user.updateLastMobileLogin(now);
216
219
  res.json(token);
217
220
  }
218
221
  };
@@ -249,18 +252,24 @@ router.get(
249
252
  * @function
250
253
  * @memberof module:auth/routes~routesRouter
251
254
  */
252
- router.get("/logout", (req, res, next) => {
253
- req.logout();
254
- if (req.session.destroy)
255
- req.session.destroy((err) => {
256
- if (err) return next(err);
255
+ router.get("/logout", async (req, res, next) => {
256
+ if (req.smr && req.user?.id) {
257
+ const user = await User.findOne({ id: req.user.id });
258
+ await user.updateLastMobileLogin(null);
259
+ res.json({ success: true });
260
+ } else if (req.logout) {
261
+ req.logout();
262
+ if (req.session.destroy)
263
+ req.session.destroy((err) => {
264
+ if (err) return next(err);
265
+ req.logout();
266
+ res.redirect("/auth/login");
267
+ });
268
+ else {
257
269
  req.logout();
270
+ req.session = null;
258
271
  res.redirect("/auth/login");
259
- });
260
- else {
261
- req.logout();
262
- req.session = null;
263
- res.redirect("/auth/login");
272
+ }
264
273
  }
265
274
  });
266
275
 
@@ -978,6 +987,11 @@ router.post(
978
987
  }
979
988
  Trigger.emitEvent("Login", null, req.user);
980
989
  req.flash("success", req.__("Welcome, %s!", req.user.email));
990
+ if (req.smr) {
991
+ const dbUser = await User.findOne({ id: req.user.id });
992
+ if (!dbUser.last_mobile_login)
993
+ await dbUser.updateLastMobileLogin(new Date());
994
+ }
981
995
  if (getState().get2FApolicy(req.user) === "Mandatory") {
982
996
  res.redirect("/auth/twofa/setup/totp");
983
997
  } else res.redirect("/");
@@ -1010,6 +1024,17 @@ router.get(
1010
1024
  })
1011
1025
  );
1012
1026
 
1027
+ /*
1028
+ returns if 'req.user' is an authenticated user
1029
+ */
1030
+ router.get(
1031
+ "/authenticated",
1032
+ error_catcher((req, res, next) => {
1033
+ const isAuth = req.user && req.user.id ? true : false;
1034
+ res.json({ authenticated: isAuth });
1035
+ })
1036
+ );
1037
+
1013
1038
  /**
1014
1039
  * @name post/login-with/:method
1015
1040
  * @function
@@ -1195,6 +1220,7 @@ const userSettings = async ({ req, res, pwform, user }) => {
1195
1220
  ? [
1196
1221
  {
1197
1222
  type: "card",
1223
+ class: "mt-0",
1198
1224
  title: userSetsName,
1199
1225
  contents: usersets,
1200
1226
  },
@@ -1203,6 +1229,7 @@ const userSettings = async ({ req, res, pwform, user }) => {
1203
1229
  {
1204
1230
  type: "card",
1205
1231
  title: req.__("User"),
1232
+ class: !usersets && "mt-0",
1206
1233
  contents: table(
1207
1234
  tbody(
1208
1235
  tr(
package/locales/en.json CHANGED
@@ -916,5 +916,8 @@
916
916
  "Delete old backup files in this directory after the set number of days": "Delete old backup files in this directory after the set number of days",
917
917
  "Mobile app": "Mobile app",
918
918
  "Build mobile app": "Build mobile app",
919
- "Build Result": "Build Result"
919
+ "Build Result": "Build Result",
920
+ "Download automated backup": "Download automated backup",
921
+ "Restoring automated backup": "Restoring automated backup",
922
+ "No errors detected during configuration check": "No errors detected during configuration check"
920
923
  }
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(
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-beta.6",
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-beta.6",
10
+ "@saltcorn/builder": "0.7.3-beta.6",
11
+ "@saltcorn/data": "0.7.3-beta.6",
12
+ "@saltcorn/admin-models": "0.7.3-beta.6",
13
+ "@saltcorn/markup": "0.7.3-beta.6",
14
+ "@saltcorn/sbadmin2": "0.7.3-beta.6",
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(),
@@ -164,7 +167,7 @@ function MenuEditor(e, t) {
164
167
  t.remove()),
165
168
  MenuEditor.updateButtons(s),
166
169
  s.updateLevels();
167
- l.onUpdate();
170
+ l.onUpdate();
168
171
  }),
169
172
  s.on("click", ".btnIn", function (e) {
170
173
  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);
@@ -631,4 +664,3 @@ function cancel_form(form) {
631
664
  $(form).append(`<input type="hidden" name="_cancel" value="on">`);
632
665
  $(form).submit();
633
666
  }
634
-
@@ -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, {
@@ -296,7 +309,11 @@ function ajax_post(url, args) {
296
309
  "CSRF-Token": _sc_globalCsrf,
297
310
  },
298
311
  ...(args || {}),
299
- }).done(ajax_done);
312
+ })
313
+ .done(ajax_done)
314
+ .fail((e) =>
315
+ ajax_done(e.responseJSON || { error: "Unknown error: " + e.responseText })
316
+ );
300
317
  }
301
318
  function ajax_post_btn(e, reload_on_done, reload_delay) {
302
319
  var form = $(e).closest("form");
package/routes/admin.js CHANGED
@@ -43,6 +43,9 @@ const {
43
43
  option,
44
44
  fieldset,
45
45
  legend,
46
+ ul,
47
+ li,
48
+ ol,
46
49
  } = require("@saltcorn/markup/tags");
47
50
  const db = require("@saltcorn/data/db");
48
51
  const {
@@ -370,7 +373,13 @@ router.get(
370
373
  ? {
371
374
  type: "card",
372
375
  title: req.__("Automated backup"),
373
- contents: div(renderForm(backupForm, req.csrfToken())),
376
+ contents: div(
377
+ renderForm(backupForm, req.csrfToken()),
378
+ a(
379
+ { href: "/admin/auto-backup-list" },
380
+ "Restore/download automated backups &raquo;"
381
+ )
382
+ ),
374
383
  }
375
384
  : { type: "blank", contents: "" },
376
385
  ],
@@ -379,6 +388,93 @@ router.get(
379
388
  })
380
389
  );
381
390
 
391
+ /**
392
+ * @name get/backup
393
+ * @function
394
+ * @memberof module:routes/admin~routes/adminRouter
395
+ */
396
+ router.get(
397
+ "/auto-backup-list",
398
+ isAdmin,
399
+ error_catcher(async (req, res) => {
400
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
401
+ if (!isRoot) {
402
+ res.redirect("/admin/backup");
403
+ return;
404
+ }
405
+ const auto_backup_directory = getState().getConfig("auto_backup_directory");
406
+ const fileNms = await fs.promises.readdir(auto_backup_directory);
407
+ const backupFiles = fileNms.filter(
408
+ (fnm) => fnm.startsWith("sc-backup") && fnm.endsWith(".zip")
409
+ );
410
+ send_admin_page({
411
+ res,
412
+ req,
413
+ active_sub: "Backup",
414
+ contents: {
415
+ above: [
416
+ {
417
+ type: "card",
418
+ title: req.__("Download automated backup"),
419
+ contents: div(
420
+ ul(
421
+ backupFiles.map((fnm) =>
422
+ li(
423
+ a(
424
+ {
425
+ href: `/admin/auto-backup-download/${encodeURIComponent(
426
+ fnm
427
+ )}`,
428
+ },
429
+ fnm
430
+ )
431
+ )
432
+ )
433
+ )
434
+ ),
435
+ },
436
+ {
437
+ type: "card",
438
+ title: req.__("Restoring automated backup"),
439
+ contents: div(
440
+ ol(
441
+ li("Download one of the backups above"),
442
+ li(
443
+ a({ href: "/admin/clear-all" }, "Clear this application"),
444
+ " ",
445
+ "(tick all boxes)"
446
+ ),
447
+ li(
448
+ "When prompted to create the first user, click the link to restore a backup"
449
+ ),
450
+ li("Select the downloaded backup file")
451
+ )
452
+ ),
453
+ },
454
+ ],
455
+ },
456
+ });
457
+ })
458
+ );
459
+
460
+ router.get(
461
+ "/auto-backup-download/:filename",
462
+ isAdmin,
463
+ error_catcher(async (req, res) => {
464
+ const { filename } = req.params;
465
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
466
+ if (
467
+ !isRoot ||
468
+ !(filename.startsWith("sc-backup") && filename.endsWith(".zip"))
469
+ ) {
470
+ res.redirect("/admin/backup");
471
+ return;
472
+ }
473
+ const auto_backup_directory = getState().getConfig("auto_backup_directory");
474
+ res.download(path.join(auto_backup_directory, filename), filename);
475
+ })
476
+ );
477
+
382
478
  /**
383
479
  * Auto backup Form
384
480
  * @param {object} req
@@ -1141,15 +1237,23 @@ router.post(
1141
1237
  "error",
1142
1238
  req.__("Please select at least one platform (android or iOS).")
1143
1239
  );
1144
- return res.redirect("/admin/system");
1240
+ return res.redirect("/admin/build-mobile-app");
1145
1241
  }
1146
1242
  if (!androidPlatform && useDocker) {
1147
1243
  req.flash("error", req.__("Only the android build supports docker."));
1148
- return res.redirect("/admin/system");
1244
+ return res.redirect("/admin/build-mobile-app");
1149
1245
  }
1150
1246
  if (appFile && !appFile.endsWith(".apk")) appFile = `${appFile}.apk`;
1151
1247
  const appOut = path.join(__dirname, "..", "mobile-app-out");
1152
- const spawnParams = ["build-app", "-v", entryView, "-c", appOut];
1248
+ const spawnParams = [
1249
+ "build-app",
1250
+ "-v",
1251
+ entryView,
1252
+ "-c",
1253
+ appOut,
1254
+ "-b",
1255
+ "/tmp/mobile_app_build",
1256
+ ];
1153
1257
  if (useDocker) spawnParams.push("-d");
1154
1258
  if (androidPlatform) spawnParams.push("-p", "android");
1155
1259
  if (iOSPlatform) spawnParams.push("-p", "ios");
@@ -1164,8 +1268,8 @@ router.post(
1164
1268
  // console.log(data.toString());
1165
1269
  childOutputs.push(data.toString());
1166
1270
  });
1167
- child.on("exit", async function (code, signal) {
1168
- if (code === 0) {
1271
+ child.on("exit", async function (exitCode, signal) {
1272
+ if (exitCode === 0) {
1169
1273
  const file = await File.from_existing_file(
1170
1274
  appOut,
1171
1275
  appFile ? appFile : "app-debug.apk",
@@ -1187,9 +1291,11 @@ router.post(
1187
1291
  {
1188
1292
  type: "card",
1189
1293
  title: req.__("Build Result"),
1190
- contents: div("Unable to build the app"),
1294
+ contents: div(
1295
+ "Unable to build the app:",
1296
+ pre(code(childOutputs.join("<br/>")))
1297
+ ),
1191
1298
  },
1192
- childOutputs.join("<br/>"),
1193
1299
  ],
1194
1300
  });
1195
1301
  });
@@ -1201,9 +1307,12 @@ router.post(
1201
1307
  {
1202
1308
  type: "card",
1203
1309
  title: req.__("Build Result"),
1204
- contents: div("Unable to build the app"),
1310
+ contents: div(
1311
+ p("Unable to build the app:"),
1312
+ pre(code(message)),
1313
+ pre(code(stack))
1314
+ ),
1205
1315
  },
1206
- `${message} <br/> ${stack}`,
1207
1316
  ],
1208
1317
  });
1209
1318
  });
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,
package/routes/files.js CHANGED
@@ -184,11 +184,11 @@ router.get(
184
184
  * @function
185
185
  */
186
186
  router.get(
187
- "/resize/:id/:width_str",
187
+ "/resize/:id/:width_str/:height_str?",
188
188
  error_catcher(async (req, res) => {
189
189
  const role = req.user && req.user.id ? req.user.role_id : 10;
190
190
  const user_id = req.user && req.user.id;
191
- const { id, width_str } = req.params;
191
+ const { id, width_str, height_str } = req.params;
192
192
  let file;
193
193
  if (typeof strictParseInt(id) !== "undefined")
194
194
  file = await File.findOne({ id });
@@ -208,15 +208,17 @@ router.get(
208
208
  if (file.s3_store) s3storage.serveObject(file, res, false);
209
209
  else {
210
210
  const width = strictParseInt(width_str);
211
+ const height = height_str ? strictParseInt(height_str) : null;
211
212
  if (!width) {
212
213
  res.sendFile(file.location);
213
214
  return;
214
215
  }
215
- const fnm = `${file.location}_w${width}`;
216
+ const fnm = `${file.location}_w${width}${height ? `_h${height}` : ""}`;
216
217
  if (!fs.existsSync(fnm)) {
217
218
  await resizer({
218
219
  fromFileName: file.location,
219
220
  width,
221
+ height,
220
222
  toFileName: fnm,
221
223
  });
222
224
  }
@@ -49,7 +49,7 @@ const tableTable = (tables, req) =>
49
49
  */
50
50
  const tableCard = (tables, req) => ({
51
51
  type: "card",
52
- class: "welcome-page-entity-list",
52
+ class: "welcome-page-entity-list mt-1",
53
53
  title: link("/table", req.__("Tables")),
54
54
  contents:
55
55
  (tables.length <= 1
@@ -102,7 +102,7 @@ const viewTable = (views, req) =>
102
102
  const viewCard = (views, req) => ({
103
103
  type: "card",
104
104
  title: link("/viewedit", req.__("Views")),
105
- class: "welcome-page-entity-list",
105
+ class: "welcome-page-entity-list mt-1",
106
106
  bodyClass: "py-0 pe-0",
107
107
  contents:
108
108
  (views.length <= 1
@@ -156,7 +156,7 @@ const pageTable = (pages, req) =>
156
156
  const pageCard = (pages, req) => ({
157
157
  type: "card",
158
158
  title: link("/pageedit", req.__("Pages")),
159
- class: "welcome-page-entity-list",
159
+ class: "welcome-page-entity-list mt-1",
160
160
  contents:
161
161
  (pages.length <= 1
162
162
  ? p(
@@ -369,9 +369,9 @@ const welcome_page = async (req) => {
369
369
  above: [
370
370
  {
371
371
  besides: [
372
- pageCard(pages, req),
373
- viewCard(views, req),
374
372
  tableCard(tables, req),
373
+ viewCard(views, req),
374
+ pageCard(pages, req),
375
375
  ],
376
376
  },
377
377
  {
@@ -380,7 +380,7 @@ const welcome_page = async (req) => {
380
380
  type: "card",
381
381
  //title: req.__("Install pack"),
382
382
  bodyClass: "py-0 pe-0",
383
- class: "welcome-page-entity-list",
383
+ class: "welcome-page-entity-list mt-2",
384
384
 
385
385
  tabContents:
386
386
  triggers.length > 0
@@ -399,7 +399,7 @@ const welcome_page = async (req) => {
399
399
  type: "card",
400
400
  //title: req.__("Learn"),
401
401
  bodyClass: "py-0 pe-0",
402
- class: "welcome-page-entity-list",
402
+ class: "welcome-page-entity-list mt-2",
403
403
  tabContents:
404
404
  users.length > 4
405
405
  ? {
@@ -326,6 +326,7 @@ router.get(
326
326
  {
327
327
  type: "card",
328
328
  title: req.__("Your pages"),
329
+ class: "mt-0",
329
330
  contents: getPageList(pages, roles, req),
330
331
  },
331
332
  {
package/routes/plugins.js CHANGED
@@ -506,6 +506,7 @@ const plugin_store_html = (items, req) => {
506
506
  },
507
507
  {
508
508
  type: "card",
509
+ class: "mt-0",
509
510
  contents: div(
510
511
  { class: "d-flex justify-content-between" },
511
512
  storeNavPills(req),
package/routes/tables.js CHANGED
@@ -448,6 +448,7 @@ router.get(
448
448
  },
449
449
  {
450
450
  type: "card",
451
+ class: "mt-0",
451
452
  title: cardHeaderTabs([
452
453
  { label: req.__("Your tables"), href: "/table" },
453
454
  {
@@ -636,7 +637,9 @@ router.get(
636
637
  }
637
638
  var viewCard;
638
639
  if (fields.length > 0) {
639
- const views = await View.find({ table_id: table.id });
640
+ const views = await View.find(
641
+ table.external ? { exttable_name: table.name } : { table_id: table.id }
642
+ );
640
643
  var viewCardContents;
641
644
  if (views.length > 0) {
642
645
  viewCardContents = mkTable(
@@ -801,15 +804,12 @@ router.get(
801
804
  type: "breadcrumbs",
802
805
  crumbs: [
803
806
  { text: req.__("Tables"), href: "/table" },
804
- { text: table.name },
807
+ { text: span({ class: "fw-bold text-body" }, table.name) },
805
808
  ],
806
809
  },
807
- {
808
- type: "pageHeader",
809
- title: req.__(`%s table`, table.name),
810
- },
811
810
  {
812
811
  type: "card",
812
+ class: "mt-0",
813
813
  title: req.__("Fields"),
814
814
  contents: fieldCard,
815
815
  },
@@ -1076,6 +1076,7 @@ router.get(
1076
1076
  },
1077
1077
  {
1078
1078
  type: "card",
1079
+ class: "mt-0",
1079
1080
  title: cardHeaderTabs([
1080
1081
  { label: req.__("Your tables"), href: "/table", active: true },
1081
1082
  {
@@ -197,6 +197,7 @@ router.get(
197
197
  },
198
198
  {
199
199
  type: "card",
200
+ class: "mt-0",
200
201
  title: req.__("Your views"),
201
202
  contents: [
202
203
  viewMarkup,
@@ -378,6 +379,7 @@ router.get(
378
379
  },
379
380
  {
380
381
  type: "card",
382
+ class: "mt-0",
381
383
  title: req.__(`Edit %s view`, viewname),
382
384
  contents: renderForm(form, req.csrfToken()),
383
385
  },
@@ -415,6 +417,7 @@ router.get(
415
417
  },
416
418
  {
417
419
  type: "card",
420
+ class: "mt-0",
418
421
  title: req.__(`Create view`),
419
422
  contents: renderForm(form, req.csrfToken()),
420
423
  },
@@ -452,6 +455,7 @@ router.post(
452
455
  },
453
456
  {
454
457
  type: "card",
458
+ class: "mt-0",
455
459
  title: req.__(`Edit view`),
456
460
  contents: renderForm(form, req.csrfToken()),
457
461
  },
@@ -527,6 +531,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
527
531
  },
528
532
  {
529
533
  type: noCard ? "container" : "card",
534
+ class: !noCard && "mt-0",
530
535
  title: wfres.title,
531
536
  contents,
532
537
  },
@@ -10,6 +10,7 @@ const {
10
10
  toInclude,
11
11
  toNotInclude,
12
12
  } = require("../auth/testhelp");
13
+ const { getState } = require("@saltcorn/data/db/state");
13
14
 
14
15
  afterAll(db.close);
15
16
 
@@ -24,6 +25,8 @@ describe("tenant routes", () => {
24
25
  if (!db.isSQLite) {
25
26
  it("shows create form", async () => {
26
27
  db.enable_multi_tenant();
28
+ await getState().setConfig("role_to_create_tenant", "10");
29
+
27
30
  const app = await getApp({ disableCsrf: true });
28
31
  await request(app).get("/tenant/create").expect(toInclude("subdomain"));
29
32
  });
@@ -37,6 +40,8 @@ describe("tenant routes", () => {
37
40
  });
38
41
  it("creates tenant with capital letter", async () => {
39
42
  db.enable_multi_tenant();
43
+ await getState().setConfig("role_to_create_tenant", "10");
44
+
40
45
  const app = await getApp({ disableCsrf: true });
41
46
  await request(app)
42
47
  .post("/tenant/create")
@@ -46,6 +51,7 @@ describe("tenant routes", () => {
46
51
  });
47
52
  it("rejects existing tenant", async () => {
48
53
  db.enable_multi_tenant();
54
+ await getState().setConfig("role_to_create_tenant", "10");
49
55
  const app = await getApp({ disableCsrf: true });
50
56
  await request(app)
51
57
  .post("/tenant/create")