@saltcorn/server 0.8.5-beta.1 → 0.8.5-beta.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/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.5-beta.1",
3
+ "version": "0.8.5-beta.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.8.5-beta.1",
10
- "@saltcorn/builder": "0.8.5-beta.1",
11
- "@saltcorn/data": "0.8.5-beta.1",
12
- "@saltcorn/admin-models": "0.8.5-beta.1",
13
- "@saltcorn/filemanager": "0.8.5-beta.1",
14
- "@saltcorn/markup": "0.8.5-beta.1",
15
- "@saltcorn/sbadmin2": "0.8.5-beta.1",
9
+ "@saltcorn/base-plugin": "0.8.5-beta.3",
10
+ "@saltcorn/builder": "0.8.5-beta.3",
11
+ "@saltcorn/data": "0.8.5-beta.3",
12
+ "@saltcorn/admin-models": "0.8.5-beta.3",
13
+ "@saltcorn/filemanager": "0.8.5-beta.3",
14
+ "@saltcorn/markup": "0.8.5-beta.3",
15
+ "@saltcorn/sbadmin2": "0.8.5-beta.3",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -52,6 +52,7 @@
52
52
  "qrcode": "1.5.1",
53
53
  "resize-with-sharp-or-jimp": "0.1.6",
54
54
  "socket.io": "4.6.0",
55
+ "systeminformation": "^5.11.12",
55
56
  "thirty-two": "1.0.2",
56
57
  "tmp-promise": "^3.0.2",
57
58
  "uuid": "^8.2.0",
@@ -145,7 +145,7 @@ function apply_showif() {
145
145
  });
146
146
  element.dispatchEvent(new Event("RefreshSelectOptions"));
147
147
  if (e.hasClass("selectized") && $().selectize) {
148
- e.selectize()[0].selectize.clearOptions();
148
+ e.selectize()[0].selectize.clearOptions(true);
149
149
  e.selectize()[0].selectize.addOption(dataOptions);
150
150
  if (typeof currentDataOption !== "undefined")
151
151
  e.selectize()[0].selectize.setValue(currentDataOption);
@@ -428,6 +428,10 @@ function initialize_page() {
428
428
  var key = $(this).attr("data-inline-edit-field") || "value";
429
429
  var ajax = !!$(this).attr("data-inline-edit-ajax");
430
430
  var type = $(this).attr("data-inline-edit-type");
431
+ var schema = $(this).attr("data-inline-edit-schema");
432
+ if (schema) {
433
+ schema = JSON.parse(decodeURIComponent(schema));
434
+ }
431
435
  var is_key = type?.startsWith("Key:");
432
436
  const opts = encodeURIComponent(
433
437
  JSON.stringify({
@@ -438,12 +442,16 @@ function initialize_page() {
438
442
  current_label: $(this).attr("data-inline-edit-current-label"),
439
443
  type,
440
444
  is_key,
445
+ schema,
441
446
  })
442
447
  );
443
- if (is_key) {
444
- const [tblName, target] = type.replace("Key:", "").split(".");
448
+ const doAjaxOptionsFetch = (tblName, target) => {
445
449
  $.ajax(`/api/${tblName}`).then((resp) => {
446
450
  if (resp.success) {
451
+ resp.success.sort((a, b) =>
452
+ a[target]?.toLowerCase?.() > b[target]?.toLowerCase?.() ? 1 : -1
453
+ );
454
+
447
455
  const selopts = resp.success.map(
448
456
  (r) =>
449
457
  `<option ${current == r.id ? `selected ` : ``}value="${
@@ -463,6 +471,14 @@ function initialize_page() {
463
471
  );
464
472
  }
465
473
  });
474
+ };
475
+ if (type === "JSON" && schema && schema.type.startsWith("Key to ")) {
476
+ const tblName = schema.type.replace("Key to ", "");
477
+ const target = schema.summary_field || "id";
478
+ doAjaxOptionsFetch(tblName, target);
479
+ } else if (is_key) {
480
+ const [tblName, target] = type.replace("Key:", "").split(".");
481
+ doAjaxOptionsFetch(tblName, target);
466
482
  } else
467
483
  $(this).replaceWith(
468
484
  `<form method="post" action="${url}" ${
@@ -590,7 +606,10 @@ $(initialize_page);
590
606
  function cancel_inline_edit(e, opts1) {
591
607
  var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
592
608
  var form = $(e.target).closest("form");
593
-
609
+ var json_fk_opt;
610
+ if (opts.schema) {
611
+ json_fk_opt = form.find(`option[value="${opts.current}"]`).text();
612
+ }
594
613
  form.replaceWith(`<div
595
614
  data-inline-edit-field="${opts.key}"
596
615
  ${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
@@ -601,8 +620,17 @@ function cancel_inline_edit(e, opts1) {
601
620
  ? `data-inline-edit-current-label="${opts.current_label}"`
602
621
  : ""
603
622
  }
623
+ ${
624
+ opts.schema
625
+ ? `data-inline-edit-schema="${encodeURIComponent(
626
+ JSON.stringify(opts.schema)
627
+ )}"`
628
+ : ""
629
+ }
604
630
  data-inline-edit-dest-url="${opts.url}">
605
- <span class="current">${opts.current_label || opts.current}</span>
631
+ <span class="current">${
632
+ json_fk_opt || opts.current_label || opts.current
633
+ }</span>
606
634
  <i class="editicon fas fa-edit ms-1"></i>
607
635
  </div>`);
608
636
  initialize_page();
@@ -624,15 +652,23 @@ function inline_ajax_submit(e, opts1) {
624
652
  success: function (res) {
625
653
  if (opts) {
626
654
  let rawVal = formDataArray.find((f) => f.name == opts.key).value;
627
- let val = opts.is_key
628
- ? form.find("select").find("option:selected").text()
629
- : rawVal;
655
+ let val =
656
+ opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
657
+ ? form.find("select").find("option:selected").text()
658
+ : rawVal;
630
659
 
631
660
  $(e.target).replaceWith(`<div
632
661
  data-inline-edit-field="${opts.key}"
633
662
  ${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
634
663
  ${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
635
664
  ${opts.current ? `data-inline-edit-current="${rawVal}"` : ""}
665
+ ${
666
+ opts.schema
667
+ ? `data-inline-edit-schema="${encodeURIComponent(
668
+ JSON.stringify(opts.schema)
669
+ )}"`
670
+ : ""
671
+ }
636
672
  ${opts.current_label ? `data-inline-edit-current-label="${val}"` : ""}
637
673
  data-inline-edit-dest-url="${opts.url}">
638
674
  <span class="current">${val}</span>
@@ -227,7 +227,6 @@ footer.bs-mobile-nav-footer {
227
227
 
228
228
  .containerbgimage {
229
229
  position: absolute;
230
- top: 0;
231
230
  left: 0;
232
231
  width: 100%;
233
232
  height: 100%;
@@ -372,7 +371,7 @@ table.table-inner-grid td {
372
371
  }
373
372
 
374
373
  .join-table-header {
375
- padding: 0.25rem 1rem;
374
+ padding: 0.25rem 1rem;
376
375
  margin-bottom: 0 !important;
377
376
  text-decoration: underline;
378
- }
377
+ }
@@ -320,6 +320,7 @@ function saveAndContinue(e, k) {
320
320
  data: form_data,
321
321
  success: function (res) {
322
322
  ajax_indicator(false);
323
+ form.parent().find(".full-form-error").text("");
323
324
  if (res.id && form.find("input[name=id")) {
324
325
  form.append(
325
326
  `<input type="hidden" class="form-control " name="id" value="${res.id}">`
@@ -327,9 +328,23 @@ function saveAndContinue(e, k) {
327
328
  }
328
329
  },
329
330
  error: function (request) {
330
- $("#page-inner-content").html(request.responseText);
331
+ var ct = request.getResponseHeader("content-type") || "";
332
+ if (ct.startsWith && ct.startsWith("application/json")) {
333
+ var errorArea = form.parent().find(".full-form-error");
334
+ if (errorArea.length) {
335
+ errorArea.text(request.responseJSON.error);
336
+ } else {
337
+ form
338
+ .parent()
339
+ .append(
340
+ `<p class="text-danger full-form-error">${request.responseJSON.error}</p>`
341
+ );
342
+ }
343
+ } else {
344
+ $("#page-inner-content").html(request.responseText);
345
+ initialize_page();
346
+ }
331
347
  ajax_indicate_error(e, request);
332
- initialize_page();
333
348
  },
334
349
  complete: function () {
335
350
  if (k) k();
package/routes/actions.js CHANGED
@@ -41,6 +41,7 @@ const {
41
41
  h6,
42
42
  pre,
43
43
  text,
44
+ i,
44
45
  } = require("@saltcorn/markup/tags");
45
46
  const Table = require("@saltcorn/data/models/table");
46
47
  const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
@@ -510,17 +511,20 @@ router.post(
510
511
  });
511
512
  form.validate(req.body);
512
513
  if (form.hasErrors) {
513
- send_events_page({
514
- res,
515
- req,
516
- active_sub: "Triggers",
517
- sub2_page: "Configure",
518
- contents: {
519
- type: "card",
520
- title: req.__("Configure trigger"),
521
- contents: renderForm(form, req.csrfToken()),
522
- },
523
- });
514
+ if (req.xhr) {
515
+ res.status(400).json({ error: form.errorSummary });
516
+ } else
517
+ send_events_page({
518
+ res,
519
+ req,
520
+ active_sub: "Triggers",
521
+ sub2_page: "Configure",
522
+ contents: {
523
+ type: "card",
524
+ title: req.__("Configure trigger"),
525
+ contents: renderForm(form, req.csrfToken()),
526
+ },
527
+ });
524
528
  } else {
525
529
  await Trigger.update(trigger.id, { configuration: form.values });
526
530
  if (req.xhr) {
@@ -598,6 +602,7 @@ router.get(
598
602
  user: req.user,
599
603
  });
600
604
  } catch (e) {
605
+ console.error(e);
601
606
  fakeConsole.error(e.message);
602
607
  }
603
608
  if (output.length === 0) {
@@ -622,8 +627,16 @@ router.get(
622
627
  div({ class: "testrunoutput" }, output),
623
628
 
624
629
  a(
625
- { href: `/actions`, class: "mt-4 btn btn-primary" },
630
+ { href: `/actions`, class: "mt-4 btn btn-primary me-1" },
626
631
  "&laquo;&nbsp;" + req.__("back to actions")
632
+ ),
633
+ a(
634
+ {
635
+ href: `/actions/testrun/${id}`,
636
+ class: "ms-1 mt-4 btn btn-primary",
637
+ },
638
+ i({ class: "fas fa-redo me-1" }),
639
+ req.__("Re-run")
627
640
  )
628
641
  ),
629
642
  },
package/routes/admin.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  error_catcher,
11
11
  getGitRevision,
12
12
  setTenant,
13
+ get_sys_info,
13
14
  } = require("./utils.js");
14
15
  const Table = require("@saltcorn/data/models/table");
15
16
  const Plugin = require("@saltcorn/data/models/plugin");
@@ -40,6 +41,7 @@ const {
40
41
  p,
41
42
  code,
42
43
  h5,
44
+ h3,
43
45
  pre,
44
46
  button,
45
47
  form,
@@ -100,6 +102,7 @@ const os = require("os");
100
102
  const Page = require("@saltcorn/data/models/page");
101
103
  const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
102
104
  const stream = require("stream");
105
+ const Crash = require("@saltcorn/data/models/crash");
103
106
 
104
107
  const router = new Router();
105
108
  module.exports = router;
@@ -862,7 +865,7 @@ router.get(
862
865
  const can_update =
863
866
  !is_latest && !process.env.SALTCORN_DISABLE_UPGRADE && !git_commit;
864
867
  const dbversion = await db.getVersion(true);
865
-
868
+ const { memUsage, diskUsage, cpuUsage } = await get_sys_info();
866
869
  send_admin_page({
867
870
  res,
868
871
  req,
@@ -892,7 +895,6 @@ router.get(
892
895
  {
893
896
  href: "/admin/configuration-check",
894
897
  class: "btn btn-info",
895
- onClick: "press_store_button(this)",
896
898
  },
897
899
  i({ class: "fas fa-stethoscope" }),
898
900
  " ",
@@ -985,7 +987,20 @@ router.get(
985
987
  tr(
986
988
  th(req.__("Process uptime")),
987
989
  td(moment(get_process_init_time()).fromNow(true))
988
- )
990
+ ),
991
+ tr(
992
+ th(req.__("Disk usage")),
993
+ diskUsage > 95
994
+ ? td(
995
+ { class: "text-danger fw-bold" },
996
+ diskUsage,
997
+ "%",
998
+ i({ class: "fas fa-exclamation-triangle ms-1" })
999
+ )
1000
+ : td(diskUsage, "%")
1001
+ ),
1002
+ tr(th(req.__("CPU usage")), td(cpuUsage, "%")),
1003
+ tr(th(req.__("Mem usage")), td(memUsage, "%"))
989
1004
  )
990
1005
  ),
991
1006
  p(
@@ -1306,49 +1321,74 @@ router.get(
1306
1321
  "/configuration-check",
1307
1322
  isAdmin,
1308
1323
  error_catcher(async (req, res) => {
1309
- const { passes, errors, pass, warnings } = await runConfigurationCheck(req);
1310
- const mkError = (err) =>
1311
- div(
1312
- { class: "alert alert-danger", role: "alert" },
1313
- pre({ class: "mb-0" }, code(err))
1324
+ const filename = `${moment().format("YYYYMMDDHHmm")}.html`;
1325
+ await File.new_folder("configuration_checks");
1326
+ const go = async () => {
1327
+ const { passes, errors, pass, warnings } = await runConfigurationCheck(
1328
+ req
1314
1329
  );
1315
- const mkWarning = (err) =>
1316
- div(
1317
- { class: "alert alert-warning", role: "alert" },
1318
- pre({ class: "mb-0" }, code(err))
1330
+ const mkError = (err) =>
1331
+ div(
1332
+ { class: "alert alert-danger", role: "alert" },
1333
+ pre({ class: "mb-0" }, code(err))
1334
+ );
1335
+ const mkWarning = (err) =>
1336
+ div(
1337
+ { class: "alert alert-warning", role: "alert" },
1338
+ pre({ class: "mb-0" }, code(err))
1339
+ );
1340
+
1341
+ const report =
1342
+ div(
1343
+ h3("Errors"),
1344
+ pass
1345
+ ? div(req.__("No errors detected during configuration check"))
1346
+ : errors.map(mkError)
1347
+ ) +
1348
+ div(
1349
+ h3("Warnings"),
1350
+ (warnings || []).length
1351
+ ? (warnings || []).map(mkWarning)
1352
+ : "No warnings"
1353
+ ) +
1354
+ div(
1355
+ h3("Passes"),
1356
+
1357
+ pre(code(passes.join("\n")))
1358
+ );
1359
+ await File.from_contents(
1360
+ filename,
1361
+ "text/html",
1362
+ report,
1363
+ req.user.id,
1364
+ 1,
1365
+ "/configuration_checks"
1319
1366
  );
1367
+ };
1368
+ go().catch((err) => Crash.create(err, req));
1320
1369
  res.sendWrap(req.__(`Admin`), {
1321
1370
  above: [
1322
1371
  {
1323
1372
  type: "breadcrumbs",
1324
1373
  crumbs: [
1325
1374
  { text: req.__("Settings") },
1326
- { text: req.__("Admin"), href: "/admin" },
1375
+ { text: req.__("About application"), href: "/admin" },
1376
+ { text: req.__("System"), href: "/admin/system" },
1327
1377
  { text: req.__("Configuration check") },
1328
1378
  ],
1329
1379
  },
1380
+
1330
1381
  {
1331
1382
  type: "card",
1332
- title: req.__("Configuration errors"),
1383
+ title: req.__("Configuration check report"),
1333
1384
  contents: div(
1334
- pass
1335
- ? div(
1336
- { class: "alert alert-success", role: "alert" },
1337
- i({ class: "fas fa-check-circle fa-lg me-2" }),
1338
- h5(
1339
- { class: "d-inline" },
1340
- req.__("No errors detected during configuration check")
1341
- )
1342
- )
1343
- : errors.map(mkError),
1344
- (warnings || []).map(mkWarning)
1385
+ "When completed, the report will be ready here: ",
1386
+ a(
1387
+ { href: `/files/serve/configuration_checks/${filename}` },
1388
+ "/configuration_checks/" + filename
1389
+ )
1345
1390
  ),
1346
1391
  },
1347
- {
1348
- type: "card",
1349
- title: req.__("Configuration checks passed"),
1350
- contents: div(pre(code(passes.join("\n")))),
1351
- },
1352
1392
  ],
1353
1393
  });
1354
1394
  })
package/routes/api.js CHANGED
@@ -421,29 +421,29 @@ router.post(
421
421
  readState(row, fields);
422
422
  let errors = [];
423
423
  let hasErrors = false;
424
- Object.keys(row).forEach((k) => {
424
+ for (const k of Object.keys(row)) {
425
425
  const field = fields.find((f) => f.name === k);
426
426
  if (!field && k.includes(".")) {
427
427
  const [fnm, jkey] = k.split(".");
428
428
  const jfield = fields.find((f) => f.name === fnm);
429
429
  if (jfield?.type?.name === "JSON") {
430
- if (!row[fnm]) row[fnm] = { [jkey]: row[k] };
431
- else row[fnm][jkey] = row[k];
430
+ if (typeof row[fnm] === "undefined") {
431
+ const dbrow = await table.getRow({ [table.pk_name]: id });
432
+ row[fnm] = dbrow[fnm] || {};
433
+ }
434
+ row[fnm][jkey] = row[k];
432
435
  delete row[k];
433
436
  }
434
437
  } else if (!field || field.calculated) {
435
438
  delete row[k];
436
- return;
437
- }
438
-
439
- if (field?.type && field.type.validate) {
439
+ } else if (field?.type && field.type.validate) {
440
440
  const vres = field.type.validate(field.attributes || {})(row[k]);
441
441
  if (vres.error) {
442
442
  hasErrors = true;
443
443
  errors.push(`${k}: ${vres.error}`);
444
444
  }
445
445
  }
446
- });
446
+ }
447
447
  if (hasErrors) {
448
448
  res.status(400).json({ error: errors.join(", ") });
449
449
  return;
package/routes/tables.js CHANGED
@@ -762,18 +762,22 @@ router.get(
762
762
  })
763
763
  )
764
764
  ),
765
+ !table.external &&
766
+ div(
767
+ { class: "mx-auto" },
768
+ a(
769
+ { href: `/table/constraints/${table.id}` },
770
+ i({ class: "fas fa-2x fa-tasks" }),
771
+ "<br/>",
772
+ req.__("Constraints")
773
+ )
774
+ ),
775
+
765
776
  // only if table is not external
766
777
  !table.external &&
767
778
  div(
768
779
  { class: "mx-auto" },
769
780
  settingsDropdown(`dataMenuButton`, [
770
- a(
771
- {
772
- class: "dropdown-item",
773
- href: `/table/constraints/${table.id}`,
774
- },
775
- '<i class="fas fa-ban"></i>&nbsp;' + req.__("Constraints")
776
- ),
777
781
  // rename table doesnt supported for sqlite
778
782
  !db.isSQLite &&
779
783
  table.name !== "users" &&
@@ -1154,8 +1158,15 @@ router.get(
1154
1158
  [
1155
1159
  { label: req.__("Type"), key: "type" },
1156
1160
  {
1157
- label: req.__("Fields"),
1158
- key: (r) => r.configuration.fields.join(", "),
1161
+ label: req.__("What"),
1162
+ key: (r) =>
1163
+ r.type === "Unique"
1164
+ ? r.configuration.fields.join(", ")
1165
+ : r.type === "Index"
1166
+ ? r.configuration.field
1167
+ : r.type === "Formula"
1168
+ ? r.configuration.formula
1169
+ : "",
1159
1170
  },
1160
1171
  {
1161
1172
  label: req.__("Delete"),
@@ -1166,7 +1177,12 @@ router.get(
1166
1177
  cons,
1167
1178
  { hover: true }
1168
1179
  ),
1169
- link(`/table/add-constraint/${id}`, req.__("Add constraint")),
1180
+ req.__("Add constraint: "),
1181
+ link(`/table/add-constraint/${id}/Unique`, req.__("Unique")),
1182
+ " | ",
1183
+ link(`/table/add-constraint/${id}/Formula`, req.__("Formula")),
1184
+ " | ",
1185
+ link(`/table/add-constraint/${id}/Index`, req.__("Index")),
1170
1186
  ],
1171
1187
  },
1172
1188
  ],
@@ -1181,18 +1197,68 @@ router.get(
1181
1197
  * @param {object[]} fields
1182
1198
  * @returns {Form}
1183
1199
  */
1184
- const constraintForm = (req, table_id, fields) =>
1185
- new Form({
1186
- action: `/table/add-constraint/${table_id}`,
1187
- blurb: req.__(
1188
- "Tick the boxes for the fields that should be jointly unique"
1189
- ),
1190
- fields: fields.map((f) => ({
1191
- name: f.name,
1192
- label: f.label,
1193
- type: "Bool",
1194
- })),
1195
- });
1200
+ const constraintForm = (req, table_id, fields, type) => {
1201
+ switch (type) {
1202
+ case "Formula":
1203
+ return new Form({
1204
+ action: `/table/add-constraint/${table_id}/${type}`,
1205
+
1206
+ fields: [
1207
+ {
1208
+ name: "formula",
1209
+ label: req.__("Constraint formula"),
1210
+ validator: expressionValidator,
1211
+ type: "String",
1212
+ class: "validate-expression",
1213
+ sublabel:
1214
+ req.__(
1215
+ "Formula must evaluate to true for valid rows. In scope: "
1216
+ ) +
1217
+ fields
1218
+ .map((f) => f.name)
1219
+ .map((fn) => code(fn))
1220
+ .join(", "),
1221
+ },
1222
+ {
1223
+ name: "errormsg",
1224
+ label: "Error message",
1225
+ sublabel: "Shown the user if formula is false",
1226
+ type: "String",
1227
+ },
1228
+ ],
1229
+ });
1230
+ case "Unique":
1231
+ return new Form({
1232
+ action: `/table/add-constraint/${table_id}/${type}`,
1233
+ blurb: req.__(
1234
+ "Tick the boxes for the fields that should be jointly unique"
1235
+ ),
1236
+ fields: fields.map((f) => ({
1237
+ name: f.name,
1238
+ label: f.label,
1239
+ type: "Bool",
1240
+ })),
1241
+ });
1242
+ case "Index":
1243
+ return new Form({
1244
+ action: `/table/add-constraint/${table_id}/${type}`,
1245
+ blurb: req.__(
1246
+ "Choose the field to be indexed. This make searching the table faster."
1247
+ ),
1248
+ fields: [
1249
+ {
1250
+ type: "String",
1251
+ name: "field",
1252
+ label: "Field",
1253
+ required: true,
1254
+ attributes: {
1255
+ options: fields.map((f) => ({ label: f.label, name: f.name })),
1256
+ },
1257
+ },
1258
+ ],
1259
+ });
1260
+ }
1261
+ };
1196
1262
 
1197
1263
  /**
1198
1264
  * Add constraint GET handler
@@ -1203,10 +1269,10 @@ const constraintForm = (req, table_id, fields) =>
1203
1269
  * @function
1204
1270
  */
1205
1271
  router.get(
1206
- "/add-constraint/:id",
1272
+ "/add-constraint/:id/:type",
1207
1273
  isAdmin,
1208
1274
  error_catcher(async (req, res) => {
1209
- const { id } = req.params;
1275
+ const { id, type } = req.params;
1210
1276
  const table = await Table.findOne({ id });
1211
1277
  if (!table) {
1212
1278
  req.flash("error", `Table not found`);
@@ -1214,7 +1280,7 @@ router.get(
1214
1280
  return;
1215
1281
  }
1216
1282
  const fields = await table.getFields();
1217
- const form = constraintForm(req, table.id, fields);
1283
+ const form = constraintForm(req, table.id, fields, type);
1218
1284
  res.sendWrap(req.__(`Add constraint to %s`, table.name), {
1219
1285
  above: [
1220
1286
  {
@@ -1231,7 +1297,7 @@ router.get(
1231
1297
  },
1232
1298
  {
1233
1299
  type: "card",
1234
- title: req.__(`Add constraint to %s`, table.name),
1300
+ title: req.__(`Add %s constraint to %s`, type, table.name),
1235
1301
  contents: renderForm(form, req.csrfToken()),
1236
1302
  },
1237
1303
  ],
@@ -1247,10 +1313,10 @@ router.get(
1247
1313
  * @function
1248
1314
  */
1249
1315
  router.post(
1250
- "/add-constraint/:id",
1316
+ "/add-constraint/:id/:type",
1251
1317
  isAdmin,
1252
1318
  error_catcher(async (req, res) => {
1253
- const { id } = req.params;
1319
+ const { id, type } = req.params;
1254
1320
  const table = await Table.findOne({ id });
1255
1321
  if (!table) {
1256
1322
  req.flash("error", `Table not found`);
@@ -1258,16 +1324,20 @@ router.post(
1258
1324
  return;
1259
1325
  }
1260
1326
  const fields = await table.getFields();
1261
- const form = constraintForm(req, table.id, fields);
1327
+ const form = constraintForm(req, table.id, fields, type);
1262
1328
  form.validate(req.body);
1263
1329
  if (form.hasErrors) req.flash("error", req.__("An error occurred"));
1264
1330
  else {
1331
+ let configuration = {};
1332
+ if (type === "Unique")
1333
+ configuration.fields = fields
1334
+ .map((f) => f.name)
1335
+ .filter((f) => form.values[f]);
1336
+ else configuration = form.values;
1265
1337
  await TableConstraint.create({
1266
1338
  table_id: table.id,
1267
- type: "Unique",
1268
- configuration: {
1269
- fields: fields.map((f) => f.name).filter((f) => form.values[f]),
1270
- },
1339
+ type,
1340
+ configuration,
1271
1341
  });
1272
1342
  }
1273
1343
  res.redirect(`/table/constraints/${table.id}`);