@saltcorn/server 0.9.6-beta.1 → 0.9.6-beta.11

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/admin.js CHANGED
@@ -99,6 +99,10 @@ const getUserFields = async (req) => {
99
99
  input_type: "email",
100
100
  };
101
101
  }
102
+ if (f.name === "role_id") {
103
+ f.fieldview = "role_select";
104
+ await f.fill_fkey_options();
105
+ }
102
106
  }
103
107
  return userFields;
104
108
  };
@@ -110,65 +114,59 @@ const getUserFields = async (req) => {
110
114
  * @param {User} user
111
115
  * @returns {Promise<Form>}
112
116
  */
113
- const userForm = contract(
114
- is.fun(
115
- [is.obj({}), is.maybe(is.class("User"))],
116
- is.promise(is.class("Form"))
117
- ),
118
- async (req, user) => {
119
- const roleField = new Field({
120
- label: req.__("Role"),
121
- name: "role_id",
122
- type: "Key",
123
- reftable_name: "roles",
124
- });
125
- const roles = (await User.get_roles()).filter((r) => r.role !== "public");
126
- roleField.options = roles.map((r) => ({ label: r.role, value: r.id }));
127
- const can_reset = getState().getConfig("smtp_host", "") !== "";
128
- const userFields = await getUserFields(req);
129
- const form = new Form({
130
- fields: [roleField, ...userFields],
131
- action: "/useradmin/save",
132
- submitLabel: user ? req.__("Save") : req.__("Create"),
133
- });
134
- if (!user) {
117
+ const userForm = async (req, user) => {
118
+ const roleField = new Field({
119
+ label: req.__("Role"),
120
+ name: "role_id",
121
+ type: "Key",
122
+ reftable_name: "roles",
123
+ });
124
+ const roles = (await User.get_roles()).filter((r) => r.role !== "public");
125
+ roleField.options = roles.map((r) => ({ label: r.role, value: r.id }));
126
+ const can_reset = getState().getConfig("smtp_host", "") !== "";
127
+ const userFields = await getUserFields(req);
128
+ const form = new Form({
129
+ fields: userFields,
130
+ action: "/useradmin/save",
131
+ submitLabel: user ? req.__("Save") : req.__("Create"),
132
+ });
133
+ if (!user) {
134
+ form.fields.push(
135
+ new Field({
136
+ label: req.__("Set random password"),
137
+ name: "rnd_password",
138
+ type: "Bool",
139
+ default: true,
140
+ })
141
+ );
142
+ form.fields.push(
143
+ new Field({
144
+ label: req.__("Password"),
145
+ name: "password",
146
+ input_type: "password",
147
+ showIf: { rnd_password: false },
148
+ })
149
+ );
150
+ can_reset &&
135
151
  form.fields.push(
136
152
  new Field({
137
- label: req.__("Set random password"),
138
- name: "rnd_password",
153
+ label: req.__("Send password reset email"),
154
+ name: "send_pwreset_email",
139
155
  type: "Bool",
140
156
  default: true,
157
+ showIf: { rnd_password: true },
141
158
  })
142
159
  );
143
- form.fields.push(
144
- new Field({
145
- label: req.__("Password"),
146
- name: "password",
147
- input_type: "password",
148
- showIf: { rnd_password: false },
149
- })
150
- );
151
- can_reset &&
152
- form.fields.push(
153
- new Field({
154
- label: req.__("Send password reset email"),
155
- name: "send_pwreset_email",
156
- type: "Bool",
157
- default: true,
158
- showIf: { rnd_password: true },
159
- })
160
- );
161
- }
162
- if (user) {
163
- form.hidden("id");
164
- form.values = user;
165
- delete form.values.password;
166
- } else {
167
- form.values.role_id = roles[roles.length - 1].id;
168
- }
169
- return form;
170
160
  }
171
- );
161
+ if (user) {
162
+ form.hidden("id");
163
+ form.values = user;
164
+ delete form.values.password;
165
+ } else {
166
+ form.values.role_id = roles[roles.length - 1].id;
167
+ }
168
+ return form;
169
+ };
172
170
 
173
171
  /**
174
172
  * Dropdown for User Info in left menu
@@ -531,7 +529,10 @@ router.post(
531
529
  if (restart_required)
532
530
  res.json({
533
531
  success: "ok",
534
- notify: req.__("Restart required for changes to take effect."),
532
+ notify:
533
+ req.__("Restart required for changes to take effect.") +
534
+ " " +
535
+ a({ href: "/admin/system" }, req.__("Restart here")),
535
536
  });
536
537
  else res.json({ success: "ok" });
537
538
  }
package/load_plugins.js CHANGED
@@ -141,7 +141,11 @@ const loadAndSaveNewPlugin = async (
141
141
  const existing = await Plugin.findOne({ location: loc });
142
142
  if (!existing && loc !== plugin.location) {
143
143
  await loadAndSaveNewPlugin(
144
- new Plugin({ name: loc, location: loc, source: "npm" }),
144
+ new Plugin({
145
+ name: loc.replace("@saltcorn/", ""),
146
+ location: loc,
147
+ source: "npm",
148
+ }),
145
149
  force,
146
150
  noSignalOrDB
147
151
  );
package/locales/en.json CHANGED
@@ -1425,5 +1425,10 @@
1425
1425
  "Keystore Alias": "Keystore Alias",
1426
1426
  "Keystore Password": "Keystore Password",
1427
1427
  "xcodebuild": "xcodebuild",
1428
- "Provisioning Profile": "Provisioning Profile"
1428
+ "Provisioning Profile": "Provisioning Profile",
1429
+ "Registry editor": "Registry editor",
1430
+ "A short name that will be in the page URL": "A short name that will be in the page URL",
1431
+ "A longer description that is not visible but appears in the page header and is indexed by search engines": "A longer description that is not visible but appears in the page header and is indexed by search engines",
1432
+ "User role required to access page": "User role required to access page",
1433
+ "Example: <code>`/view/TheOtherView?id=${id}`</code>": "Example: <code>`/view/TheOtherView?id=${id}`</code>"
1429
1434
  }
package/locales/it.json CHANGED
@@ -518,5 +518,6 @@
518
518
  "Save before going back": "Save before going back",
519
519
  "Reload after going back": "Reload after going back",
520
520
  "Steps to go back": "Steps to go back",
521
- "%s configuration": "%s configuration"
522
- }
521
+ "%s configuration": "%s configuration",
522
+ "The current theme has no user specific settings": "The current theme has no user specific settings"
523
+ }
package/markup/admin.js CHANGED
@@ -241,6 +241,7 @@ const send_infoarch_page = (args) => {
241
241
  { text: "Pagegroups", href: "/page_group/settings" },
242
242
  { text: "Tags", href: "/tag" },
243
243
  { text: "Diagram", href: "/diagram" },
244
+ { text: "Registry editor", href: "/registry-editor" },
244
245
  ],
245
246
  ...args,
246
247
  });
package/markup/forms.js CHANGED
@@ -28,10 +28,14 @@ const editRoleForm = ({ url, current_role, roles, req }) =>
28
28
  {
29
29
  action: url,
30
30
  method: "post",
31
+ onchange: "saveAndContinue(this)",
31
32
  },
32
33
  csrfField(req),
33
34
  select(
34
- { name: "role", onchange: "form.submit()" },
35
+ {
36
+ name: "role",
37
+ class: "w-unset form-select form-select-sm",
38
+ },
35
39
  roles.map((role) =>
36
40
  option(
37
41
  {
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.6-beta.1",
3
+ "version": "0.9.6-beta.11",
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
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "0.9.6-beta.1",
11
- "@saltcorn/builder": "0.9.6-beta.1",
12
- "@saltcorn/data": "0.9.6-beta.1",
13
- "@saltcorn/admin-models": "0.9.6-beta.1",
14
- "@saltcorn/filemanager": "0.9.6-beta.1",
15
- "@saltcorn/markup": "0.9.6-beta.1",
16
- "@saltcorn/plugins-loader": "0.9.6-beta.1",
17
- "@saltcorn/sbadmin2": "0.9.6-beta.1",
10
+ "@saltcorn/base-plugin": "0.9.6-beta.11",
11
+ "@saltcorn/builder": "0.9.6-beta.11",
12
+ "@saltcorn/data": "0.9.6-beta.11",
13
+ "@saltcorn/admin-models": "0.9.6-beta.11",
14
+ "@saltcorn/filemanager": "0.9.6-beta.11",
15
+ "@saltcorn/markup": "0.9.6-beta.11",
16
+ "@saltcorn/plugins-loader": "0.9.6-beta.11",
17
+ "@saltcorn/sbadmin2": "0.9.6-beta.11",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -179,14 +179,19 @@ function apply_showif() {
179
179
  is_or ? "&_or_field=" + k : ""
180
180
  }`;
181
181
  };
182
- const qss = Object.entries(dynwhere.whereParsed).map(kvToQs);
182
+ const qss = Object.entries(dynwhere.whereParsed).map((kv) => kvToQs(kv));
183
+ if (dynwhere.existingValue) {
184
+ qss.push(`id=${dynwhere.existingValue}`);
185
+ qss.push(`_or_field=id`);
186
+ }
183
187
  if (dynwhere.dereference) {
184
188
  if (Array.isArray(dynwhere.dereference))
185
189
  qss.push(...dynwhere.dereference.map((d) => `dereference=${d}`));
186
190
  else qss.push(`dereference=${dynwhere.dereference}`);
187
191
  }
188
192
  const qs = qss.join("&");
189
- var current = e.attr("data-selected");
193
+ let current = e.attr("data-selected");
194
+ if (current === "null") current = null;
190
195
  e.change(function (ec) {
191
196
  e.attr("data-selected", ec.target.value);
192
197
  });
@@ -195,12 +200,14 @@ function apply_showif() {
195
200
  if (currentOptionsSet === qs) return;
196
201
 
197
202
  const activate = (success, qs) => {
203
+ //re-fetch current, because it may have changed
204
+ let current = e.attr("data-selected");
205
+ if (current === "null") current = null;
198
206
  if (e.prop("data-fetch-options-current-set") === qs) return;
199
207
  e.empty();
200
208
  e.prop("data-fetch-options-current-set", qs);
201
209
  const toAppend = [];
202
- if (!dynwhere.required)
203
- toAppend.push({ label: dynwhere.neutral_label || "", value: "" });
210
+
204
211
  let currentDataOption = undefined;
205
212
  const dataOptions = [];
206
213
  //console.log(success);
@@ -231,13 +238,24 @@ function apply_showif() {
231
238
  ? 1
232
239
  : -1
233
240
  );
241
+ if (!dynwhere.required)
242
+ toAppend.unshift({ label: dynwhere.neutral_label || "", value: "" });
243
+ if (dynwhere.required && dynwhere.placeholder)
244
+ toAppend.unshift({
245
+ disabled: true,
246
+ label: dynwhere.placeholder,
247
+ value: "",
248
+ selected: !current,
249
+ });
234
250
  e.html(
235
251
  toAppend
236
252
  .map(
237
- ({ label, value, selected }) =>
253
+ ({ label, value, selected, disabled }) =>
238
254
  `<option${selected ? ` selected` : ""}${
239
- typeof value !== "undefined" ? ` value="${value}"` : ""
240
- }>${label || ""}</option>`
255
+ disabled ? ` disabled` : ""
256
+ }${typeof value !== "undefined" ? ` value="${value}"` : ""}>${
257
+ label || ""
258
+ }</option>`
241
259
  )
242
260
  .join("")
243
261
  );
@@ -428,7 +446,7 @@ function get_form_record(e_in, select_labels) {
428
446
 
429
447
  const e = e_in.viewname
430
448
  ? $(`form[data-viewname="${e_in.viewname}"]`)
431
- : e_in.closest(".form-namespace");
449
+ : $(e_in).closest(".form-namespace");
432
450
 
433
451
  const form = $(e).closest("form");
434
452
 
@@ -462,6 +480,38 @@ function get_form_record(e_in, select_labels) {
462
480
  rec[name] = f(rec[name], $this);
463
481
  }
464
482
  });
483
+
484
+ const joinFieldsStr =
485
+ typeof e_in !== "string" && $(e_in).attr("data-show-if-joinfields");
486
+ if (joinFieldsStr) {
487
+ const joinFields = JSON.parse(decodeURIComponent(joinFieldsStr));
488
+
489
+ const joinVals = $(e_in).prop("data-join-values");
490
+ const kvals = $(e_in).prop("data-join-key-values") || {};
491
+ let differentKeys = false;
492
+ for (const { ref } of joinFields) {
493
+ if (rec[ref] != kvals[ref]) differentKeys = true;
494
+ }
495
+ if (!joinVals || differentKeys) {
496
+ $(e_in).prop("data-join-values", {});
497
+ const keyVals = {};
498
+ for (const { ref, target, refTable } of joinFields) {
499
+ keyVals[ref] = rec[ref];
500
+ $.ajax(`/api/${refTable}?id=${rec[ref]}`, {
501
+ success: (val) => {
502
+ const jvs = $(e_in).prop("data-join-values") || {};
503
+
504
+ jvs[ref] = val.success[0];
505
+ $(e_in).prop("data-join-values", jvs);
506
+ apply_showif();
507
+ },
508
+ });
509
+ }
510
+ $(e_in).prop("data-join-key-values", keyVals);
511
+ } else if (joinFieldsStr) {
512
+ Object.assign(rec, joinVals);
513
+ }
514
+ }
465
515
  return rec;
466
516
  }
467
517
  function showIfFormulaInputs(e, fml) {
@@ -1219,7 +1269,13 @@ function restore_old_button(btnId) {
1219
1269
  btn.removeData("old-text");
1220
1270
  }
1221
1271
 
1222
- async function common_done(res, viewname, isWeb = true) {
1272
+ async function common_done(res, viewnameOrElem, isWeb = true) {
1273
+ const viewname =
1274
+ typeof viewnameOrElem === "string"
1275
+ ? viewnameOrElem
1276
+ : $(viewnameOrElem)
1277
+ .closest("[data-sc-embed-viewname]")
1278
+ .attr("data-sc-embed-viewname");
1223
1279
  if (window._sc_loglevel > 4)
1224
1280
  console.log("ajax result directives", viewname, res);
1225
1281
  const handle = async (element, fn) => {
@@ -1231,15 +1287,15 @@ async function common_done(res, viewname, isWeb = true) {
1231
1287
  if (res.row && res.field_names) {
1232
1288
  const f = new Function(`viewname, row, {${res.field_names}}`, s);
1233
1289
  const evalres = await f(viewname, res.row, res.row);
1234
- if (evalres) await common_done(evalres, viewname, isWeb);
1290
+ if (evalres) await common_done(evalres, viewnameOrElem, isWeb);
1235
1291
  } else if (res.row) {
1236
1292
  const f = new Function(`viewname, row`, s);
1237
1293
  const evalres = await f(viewname, res.row);
1238
- if (evalres) await common_done(evalres, viewname, isWeb);
1294
+ if (evalres) await common_done(evalres, viewnameOrElem, isWeb);
1239
1295
  } else {
1240
1296
  const f = new Function(`viewname`, s);
1241
1297
  const evalres = await f(viewname);
1242
- if (evalres) await common_done(evalres, viewname, isWeb);
1298
+ if (evalres) await common_done(evalres, viewnameOrElem, isWeb);
1243
1299
  }
1244
1300
  };
1245
1301
  if (res.notify) await handle(res.notify, notifyAlert);
@@ -1252,9 +1308,10 @@ async function common_done(res, viewname, isWeb = true) {
1252
1308
  notifyAlert({ type: "success", text: text })
1253
1309
  );
1254
1310
  if (res.set_fields && (viewname || res.set_fields._viewname)) {
1255
- const form = $(
1256
- `form[data-viewname="${res.set_fields._viewname || viewname}"]`
1257
- );
1311
+ const form =
1312
+ typeof viewnameOrElem === "string" || res.set_fields._viewname
1313
+ ? $(`form[data-viewname="${res.set_fields._viewname || viewname}"]`)
1314
+ : $(viewnameOrElem).closest("form[data-viewname]");
1258
1315
  if (form.length === 0 && set_state_fields) {
1259
1316
  // assume this is a filter
1260
1317
  set_state_fields(
@@ -1279,9 +1336,14 @@ async function common_done(res, viewname, isWeb = true) {
1279
1336
  if (input.attr("type") === "checkbox")
1280
1337
  input.prop("checked", res.set_fields[k]);
1281
1338
  else input.val(res.set_fields[k]);
1339
+ if (input.attr("data-selected")) {
1340
+ input.attr("data-selected", res.set_fields[k]);
1341
+ }
1342
+
1282
1343
  input.trigger("set_form_field");
1283
1344
  });
1284
1345
  }
1346
+ form.trigger("change");
1285
1347
  }
1286
1348
 
1287
1349
  if (res.download) {
@@ -1440,10 +1502,10 @@ const columnSummary = (col) => {
1440
1502
  };
1441
1503
 
1442
1504
  function submitWithEmptyAction(form) {
1443
- var formAction = form.action;
1444
- form.action = "javascript:void(0)";
1505
+ var formAction = form.getAttribute("action");
1506
+ form.setAttribute("action", "javascript:void(0)");
1445
1507
  form.submit();
1446
- form.action = formAction;
1508
+ form.setAttribute("action", formAction);
1447
1509
  }
1448
1510
 
1449
1511
  function unique_field_from_rows(
@@ -506,3 +506,11 @@ tr[onclick] {
506
506
  .modal-header {
507
507
  justify-content: space-between;
508
508
  }
509
+
510
+ ul.katetree {
511
+ list-style-type: none;
512
+ }
513
+
514
+ ul.katetree details ul {
515
+ list-style-type: none;
516
+ }
@@ -236,7 +236,13 @@ function reset_spinners() {
236
236
  });
237
237
  }
238
238
 
239
- function view_post(viewname, route, data, onDone, sendState) {
239
+ function view_post(viewnameOrElem, route, data, onDone, sendState) {
240
+ const viewname =
241
+ typeof viewnameOrElem === "string"
242
+ ? viewnameOrElem
243
+ : $(viewnameOrElem)
244
+ .closest("[data-sc-embed-viewname]")
245
+ .attr("data-sc-embed-viewname");
240
246
  const query = sendState
241
247
  ? `?${new URL(get_current_state_url()).searchParams.toString()}`
242
248
  : "";
@@ -254,7 +260,7 @@ function view_post(viewname, route, data, onDone, sendState) {
254
260
  })
255
261
  .done(function (res) {
256
262
  if (onDone) onDone(res);
257
- ajax_done(res, viewname);
263
+ ajax_done(res, viewnameOrElem);
258
264
  reset_spinners();
259
265
  })
260
266
  .fail(function (res) {
package/routes/actions.js CHANGED
@@ -375,6 +375,7 @@ router.get(
375
375
 
376
376
  const form = await triggerForm(req, trigger);
377
377
  form.values = trigger;
378
+ form.onChange = `saveAndContinue(this)`;
378
379
  send_events_page({
379
380
  res,
380
381
  req,
@@ -383,6 +384,7 @@ router.get(
383
384
  contents: {
384
385
  type: "card",
385
386
  title: req.__("Edit trigger %s", id),
387
+ titleAjaxIndicator: true,
386
388
  contents: renderForm(form, req.csrfToken()),
387
389
  },
388
390
  });
@@ -464,6 +466,10 @@ router.post(
464
466
  ...form.values.configuration,
465
467
  };
466
468
  await Trigger.update(trigger.id, form.values); //{configuration: form.values});
469
+ if (req.xhr) {
470
+ res.json({ success: "ok" });
471
+ return;
472
+ }
467
473
  req.flash("success", req.__("Action information saved"));
468
474
  res.redirect(`/actions/`);
469
475
  }
package/routes/fields.js CHANGED
@@ -255,6 +255,7 @@ const fieldFlow = (req) =>
255
255
  expression = "__aggregation";
256
256
  attributes.agg_relation = context.agg_relation;
257
257
  attributes.agg_field = context.agg_field;
258
+ attributes.agg_order_by = context.agg_order_by;
258
259
  attributes.aggwhere = context.aggwhere;
259
260
  attributes.aggregate = context.aggregate;
260
261
  const [table, ref] = context.agg_relation.split(".");
@@ -435,46 +436,64 @@ const fieldFlow = (req) =>
435
436
 
436
437
  const { child_field_list, child_relations } =
437
438
  await table.get_child_relations(true);
438
- const agg_field_opts = child_relations.map(
439
- ({ table, key_field, through }) => {
440
- const aggKey =
441
- (through ? `${through.name}->` : "") +
442
- `${table.name}.${key_field.name}`;
443
- aggStatOptions[aggKey] = [
444
- "Count",
445
- "CountUnique",
446
- "Avg",
447
- "Sum",
448
- "Max",
449
- "Min",
450
- "Array_Agg",
451
- ];
452
- table.fields.forEach((f) => {
453
- if (f.type && f.type.name === "Date") {
454
- aggStatOptions[aggKey].push(`Latest ${f.name}`);
455
- aggStatOptions[aggKey].push(`Earliest ${f.name}`);
456
- }
457
- });
458
- return {
459
- name: `agg_field`,
460
- label: req.__("On Field"),
461
- type: "String",
462
- required: true,
463
- attributes: {
464
- options: table.fields
465
- .filter((f) => !f.calculated || f.stored)
466
- .map((f) => ({
467
- label: f.name,
468
- name: `${f.name}@${f.type_name}`,
469
- })),
470
- },
471
- showIf: {
472
- agg_relation: aggKey,
473
- expression_type: "Aggregation",
474
- },
475
- };
476
- }
477
- );
439
+ const agg_field_opts = [];
440
+ const agg_order_opts = [];
441
+ child_relations.forEach(({ table, key_field, through }) => {
442
+ const aggKey =
443
+ (through ? `${through.name}->` : "") +
444
+ `${table.name}.${key_field.name}`;
445
+ aggStatOptions[aggKey] = [
446
+ "Count",
447
+ "CountUnique",
448
+ "Avg",
449
+ "Sum",
450
+ "Max",
451
+ "Min",
452
+ "Array_Agg",
453
+ ];
454
+ table.fields.forEach((f) => {
455
+ if (f.type && f.type.name === "Date") {
456
+ aggStatOptions[aggKey].push(`Latest ${f.name}`);
457
+ aggStatOptions[aggKey].push(`Earliest ${f.name}`);
458
+ }
459
+ });
460
+ agg_field_opts.push({
461
+ name: `agg_field`,
462
+ label: req.__("On Field"),
463
+ type: "String",
464
+ required: true,
465
+ attributes: {
466
+ options: table.fields
467
+ .filter((f) => !f.calculated || f.stored)
468
+ .map((f) => ({
469
+ label: f.name,
470
+ name: `${f.name}@${f.type_name}`,
471
+ })),
472
+ },
473
+ showIf: {
474
+ agg_relation: aggKey,
475
+ expression_type: "Aggregation",
476
+ },
477
+ });
478
+ agg_order_opts.push({
479
+ name: `agg_order_by`,
480
+ label: req.__("Order by"),
481
+ type: "String",
482
+ attributes: {
483
+ options: table.fields
484
+ .filter((f) => !f.calculated || f.stored)
485
+ .map((f) => ({
486
+ label: f.name,
487
+ name: f.name,
488
+ })),
489
+ },
490
+ showIf: {
491
+ agg_relation: aggKey,
492
+ expression_type: "Aggregation",
493
+ aggregate: "Array_Agg",
494
+ },
495
+ });
496
+ });
478
497
  return new Form({
479
498
  fields: [
480
499
  {
@@ -520,6 +539,7 @@ const fieldFlow = (req) =>
520
539
  required: false,
521
540
  showIf: { expression_type: "Aggregation" },
522
541
  },
542
+ ...agg_order_opts,
523
543
  {
524
544
  name: "model",
525
545
  label: req.__("Model"),
@@ -1129,7 +1149,7 @@ router.post(
1129
1149
  const jf = table.getField(ref);
1130
1150
  const jtable = Table.findOne(jf.reftable_name);
1131
1151
  const jrow = await jtable.getRow(
1132
- { [jtable.pk_name]: row[ref] },
1152
+ { [jtable.pk_name]: row[ref]?.[jtable.pk_name] || row[ref] },
1133
1153
  { forUser: req.user, forPublic: !req.user }
1134
1154
  );
1135
1155
  row[ref] = jrow;
@@ -1156,7 +1176,8 @@ router.post(
1156
1176
  if (!fv) res.send(text(result));
1157
1177
  else res.send(fv.run(result, req, { row, ...configuration }));
1158
1178
  } catch (e) {
1159
- return res.status(400).send(`Error: ${e.message}`);
1179
+ console.error("show-calculated error", e);
1180
+ return res.status(200).send(``);
1160
1181
  }
1161
1182
  })
1162
1183
  );
package/routes/index.js CHANGED
@@ -36,6 +36,7 @@ const roleadmin = require("../auth/roleadmin");
36
36
  const tags = require("./tags");
37
37
  const tagentries = require("./tag_entries");
38
38
  const diagram = require("./diagram");
39
+ const registry = require("./registry");
39
40
  const sync = require("./sync");
40
41
 
41
42
  module.exports =
@@ -78,5 +79,6 @@ module.exports =
78
79
  app.use("/tag", tags);
79
80
  app.use("/tag-entries", tagentries);
80
81
  app.use("/diagram", diagram);
82
+ app.use("/registry-editor", registry);
81
83
  app.use("/sync", sync);
82
84
  };
@@ -94,7 +94,7 @@ const pagePropertiesForm = async (req, isNew) => {
94
94
  if (groups.includes(s) && isNew)
95
95
  return req.__("A page group with this name already exists");
96
96
  },
97
- sublabel: req.__("A short name that will be in your URL"),
97
+ sublabel: req.__("A short name that will be in the page URL"),
98
98
  type: "String",
99
99
  attributes: { autofocus: true },
100
100
  }),
@@ -107,13 +107,15 @@ const pagePropertiesForm = async (req, isNew) => {
107
107
  new Field({
108
108
  label: req.__("Description"),
109
109
  name: "description",
110
- sublabel: req.__("A longer description"),
110
+ sublabel: req.__(
111
+ "A longer description that is not visible but appears in the page header and is indexed by search engines"
112
+ ),
111
113
  input_type: "text",
112
114
  }),
113
115
  {
114
116
  name: "min_role",
115
117
  label: req.__("Minimum role"),
116
- sublabel: req.__("Role required to access page"),
118
+ sublabel: req.__("User role required to access page"),
117
119
  input_type: "select",
118
120
  options: roles.map((r) => ({ value: r.id, label: r.role })),
119
121
  },
@@ -336,6 +338,15 @@ router.get(
336
338
  )
337
339
  ),
338
340
  },
341
+ {
342
+ type: "card",
343
+ title: req.__("Root pages"),
344
+ titleAjaxIndicator: true,
345
+ contents: renderForm(
346
+ getRootPageForm(pages, pageGroups, roles, req),
347
+ req.csrfToken()
348
+ ),
349
+ },
339
350
  {
340
351
  type: "card",
341
352
  title: req.__("Your page groups"),
@@ -357,15 +368,6 @@ router.get(
357
368
  )
358
369
  ),
359
370
  },
360
- {
361
- type: "card",
362
- title: req.__("Root pages"),
363
- titleAjaxIndicator: true,
364
- contents: renderForm(
365
- getRootPageForm(pages, pageGroups, roles, req),
366
- req.csrfToken()
367
- ),
368
- },
369
371
  ],
370
372
  });
371
373
  })
@@ -392,6 +394,7 @@ const wrap = (contents, noCard, req, page) => ({
392
394
  {
393
395
  type: noCard ? "container" : "card",
394
396
  title: page ? page.name : req.__("New"),
397
+ titleAjaxIndicator: true,
395
398
  contents,
396
399
  },
397
400
  ],
@@ -418,6 +421,7 @@ router.get(
418
421
  form.hidden("id");
419
422
  form.values = page;
420
423
  form.values.no_menu = page.attributes?.no_menu;
424
+ form.onChange = `saveAndContinue(this)`;
421
425
  res.sendWrap(
422
426
  req.__(`Page attributes`),
423
427
  wrap(renderForm(form, req.csrfToken()), false, req, page)
@@ -477,7 +481,8 @@ router.post(
477
481
  pageRow.layout = {};
478
482
  }
479
483
  await Page.update(+id, pageRow);
480
- res.redirect(`/pageedit/`);
484
+ if (req.xhr) res.json({ success: "ok" });
485
+ else res.redirect(`/pageedit/`);
481
486
  } else {
482
487
  if (!pageRow.layout) pageRow.layout = {};
483
488
  if (!pageRow.fixed_states) pageRow.fixed_states = {};
package/routes/plugins.js CHANGED
@@ -1315,7 +1315,12 @@ router.get(
1315
1315
  await upgrade_all_tenants_plugins((p, f) =>
1316
1316
  load_plugins.loadPlugin(p, f)
1317
1317
  );
1318
- req.flash("success", req.__(`Modules up-to-date. Please restart server`));
1318
+ req.flash(
1319
+ "success",
1320
+ req.__(`Modules up-to-date. Please restart server`) +
1321
+ ". " +
1322
+ a({ href: "/admin/system" }, req.__("Restart here"))
1323
+ );
1319
1324
  } else {
1320
1325
  const installed_plugins = await Plugin.find({});
1321
1326
  for (const plugin of installed_plugins) {
@@ -0,0 +1,289 @@
1
+ const Router = require("express-promise-router");
2
+
3
+ const db = require("@saltcorn/data/db");
4
+ const { mkTable, link, post_btn, renderForm } = require("@saltcorn/markup");
5
+ const {
6
+ script,
7
+ domReady,
8
+ a,
9
+ div,
10
+ i,
11
+ text,
12
+ button,
13
+ input,
14
+ label,
15
+ form,
16
+ ul,
17
+ li,
18
+ details,
19
+ summary,
20
+ } = require("@saltcorn/markup/tags");
21
+ const Table = require("@saltcorn/data/models/table");
22
+ const { isAdmin, error_catcher } = require("./utils");
23
+ const { send_infoarch_page } = require("../markup/admin.js");
24
+ const View = require("@saltcorn/data/models/view");
25
+ const Page = require("@saltcorn/data/models/page");
26
+ const Form = require("@saltcorn/data/models/form");
27
+ const {
28
+ table_pack,
29
+ view_pack,
30
+ plugin_pack,
31
+ page_pack,
32
+ page_group_pack,
33
+ role_pack,
34
+ library_pack,
35
+ trigger_pack,
36
+ tag_pack,
37
+ model_pack,
38
+ model_instance_pack,
39
+ event_log_pack,
40
+ install_pack,
41
+ } = require("@saltcorn/admin-models/models/pack");
42
+ const Trigger = require("@saltcorn/data/models/trigger");
43
+ /**
44
+ * @type {object}
45
+ * @const
46
+ * @namespace listRouter
47
+ * @category server
48
+ * @subcategory routes
49
+ */
50
+ const router = new Router();
51
+
52
+ // export our router to be mounted by the parent application
53
+ module.exports = router;
54
+
55
+ async function asyncFilter(arr, cb) {
56
+ const filtered = [];
57
+
58
+ for (const element of arr) {
59
+ const needAdd = await cb(element);
60
+
61
+ if (needAdd) {
62
+ filtered.push(element);
63
+ }
64
+ }
65
+
66
+ return filtered;
67
+ }
68
+ router.get(
69
+ "/",
70
+ isAdmin,
71
+ error_catcher(async (req, res) => {
72
+ const { etype, ename, q } = req.query;
73
+ const qlink = q ? `&q=${encodeURIComponent(q)}` : "";
74
+ let edContents = "Choose an entity to edit";
75
+ const all_tables = await Table.find({}, { orderBy: "name", nocase: true });
76
+ const all_views = await View.find({}, { orderBy: "name", nocase: true });
77
+ const all_pages = await Page.find({}, { orderBy: "name", nocase: true });
78
+ const all_triggers = await Trigger.find(
79
+ {},
80
+ { orderBy: "name", nocase: true }
81
+ );
82
+ let tables, views, pages, triggers;
83
+ if (q) {
84
+ const qlower = q.toLowerCase();
85
+ const includesQ = (s) => s.toLowerCase().includes(qlower);
86
+
87
+ tables = await asyncFilter(all_tables, async (t) => {
88
+ const pack = await table_pack(t);
89
+ return includesQ(JSON.stringify(pack));
90
+ });
91
+ views = await asyncFilter(all_views, async (t) => {
92
+ const pack = await view_pack(t);
93
+ return includesQ(JSON.stringify(pack));
94
+ });
95
+ pages = await asyncFilter(all_pages, async (t) => {
96
+ const pack = await page_pack(t);
97
+ return includesQ(JSON.stringify(pack));
98
+ });
99
+ triggers = await asyncFilter(all_triggers, async (t) => {
100
+ const pack = await trigger_pack(t);
101
+ return includesQ(JSON.stringify(pack));
102
+ });
103
+ } else {
104
+ tables = all_tables;
105
+ views = all_views;
106
+ pages = all_pages;
107
+ triggers = all_triggers;
108
+ }
109
+ const li_link = (etype1, ename1) =>
110
+ li(
111
+ a(
112
+ {
113
+ href: `/registry-editor?etype=${etype1}&ename=${encodeURIComponent(
114
+ ename1
115
+ )}${qlink}`,
116
+ class: etype1 === etype && ename1 === ename ? "fw-bold" : undefined,
117
+ },
118
+ ename1
119
+ )
120
+ );
121
+ const mkForm = (jsonVal) =>
122
+ new Form({
123
+ labelCols: 0,
124
+ action: `/registry-editor?etype=${etype}&ename=${encodeURIComponent(
125
+ ename
126
+ )}${qlink}`,
127
+
128
+ values: { regval: JSON.stringify(jsonVal, null, 2) },
129
+ fields: [
130
+ {
131
+ name: "regval",
132
+ label: "",
133
+ input_type: "code",
134
+ attributes: { mode: "application/json" },
135
+ },
136
+ ],
137
+ });
138
+ switch (etype) {
139
+ case "table":
140
+ const tpack = await table_pack(
141
+ all_tables.find((t) => t.name === ename)
142
+ );
143
+ edContents = renderForm(mkForm(tpack), req.csrfToken());
144
+ break;
145
+ case "view":
146
+ const vpack = await view_pack(all_views.find((v) => v.name === ename));
147
+ edContents = renderForm(mkForm(vpack), req.csrfToken());
148
+ break;
149
+ case "page":
150
+ const ppack = await page_pack(all_pages.find((v) => v.name === ename));
151
+ edContents = renderForm(mkForm(ppack), req.csrfToken());
152
+ break;
153
+ case "trigger":
154
+ const trpack = await trigger_pack(
155
+ all_triggers.find((t) => t.name === ename)
156
+ );
157
+ edContents = renderForm(mkForm(trpack), req.csrfToken());
158
+ break;
159
+ }
160
+ send_infoarch_page({
161
+ res,
162
+ req,
163
+ active_sub: "Registry editor",
164
+ contents: {
165
+ widths: [3, 9],
166
+ besides: [
167
+ {
168
+ type: "card",
169
+ bodyClass: "p-1",
170
+ title: "Entities",
171
+ contents: div(
172
+ form(
173
+ { method: "GET", action: `/registry-editor` },
174
+ div(
175
+ { class: "input-group search-bar mb-2" },
176
+ etype &&
177
+ input({ type: "hidden", name: "etype", value: etype }),
178
+ ename &&
179
+ input({ type: "hidden", name: "ename", value: ename }),
180
+ input({
181
+ type: "search",
182
+ class: "form-control search-bar ps-2 hasbl",
183
+ placeholder: "Search",
184
+ name: "q",
185
+ value: q,
186
+ "aria-label": "Search",
187
+ "aria-describedby": "button-search-submit",
188
+ }),
189
+ button(
190
+ {
191
+ class: "btn btn-outline-secondary search-bar",
192
+ type: "submit",
193
+ },
194
+ i({ class: "fas fa-search" })
195
+ )
196
+ )
197
+ ),
198
+ // following https://iamkate.com/code/tree-views/
199
+ ul(
200
+ { class: "katetree ps-2" },
201
+ li(
202
+ details(
203
+ { open: q || etype === "table" },
204
+ summary("Tables"),
205
+ ul(
206
+ { class: "ps-3" },
207
+ tables.map((t) => li_link("table", t.name))
208
+ )
209
+ )
210
+ ),
211
+ li(
212
+ details(
213
+ { open: q || etype === "view" },
214
+ summary("Views"),
215
+ ul(
216
+ { class: "ps-3" },
217
+ views.map((v) => li_link("view", v.name))
218
+ )
219
+ )
220
+ ),
221
+ li(
222
+ details(
223
+ { open: q || etype === "page" }, //
224
+ summary("Pages"),
225
+ ul(
226
+ { class: "ps-3" },
227
+ pages.map((p) => li_link("page", p.name))
228
+ )
229
+ )
230
+ ),
231
+ li(
232
+ details(
233
+ { open: q || etype === "trigger" }, //
234
+ summary("Triggers"),
235
+ ul(
236
+ { class: "ps-3" },
237
+ triggers.map((t) => li_link("trigger", t.name))
238
+ )
239
+ )
240
+ )
241
+ )
242
+ ),
243
+ },
244
+ {
245
+ type: "card",
246
+ title:
247
+ ename && etype
248
+ ? `Registry editor: ${ename} ${etype}`
249
+ : "Registry editor",
250
+ contents: edContents,
251
+ },
252
+ ],
253
+ },
254
+ });
255
+ })
256
+ );
257
+
258
+ router.post(
259
+ "/",
260
+ isAdmin,
261
+ error_catcher(async (req, res) => {
262
+ const { etype, ename, q } = req.query;
263
+ const qlink = q ? `&q=${encodeURIComponent(q)}` : "";
264
+
265
+ const entVal = JSON.parse(req.body.regval);
266
+ let pack = { plugins: [], tables: [], views: [], pages: [], triggers: [] };
267
+
268
+ switch (etype) {
269
+ case "table":
270
+ pack.tables = [entVal];
271
+ break;
272
+ case "view":
273
+ pack.views = [entVal];
274
+ break;
275
+ case "page":
276
+ pack.pages = [entVal];
277
+ break;
278
+ case "trigger":
279
+ pack.triggers = [entVal];
280
+ break;
281
+ }
282
+ await install_pack(pack);
283
+ res.redirect(
284
+ `/registry-editor?etype=${etype}&ename=${encodeURIComponent(
285
+ ename
286
+ )}${qlink}`
287
+ );
288
+ })
289
+ );
package/routes/tables.js CHANGED
@@ -681,6 +681,10 @@ const attribBadges = (f) => {
681
681
  "on_delete_cascade",
682
682
  "on_delete",
683
683
  "unique_error_msg",
684
+ "ref",
685
+ "table",
686
+ "agg_field",
687
+ "agg_relation",
684
688
  ].includes(k)
685
689
  )
686
690
  return;
package/routes/utils.js CHANGED
@@ -14,7 +14,7 @@ const {
14
14
  } = require("@saltcorn/data/db/state");
15
15
  const { get_base_url } = require("@saltcorn/data/models/config");
16
16
  const { hash } = require("@saltcorn/data/utils");
17
- const { input, script, domReady } = require("@saltcorn/markup/tags");
17
+ const { input, script, domReady, a } = require("@saltcorn/markup/tags");
18
18
  const session = require("express-session");
19
19
  const cookieSession = require("cookie-session");
20
20
  const is = require("contractis/is");
@@ -398,7 +398,10 @@ const admin_config_route = ({
398
398
  if (restart_required)
399
399
  res.json({
400
400
  success: "ok",
401
- notify: req.__("Restart required for changes to take effect."),
401
+ notify:
402
+ req.__("Restart required for changes to take effect.") +
403
+ " " +
404
+ a({ href: "/admin/system" }, req.__("Restart here")),
402
405
  });
403
406
  else res.json({ success: "ok" });
404
407
  }
package/routes/view.js CHANGED
@@ -127,7 +127,7 @@ router.get(
127
127
  else {
128
128
  const qs = "";
129
129
  const contents =
130
- typeof contents0 === "string" && !req.xhr
130
+ typeof contents0 === "string"
131
131
  ? div(
132
132
  {
133
133
  class: "d-inline",
@@ -371,6 +371,7 @@ router.get(
371
371
  const form = await viewForm(req, tableOptions, roles, pages, viewrow);
372
372
  const inbound_connected = await viewrow.inbound_connected_objects();
373
373
  form.hidden("id");
374
+ form.onChange = `saveAndContinue(this)`;
374
375
  res.sendWrap(req.__(`Edit view`), {
375
376
  above: [
376
377
  {
@@ -383,6 +384,7 @@ router.get(
383
384
  {
384
385
  type: "card",
385
386
  class: "mt-0",
387
+ titleAjaxIndicator: true,
386
388
  title: req.__(
387
389
  `%s view - %s on %s`,
388
390
  viewname,
@@ -541,12 +543,14 @@ router.post(
541
543
  //console.log(v);
542
544
  await View.create(v);
543
545
  }
544
- res.redirect(
545
- addOnDoneRedirect(
546
- `/viewedit/config/${encodeURIComponent(v.name)}`,
547
- req
548
- )
549
- );
546
+ if (req.xhr) res.json({ success: "ok" });
547
+ else
548
+ res.redirect(
549
+ addOnDoneRedirect(
550
+ `/viewedit/config/${encodeURIComponent(v.name)}`,
551
+ req
552
+ )
553
+ );
550
554
  }
551
555
  } else {
552
556
  sendForm(form);
@@ -902,7 +906,7 @@ router.post(
902
906
  ? `/${req.query.on_done_redirect}`
903
907
  : "/viewedit";
904
908
  res.redirect(redirectTarget);
905
- } else res.json({ okay: true, responseText: message });
909
+ } else res.json({ success: "ok" });
906
910
  })
907
911
  );
908
912
 
@@ -391,6 +391,27 @@ describe("Field Endpoints", () => {
391
391
  })
392
392
  .expect(toBeTrue((r) => +r.text > 2));
393
393
  });
394
+ it("should show calculated field with two single joinfields", async () => {
395
+ const loginCookie = await getAdminLoginCookie();
396
+ const table = Table.findOne({ name: "patients" });
397
+ await Field.create({
398
+ table,
399
+ label: "pagesp12",
400
+ type: "Integer",
401
+ calculated: true,
402
+ stored: true,
403
+ expression: "favbook.pages+1+favbook.id",
404
+ });
405
+ const app = await getApp({ disableCsrf: true });
406
+
407
+ await request(app)
408
+ .post("/field/show-calculated/patients/pagesp12/show")
409
+ .set("Cookie", loginCookie)
410
+ .send({
411
+ id: 1,
412
+ })
413
+ .expect(toBeTrue((r) => +r.text > 2));
414
+ });
394
415
  it("should show calculated field with double joinfield", async () => {
395
416
  const loginCookie = await getAdminLoginCookie();
396
417
  const table = Table.findOne({ name: "readings" });
@@ -76,7 +76,7 @@ describe("page create", () => {
76
76
  await request(app)
77
77
  .get("/pageedit/new")
78
78
  .set("Cookie", loginCookie)
79
- .expect(toInclude("A short name that will be in your URL"));
79
+ .expect(toInclude("A short name that will be in the page URL"));
80
80
  });
81
81
  it("shows new with html file selector", async () => {
82
82
  const app = await getApp({ disableCsrf: true });
@@ -237,7 +237,7 @@ describe("pageedit", () => {
237
237
  await request(app)
238
238
  .get("/pageedit/edit-properties/a_page")
239
239
  .set("Cookie", loginCookie)
240
- .expect(toInclude("A short name that will be in your URL"));
240
+ .expect(toInclude("A short name that will be in the page URL"));
241
241
 
242
242
  //TODO full context
243
243
  const ctx = encodeURIComponent(JSON.stringify({}));
@@ -137,8 +137,10 @@ describe("Plugin dependency resolution and upgrade", () => {
137
137
  .expect(toRedirect("/plugins"));
138
138
  const quill = await Plugin.findOne({ name: "quill-editor" });
139
139
  expect(quill.location).toBe("@saltcorn/quill-editor");
140
+ expect(quill.name).toBe("quill-editor");
140
141
  const html = await Plugin.findOne({ location: "@saltcorn/html" });
141
142
  expect(html.location).toBe("@saltcorn/html");
143
+ expect(html.name).toBe("html");
142
144
  const html_type = getState().types.HTML;
143
145
  expect(!!html_type.fieldviews.Quill).toBe(true);
144
146
  });