@saltcorn/server 0.8.8-beta.4 → 0.8.8-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/auth/routes.js CHANGED
@@ -254,7 +254,9 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
254
254
  res.json(token);
255
255
  } else {
256
256
  res.json({
257
- alerts: [{ type: "danger", msg: req.__("Incorrect user or password") }],
257
+ alerts: [
258
+ { type: "danger", msg: req.__("Incorrect user or password") },
259
+ ],
258
260
  });
259
261
  }
260
262
  } else if (publicUserLink) {
@@ -276,7 +278,9 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
276
278
  res.json(token);
277
279
  } else {
278
280
  res.json({
279
- alerts: [{ type: "danger", msg: req.__("The public login is deactivated") }],
281
+ alerts: [
282
+ { type: "danger", msg: req.__("The public login is deactivated") },
283
+ ],
280
284
  });
281
285
  }
282
286
  };
@@ -628,9 +632,11 @@ router.post(
628
632
  * @throws {InvalidConfiguration}
629
633
  */
630
634
  const getNewUserForm = async (new_user_view_name, req, askEmail) => {
635
+ if (!new_user_view_name) return;
631
636
  const view = await View.findOne({ name: new_user_view_name });
632
637
  if (!view)
633
638
  throw new InvalidConfiguration("New user form view does not exist");
639
+ if (view.viewtemplate !== "Edit") return;
634
640
  const table = Table.findOne({ name: "users" });
635
641
  const fields = table.getFields();
636
642
  const { columns, layout } = view.configuration;
@@ -704,14 +710,14 @@ const getNewUserForm = async (new_user_view_name, req, askEmail) => {
704
710
  * @param {object} res
705
711
  * @returns {void}
706
712
  */
707
- const signup_login_with_user = (u, req, res) =>
713
+ const signup_login_with_user = (u, req, res, redirUrl) =>
708
714
  req.login(u.session_object, function (err) {
709
715
  if (!err) {
710
716
  Trigger.emitEvent("Login", null, u);
711
717
  if (getState().verifier) res.redirect("/auth/verification-flow");
712
718
  else if (getState().get2FApolicy(u) === "Mandatory")
713
719
  res.redirect("/auth/twofa/setup/totp");
714
- else res.redirect("/");
720
+ else res.redirect(redirUrl || "/");
715
721
  } else {
716
722
  req.flash("danger", err);
717
723
  res.redirect("/auth/signup");
@@ -869,7 +875,8 @@ router.post(
869
875
  return;
870
876
  }
871
877
 
872
- const unsuitableEmailPassword = async (email, password, passwordRepeat) => {
878
+ const unsuitableEmailPassword = async (urecord) => {
879
+ const { email, password, passwordRepeat } = urecord;
873
880
  if (!email || !password) {
874
881
  req.flash("danger", req.__("E-mail and password required"));
875
882
  res.redirect("/auth/signup");
@@ -911,6 +918,12 @@ router.post(
911
918
  res.redirect("/auth/signup");
912
919
  return true;
913
920
  }
921
+ let constraint_check_error = User.table.check_table_constraints(urecord);
922
+ if (constraint_check_error) {
923
+ req.flash("danger", constraint_check_error);
924
+ res.redirect("/auth/signup");
925
+ return true;
926
+ }
914
927
  };
915
928
  const new_user_form = getState().getConfig("new_user_form");
916
929
 
@@ -943,21 +956,32 @@ router.post(
943
956
  signup_form.values[f.name] = signup_form.values[f.name] || "";
944
957
  });
945
958
  const userObject = signup_form.values;
946
- const { email, password, passwordRepeat } = userObject;
947
- if (await unsuitableEmailPassword(email, password, passwordRepeat))
948
- return;
949
- if (new_user_form) {
950
- const form = await getNewUserForm(new_user_form, req);
959
+ //const { email, password, passwordRepeat } = userObject;
960
+ if (await unsuitableEmailPassword(userObject)) return;
961
+ const new_user_form_form = await getNewUserForm(new_user_form, req);
962
+ if (new_user_form_form) {
951
963
  Object.entries(userObject).forEach(([k, v]) => {
952
- form.values[k] = v;
953
- if (!form.fields.find((f) => f.name === k)) form.hidden(k);
964
+ new_user_form_form.values[k] = v;
965
+ if (!new_user_form_form.fields.find((f) => f.name === k))
966
+ new_user_form_form.hidden(k);
954
967
  });
955
- res.sendAuthWrap(new_user_form, form, getAuthLinks("signup", true));
968
+ res.sendAuthWrap(
969
+ new_user_form,
970
+ new_user_form_form,
971
+ getAuthLinks("signup", true)
972
+ );
956
973
  } else {
957
974
  const u = await User.create(userObject);
958
975
  await send_verification_email(u, req);
959
976
 
960
- signup_login_with_user(u, req, res);
977
+ signup_login_with_user(
978
+ u,
979
+ req,
980
+ res,
981
+ new_user_form && !new_user_form_form
982
+ ? `/view/${new_user_form}?id=${u.id}`
983
+ : undefined
984
+ );
961
985
  }
962
986
  return;
963
987
  }
@@ -972,7 +996,7 @@ router.post(
972
996
  res.sendAuthWrap(req.__(`Sign up`), form, getAuthLinks("signup"));
973
997
  } else {
974
998
  const { email, password } = form.values;
975
- if (await unsuitableEmailPassword(email, password)) return;
999
+ if (await unsuitableEmailPassword({ email, password })) return;
976
1000
  if (new_user_form) {
977
1001
  const form = await getNewUserForm(new_user_form, req);
978
1002
  form.values.email = email;
@@ -1100,7 +1124,13 @@ router.get(
1100
1124
  const { method } = req.params;
1101
1125
  if (method === "jwt") {
1102
1126
  const { email, password } = req.query;
1103
- await loginWithJwt(email, password, req.headers["x-saltcorn-app"], res, req);
1127
+ await loginWithJwt(
1128
+ email,
1129
+ password,
1130
+ req.headers["x-saltcorn-app"],
1131
+ res,
1132
+ req
1133
+ );
1104
1134
  } else {
1105
1135
  const auth = getState().auth_methods[method];
1106
1136
  if (auth) {
package/locales/en.json CHANGED
@@ -1246,5 +1246,7 @@
1246
1246
  "Included Plugins": "Included Plugins",
1247
1247
  "exclude": "exclude",
1248
1248
  "include": "include",
1249
- "Auto public login": "Auto public login"
1250
- }
1249
+ "Auto public login": "Auto public login",
1250
+ "New user view": "New user view",
1251
+ "A view to show to new users, to finalise registration (if Edit) or as a welcome view": "A view to show to new users, to finalise registration (if Edit) or as a welcome view"
1252
+ }
package/locales/it.json CHANGED
@@ -503,5 +503,20 @@
503
503
  "Table columns": "Table columns",
504
504
  "Configuration items": "Configuration items",
505
505
  "Crashlogs": "Crashlogs",
506
- "Logged out user %s": "Logged out user %s"
507
- }
506
+ "Logged out user %s": "Logged out user %s",
507
+ "CSV upload": "CSV upload",
508
+ "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.": "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.",
509
+ "Create page": "Create page",
510
+ "Triggers run actions in response to events.": "Triggers run actions in response to events.",
511
+ "No triggers": "No triggers",
512
+ "Upload file(s)": "Upload file(s)",
513
+ "Pattern": "Pattern",
514
+ "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.": "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.",
515
+ "Views potentially affected": "Views potentially affected",
516
+ "Fixed and blocked fields": "Fixed and blocked fields",
517
+ "URL after delete": "URL after delete",
518
+ "Save before going back": "Save before going back",
519
+ "Reload after going back": "Reload after going back",
520
+ "Steps to go back": "Steps to go back",
521
+ "%s configuration": "%s configuration"
522
+ }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.8-beta.4",
3
+ "version": "0.8.8-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.8.8-beta.4",
10
- "@saltcorn/builder": "0.8.8-beta.4",
11
- "@saltcorn/data": "0.8.8-beta.4",
12
- "@saltcorn/admin-models": "0.8.8-beta.4",
13
- "@saltcorn/filemanager": "0.8.8-beta.4",
14
- "@saltcorn/markup": "0.8.8-beta.4",
15
- "@saltcorn/sbadmin2": "0.8.8-beta.4",
9
+ "@saltcorn/base-plugin": "0.8.8-beta.6",
10
+ "@saltcorn/builder": "0.8.8-beta.6",
11
+ "@saltcorn/data": "0.8.8-beta.6",
12
+ "@saltcorn/admin-models": "0.8.8-beta.6",
13
+ "@saltcorn/filemanager": "0.8.8-beta.6",
14
+ "@saltcorn/markup": "0.8.8-beta.6",
15
+ "@saltcorn/sbadmin2": "0.8.8-beta.6",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -41,6 +41,15 @@ const _apply_showif_plugins = [];
41
41
  const add_apply_showif_plugin = (p) => {
42
42
  _apply_showif_plugins.push(p);
43
43
  };
44
+
45
+ const nubBy = (prop, xs) => {
46
+ const vs = new Set();
47
+ return xs.filter((x) => {
48
+ if (vs.has(x[prop])) return false;
49
+ vs.add(x[prop]);
50
+ return true;
51
+ });
52
+ };
44
53
  function apply_showif() {
45
54
  $("[data-show-if]").each(function (ix, element) {
46
55
  var e = $(element);
@@ -109,10 +118,16 @@ function apply_showif() {
109
118
  const dynwhere = JSON.parse(
110
119
  decodeURIComponent(e.attr("data-fetch-options"))
111
120
  );
112
- //console.log(dynwhere);
113
- const qs = Object.entries(dynwhere.whereParsed)
114
- .map(([k, v]) => `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`)
115
- .join("&");
121
+ //console.log("dynwhere", dynwhere);
122
+ const qss = Object.entries(dynwhere.whereParsed).map(
123
+ ([k, v]) => `${k}=${v[0] === "$" ? rec[v.substring(1)] : v}`
124
+ );
125
+ if (dynwhere.dereference) {
126
+ if (Array.isArray(dynwhere.dereference))
127
+ qss.push(...dynwhere.dereference.map((d) => `dereference=${d}`));
128
+ else qss.push(`dereference=${dynwhere.dereference}`);
129
+ }
130
+ const qs = qss.join("&");
116
131
  var current = e.attr("data-selected");
117
132
  e.change(function (ec) {
118
133
  e.attr("data-selected", ec.target.value);
@@ -129,7 +144,11 @@ function apply_showif() {
129
144
  if (!dynwhere.required) toAppend.push(`<option></option>`);
130
145
  let currentDataOption = undefined;
131
146
  const dataOptions = [];
132
- success.forEach((r) => {
147
+ //console.log(success);
148
+ const success1 = dynwhere.nubBy
149
+ ? nubBy(dynwhere.nubBy, success)
150
+ : success;
151
+ success1.forEach((r) => {
133
152
  const label = dynwhere.label_formula
134
153
  ? new Function(
135
154
  `{${Object.keys(r).join(",")}}`,
@@ -137,6 +156,7 @@ function apply_showif() {
137
156
  )(r)
138
157
  : r[dynwhere.summary_field];
139
158
  const value = r[dynwhere.refname];
159
+ //console.log("lv", label, value, r, dynwhere.summary_field);
140
160
  const selected = `${current}` === `${r[dynwhere.refname]}`;
141
161
  dataOptions.push({ text: label, value });
142
162
  if (selected) currentDataOption = value;
@@ -510,6 +530,14 @@ function initialize_page() {
510
530
  if (schema) {
511
531
  schema = JSON.parse(decodeURIComponent(schema));
512
532
  }
533
+ if (type === "Date") {
534
+ console.log("timeelsems", $(this).find("span.current time"));
535
+ current =
536
+ $(this).attr("data-inline-edit-current") ||
537
+ $(this).find("span.current time").attr("datetime"); // ||
538
+ //$(this).children("span.current").html();
539
+ }
540
+ console.log({ type, current });
513
541
  var is_key = type?.startsWith("Key:");
514
542
  const opts = encodeURIComponent(
515
543
  JSON.stringify({
@@ -875,7 +903,7 @@ function common_done(res, isWeb = true) {
875
903
  if (res.eval_js) handle(res.eval_js, eval);
876
904
 
877
905
  if (res.reload_page) {
878
- (isWeb ? location : parent.location).reload(); //TODO notify to cookie if reload or goto
906
+ (isWeb ? location : parent).reload(); //TODO notify to cookie if reload or goto
879
907
  }
880
908
  if (res.download) {
881
909
  handle(res.download, (download) => {
@@ -467,3 +467,8 @@ div.unread-notify {
467
467
  .sc-modal-linkout {
468
468
  color: inherit;
469
469
  }
470
+
471
+ .link-style {
472
+ cursor: pointer;
473
+ text-decoration: underline;
474
+ }
@@ -1,18 +1,22 @@
1
- function sortby(k, desc, viewIdentifier) {
2
- set_state_fields({
3
- [viewIdentifier ? `_${viewIdentifier}_sortby` : "_sortby"]: k,
4
- [viewIdentifier ? `_${viewIdentifier}_sortdesc` : "_sortdesc"]: desc
5
- ? "on"
6
- : { unset: true },
7
- });
1
+ function sortby(k, desc, viewIdentifier, e) {
2
+ set_state_fields(
3
+ {
4
+ [viewIdentifier ? `_${viewIdentifier}_sortby` : "_sortby"]: k,
5
+ [viewIdentifier ? `_${viewIdentifier}_sortdesc` : "_sortdesc"]: desc
6
+ ? "on"
7
+ : { unset: true },
8
+ },
9
+ false,
10
+ e
11
+ );
8
12
  }
9
- function gopage(n, pagesize, viewIdentifier, extra = {}) {
13
+ function gopage(n, pagesize, viewIdentifier, extra = {}, e) {
10
14
  const cfg = {
11
15
  ...extra,
12
16
  [viewIdentifier ? `_${viewIdentifier}_page` : "_page"]: n,
13
17
  [viewIdentifier ? `_${viewIdentifier}_pagesize` : "_pagesize"]: pagesize,
14
18
  };
15
- set_state_fields(cfg);
19
+ set_state_fields(cfg, false, e);
16
20
  }
17
21
 
18
22
  if (localStorage.getItem("reload_on_init")) {
@@ -72,28 +76,31 @@ function get_current_state_url(e) {
72
76
  else return $modal.prop("data-modal-state");
73
77
  }
74
78
 
75
- function select_id(id) {
76
- pjax_to(updateQueryStringParameter(get_current_state_url(), "id", id));
79
+ function select_id(id, e) {
80
+ pjax_to(updateQueryStringParameter(get_current_state_url(e), "id", id), e);
77
81
  }
78
82
 
79
83
  function set_state_field(key, value, e) {
80
84
  pjax_to(updateQueryStringParameter(get_current_state_url(e), key, value), e);
81
85
  }
82
86
 
83
- function check_state_field(that) {
87
+ function check_state_field(that, e) {
84
88
  const checked = that.checked;
85
89
  const name = that.name;
86
90
  const value = encodeURIComponent(that.value);
87
- var separator = window.location.href.indexOf("?") !== -1 ? "&" : "?";
91
+ var separator = get_current_state_url(e).indexOf("?") !== -1 ? "&" : "?";
88
92
  let dest;
89
- if (checked) dest = get_current_state_url() + `${separator}${name}=${value}`;
90
- else dest = get_current_state_url().replace(`${name}=${value}`, "");
91
- pjax_to(dest.replace("&&", "&").replace("?&", "?"));
93
+ if (checked) dest = get_current_state_url(e) + `${separator}${name}=${value}`;
94
+ else dest = get_current_state_url(e).replace(`${name}=${value}`, "");
95
+ pjax_to(dest.replace("&&", "&").replace("?&", "?"), e);
92
96
  }
93
97
 
94
98
  function invalidate_pagings(href) {
95
99
  let newhref = href;
96
- const queryObj = Object.fromEntries(new URL(newhref).searchParams.entries());
100
+ const prev = new URL(window.location.href);
101
+ const queryObj = Object.fromEntries(
102
+ new URL(newhref, prev.origin).searchParams.entries()
103
+ );
97
104
  const toRemove = Object.keys(queryObj).filter((val) => is_paging_param(val));
98
105
  for (const k of toRemove) {
99
106
  newhref = removeQueryStringParameter(newhref, k);
@@ -173,12 +180,12 @@ function pjax_to(href, e) {
173
180
  function href_to(href) {
174
181
  window.location.href = href;
175
182
  }
176
- function clear_state(omit_fields_str) {
177
- let newUrl = get_current_state_url().split("?")[0];
178
- const hash = get_current_state_url().split("#")[1];
183
+ function clear_state(omit_fields_str, e) {
184
+ let newUrl = get_current_state_url(e).split("?")[0];
185
+ const hash = get_current_state_url(e).split("#")[1];
179
186
  if (omit_fields_str) {
180
187
  const omit_fields = omit_fields_str.split(",").map((s) => s.trim());
181
- let qs = (get_current_state_url().split("?")[1] || "").split("#")[0];
188
+ let qs = (get_current_state_url(e).split("?")[1] || "").split("#")[0];
182
189
  let params = new URLSearchParams(qs);
183
190
  newUrl = newUrl + "?";
184
191
  omit_fields.forEach((f) => {
@@ -188,7 +195,7 @@ function clear_state(omit_fields_str) {
188
195
  }
189
196
  if (hash) newUrl += "#" + hash;
190
197
 
191
- pjax_to(newUrl);
198
+ pjax_to(newUrl, e);
192
199
  }
193
200
 
194
201
  function ajax_done(res) {
@@ -389,6 +396,20 @@ function saveAndContinue(e, k) {
389
396
  return false;
390
397
  }
391
398
 
399
+ function updateMatchingRows(e, viewname) {
400
+ const form = $(e).closest("form");
401
+ try {
402
+ const sp = `${new URL(get_current_state_url()).searchParams.toString()}`;
403
+ form.attr(
404
+ "action",
405
+ `/view/${viewname}/update_matching_rows${sp ? `?${sp}` : ""}`
406
+ );
407
+ form[0].submit();
408
+ } finally {
409
+ form.attr("action", `/view/${viewname}`);
410
+ }
411
+ }
412
+
392
413
  function applyViewConfig(e, url, k) {
393
414
  var form = $(e).closest("form");
394
415
  var form_data = form.serializeArray();
@@ -755,6 +776,18 @@ function toggle_tbl_sync() {
755
776
  }
756
777
  }
757
778
 
779
+ function toggle_android_platform() {
780
+ if ($("#androidCheckboxId")[0].checked === true) {
781
+ $("#dockerCheckboxId").attr("hidden", false);
782
+ $("#dockerCheckboxId").attr("checked", true);
783
+ $("#dockerLabelId").removeClass("d-none");
784
+ } else {
785
+ $("#dockerCheckboxId").attr("hidden", true);
786
+ $("#dockerCheckboxId").attr("checked", false);
787
+ $("#dockerLabelId").addClass("d-none");
788
+ }
789
+ }
790
+
758
791
  function join_field_clicked(e, fieldPath) {
759
792
  $("#inputjoin_field").val(fieldPath);
760
793
  apply_showif();
package/routes/admin.js CHANGED
@@ -1535,7 +1535,9 @@ router.get(
1535
1535
  div({ class: "col-sm-4 fw-bold" }, req.__("Platform")),
1536
1536
  div(
1537
1537
  {
1538
- class: "col-sm-1 fw-bold d-flex justify-content-center",
1538
+ class:
1539
+ "col-sm-1 fw-bold d-flex justify-content-center d-none",
1540
+ id: "dockerLabelId",
1539
1541
  },
1540
1542
  req.__("docker")
1541
1543
  )
@@ -1596,7 +1598,7 @@ router.get(
1596
1598
  ),
1597
1599
  div(
1598
1600
  { class: "col-sm-4" },
1599
-
1601
+ // android
1600
1602
  div(
1601
1603
  { class: "container ps-0" },
1602
1604
  div(
@@ -1609,9 +1611,11 @@ router.get(
1609
1611
  class: "form-check-input",
1610
1612
  name: "androidPlatform",
1611
1613
  id: "androidCheckboxId",
1614
+ onClick: "toggle_android_platform()",
1612
1615
  })
1613
1616
  )
1614
1617
  ),
1618
+ // iOS
1615
1619
  div(
1616
1620
  { class: "row" },
1617
1621
  div({ class: "col-sm-8" }, req.__("iOS")),
@@ -1627,6 +1631,7 @@ router.get(
1627
1631
  )
1628
1632
  )
1629
1633
  ),
1634
+ // android with docker
1630
1635
  div(
1631
1636
  { class: "col-sm-1 d-flex justify-content-center" },
1632
1637
  input({
@@ -1634,6 +1639,7 @@ router.get(
1634
1639
  class: "form-check-input",
1635
1640
  name: "useDocker",
1636
1641
  id: "dockerCheckboxId",
1642
+ hidden: true,
1637
1643
  })
1638
1644
  )
1639
1645
  ),
package/routes/api.js CHANGED
@@ -251,7 +251,8 @@ router.get(
251
251
  //passport.authenticate("api-bearer", { session: false }),
252
252
  error_catcher(async (req, res, next) => {
253
253
  let { tableName } = req.params;
254
- const { fields, versioncount, approximate, ...req_query } = req.query;
254
+ const { fields, versioncount, approximate, dereference, ...req_query } =
255
+ req.query;
255
256
  const table = Table.findOne(
256
257
  strictParseInt(tableName)
257
258
  ? { id: strictParseInt(tableName) }
@@ -284,7 +285,7 @@ router.get(
284
285
  },
285
286
  };
286
287
  rows = await table.getJoinedRows(joinOpts);
287
- } else if (req_query && req_query !== {}) {
288
+ } else {
288
289
  const tbl_fields = table.getFields();
289
290
  readState(req_query, tbl_fields, req);
290
291
  const qstate = await stateFieldsToWhere({
@@ -293,18 +294,26 @@ router.get(
293
294
  state: req_query,
294
295
  table,
295
296
  });
296
- rows = await table.getRows(qstate, {
297
+ const joinFields = {};
298
+ const derefs = Array.isArray(dereference)
299
+ ? dereference
300
+ : !dereference
301
+ ? []
302
+ : [dereference];
303
+ derefs.forEach((f) => {
304
+ const field = table.getField(f);
305
+ if (field?.attributes?.summary_field)
306
+ joinFields[`${f}_${field?.attributes?.summary_field}`] = {
307
+ ref: f,
308
+ target: field?.attributes?.summary_field,
309
+ };
310
+ });
311
+ rows = await table.getJoinedRows({
312
+ where: qstate,
313
+ joinFields,
297
314
  forPublic: !(req.user || user),
298
315
  forUser: req.user || user,
299
316
  });
300
- } else {
301
- rows = await table.getRows(
302
- {},
303
- {
304
- forPublic: !(req.user || user),
305
- forUser: req.user || user,
306
- }
307
- );
308
317
  }
309
318
  res.json({ success: rows.map(limitFields(fields)) });
310
319
  } else {
@@ -187,7 +187,7 @@ const viewsList = async (
187
187
  label: req.__("Name"),
188
188
  key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
189
189
  sortlink: !tagId
190
- ? `javascript:set_state_field('_sortby', 'name')`
190
+ ? `set_state_field('_sortby', 'name', this)`
191
191
  : undefined,
192
192
  },
193
193
  // description - currently I dont want to show description in view list
@@ -205,7 +205,7 @@ const viewsList = async (
205
205
  label: req.__("Pattern"),
206
206
  key: "viewtemplate",
207
207
  sortlink: !tagId
208
- ? `javascript:set_state_field('_sortby', 'viewtemplate')`
208
+ ? `set_state_field('_sortby', 'viewtemplate', this)`
209
209
  : undefined,
210
210
  },
211
211
  ...(notable
@@ -215,7 +215,7 @@ const viewsList = async (
215
215
  label: req.__("Table"),
216
216
  key: (r) => link(`/table/${r.table}`, r.table),
217
217
  sortlink: !tagId
218
- ? `javascript:set_state_field('_sortby', 'table')`
218
+ ? `set_state_field('_sortby', 'table', this)`
219
219
  : undefined,
220
220
  },
221
221
  ]),
@@ -59,7 +59,7 @@ router.get(
59
59
  page_opts.pagination = {
60
60
  current_page,
61
61
  pages: Math.ceil(nrows / rows_per_page),
62
- get_page_link: (n) => `javascript:gopage(${n}, ${rows_per_page})`,
62
+ get_page_link: (n) => `gopage(${n}, ${rows_per_page})`,
63
63
  };
64
64
  }
65
65
  }
@@ -180,6 +180,7 @@ const customEventForm = async (req) => {
180
180
  name: "name",
181
181
  label: req.__("Event Name"),
182
182
  type: "String",
183
+ required: true,
183
184
  },
184
185
  {
185
186
  name: "hasChannel",
@@ -256,11 +257,11 @@ router.post(
256
257
  * @function
257
258
  */
258
259
  router.post(
259
- "/custom/delete/:name",
260
+ "/custom/delete/:name?",
260
261
  isAdmin,
261
262
  error_catcher(async (req, res) => {
262
- const { name } = req.params;
263
-
263
+ let { name } = req.params;
264
+ if (!name) name = "";
264
265
  const cevs = getState().getConfig("custom_events", []);
265
266
 
266
267
  await getState().setConfig(
@@ -331,7 +332,7 @@ router.get(
331
332
  page_opts.pagination = {
332
333
  current_page,
333
334
  pages: Math.ceil(nrows / rows_per_page),
334
- get_page_link: (n) => `javascript:gopage(${n}, ${rows_per_page})`,
335
+ get_page_link: (n) => `gopage(${n}, ${rows_per_page})`,
335
336
  };
336
337
  }
337
338
  }
package/routes/search.js CHANGED
@@ -201,7 +201,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
201
201
  pages: current_page + (vresps.length === page_size ? 1 : 0),
202
202
  trailing_ellipsis: vresps.length === page_size,
203
203
  get_page_link: (n) =>
204
- `javascript:gopage(${n}, ${page_size}, undefined, {table:'${tableName}'})`,
204
+ `gopage(${n}, ${page_size}, undefined, {table:'${tableName}'}, this)`,
205
205
  });
206
206
  }
207
207
 
package/routes/tables.js CHANGED
@@ -90,7 +90,7 @@ const tableForm = async (table, req) => {
90
90
  noSubmitButton: true,
91
91
  onChange: "saveAndContinue(this)",
92
92
  fields: [
93
- ...(!table.external
93
+ ...(!table.external && !table.provider_name
94
94
  ? [
95
95
  {
96
96
  label: req.__("Ownership field"),
@@ -146,9 +146,9 @@ const tableForm = async (table, req) => {
146
146
  name: "min_role_read",
147
147
  input_type: "select",
148
148
  options: roleOptions,
149
- attributes: { asideNext: !table.external },
149
+ attributes: { asideNext: !table.external && !table.provider_name },
150
150
  },
151
- ...(table.external
151
+ ...(table.external || table.provider_name
152
152
  ? []
153
153
  : [
154
154
  {
@@ -790,6 +790,7 @@ router.get(
790
790
  "<br>"
791
791
  : "",
792
792
  !table.external &&
793
+ !table.provider_name &&
793
794
  a(
794
795
  {
795
796
  href: `/field/new/${table.id}`,
@@ -903,6 +904,7 @@ router.get(
903
904
  )
904
905
  ),
905
906
  !table.external &&
907
+ !table.provider_name &&
906
908
  div(
907
909
  { class: "mx-auto" },
908
910
  form(
@@ -929,6 +931,7 @@ router.get(
929
931
  )
930
932
  ),
931
933
  !table.external &&
934
+ !table.provider_name &&
932
935
  div(
933
936
  { class: "mx-auto" },
934
937
  a(
@@ -944,6 +947,7 @@ router.get(
944
947
 
945
948
  // only if table is not external
946
949
  !table.external &&
950
+ !table.provider_name &&
947
951
  div(
948
952
  { class: "mx-auto" },
949
953
  settingsDropdown(`dataMenuButton`, [
package/tests/api.test.js CHANGED
@@ -127,6 +127,22 @@ describe("API read", () => {
127
127
  .set("Cookie", loginCookie)
128
128
  .expect(succeedJsonWith((rows) => rows.length == 2));
129
129
  });
130
+ it("should dereference", async () => {
131
+ const loginCookie = await getStaffLoginCookie();
132
+
133
+ const app = await getApp({ disableCsrf: true });
134
+ await request(app)
135
+ .get("/api/patients/?dereference=favbook")
136
+ .set("Cookie", loginCookie)
137
+ .expect(
138
+ succeedJsonWith(
139
+ (rows) =>
140
+ rows.length == 2 &&
141
+ rows.find((r) => r.favbook === 1).favbook_author ==
142
+ "Herman Melville"
143
+ )
144
+ );
145
+ });
130
146
  it("should add version counts", async () => {
131
147
  const patients = Table.findOne({ name: "patients" });
132
148
  await patients.update({ versioned: true });
@@ -9,13 +9,12 @@ const {
9
9
  toNotInclude,
10
10
  resetToFixtures,
11
11
  respondJsonWith,
12
+ toSucceed,
12
13
  } = require("../auth/testhelp");
13
14
  const db = require("@saltcorn/data/db");
14
15
  const { getState } = require("@saltcorn/data/db/state");
15
16
  const View = require("@saltcorn/data/models/view");
16
17
  const Table = require("@saltcorn/data/models/table");
17
- const Trigger = require("@saltcorn/data/models/trigger");
18
- const Page = require("@saltcorn/data/models/page");
19
18
 
20
19
  const { plugin_with_routes } = require("@saltcorn/data/tests/mocks");
21
20
 
@@ -397,6 +396,70 @@ describe("action row_variable", () => {
397
396
  });
398
397
  });
399
398
 
399
+ describe("update matching rows", () => {
400
+ const updateMatchingRows = async ({ query, body }) => {
401
+ const app = await getApp({ disableCsrf: true });
402
+ const loginCookie = await getAdminLoginCookie();
403
+ await request(app)
404
+ .post(
405
+ `/view/author_multi_edit/update_matching_rows${
406
+ query ? `?${query}` : ""
407
+ }`
408
+ )
409
+ .set("Cookie", loginCookie)
410
+ .send(body)
411
+ .set("Content-Type", "application/json")
412
+ .set("Accept", "application/json")
413
+ .expect(toSucceed(302));
414
+ };
415
+
416
+ beforeAll(async () => {
417
+ const table = Table.findOne({ name: "books" });
418
+ const field = table.getFields().find((f) => f.name === "author");
419
+ await field.update({ is_unique: false });
420
+ });
421
+
422
+ it("update matching books normal", async () => {
423
+ const table = Table.findOne({ name: "books" });
424
+ await updateMatchingRows({
425
+ query: "author=leo&publisher=1",
426
+ body: { author: "new_author" },
427
+ });
428
+ let actualRows = await table.getRows({ author: "new_author" });
429
+ expect(actualRows.length).toBe(1);
430
+ await updateMatchingRows({
431
+ query: "_gte_pages=600",
432
+ body: { author: "more_than" },
433
+ });
434
+ actualRows = await table.getRows({ author: "more_than" });
435
+ expect(actualRows.length >= 2).toBe(true);
436
+ const expected = (await table.getRows()).map((row) => {
437
+ return { id: row.id, author: "agi", pages: 100, publisher: null };
438
+ });
439
+ await updateMatchingRows({
440
+ body: { author: "agi", pages: 100, publisher: null },
441
+ });
442
+ actualRows = await table.getRows({});
443
+ expect(actualRows).toEqual(expected);
444
+ });
445
+
446
+ it("update matching books with edit-in-edit", async () => {
447
+ const disBooks = Table.findOne({ name: "discusses_books" });
448
+ await updateMatchingRows({
449
+ query: "id=2",
450
+ body: { author: "Leo Tolstoy" },
451
+ });
452
+ await updateMatchingRows({
453
+ query: "author=leo",
454
+ body: { author: "agi", discussant_0: "1", discussant_1: "2" },
455
+ });
456
+ const discBooksRows = (await disBooks.getRows({ book: 2 })).filter(
457
+ ({ discussant }) => discussant === 1 || discussant === 2
458
+ );
459
+ expect(discBooksRows.length).toBe(2);
460
+ });
461
+ });
462
+
400
463
  describe("inbound relations", () => {
401
464
  it("view with inbound relation", async () => {
402
465
  const app = await getApp({ disableCsrf: true });