@saltcorn/server 0.9.6-beta.0 → 0.9.6-beta.10

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/routes/admin.js CHANGED
@@ -1710,11 +1710,14 @@ router.get(
1710
1710
  above: [
1711
1711
  {
1712
1712
  type: "card",
1713
+ titleAjaxIndicator: true,
1713
1714
  title: req.__("Build mobile app"),
1714
1715
  contents: form(
1715
1716
  {
1716
1717
  action: "/admin/build-mobile-app",
1717
1718
  method: "post",
1719
+ onchange: "builderMenuChanged(this)",
1720
+ id: "buildMobileAppForm",
1718
1721
  },
1719
1722
 
1720
1723
  fieldset(
@@ -2178,19 +2181,21 @@ router.get(
2178
2181
  class: "form-control form-select",
2179
2182
  multiple: true,
2180
2183
  },
2181
- withSyncInfo.map((table) =>
2182
- option({
2183
- id: `${table.name}_unsynched_opt`,
2184
- value: table.name,
2185
- label: table.name,
2186
- hidden:
2187
- builderSettings.synchedTables?.indexOf(
2184
+ withSyncInfo
2185
+ .filter(
2186
+ (table) =>
2187
+ !builderSettings.synchedTables ||
2188
+ builderSettings.synchedTables.indexOf(
2188
2189
  table.name
2189
- ) >= 0
2190
- ? true
2191
- : false,
2192
- })
2193
- )
2190
+ ) < 0
2191
+ )
2192
+ .map((table) =>
2193
+ option({
2194
+ id: `${table.name}_unsynched_opt`,
2195
+ value: table.name,
2196
+ label: table.name,
2197
+ })
2198
+ )
2194
2199
  )
2195
2200
  ),
2196
2201
  div(
@@ -2228,19 +2233,20 @@ router.get(
2228
2233
  class: "form-control form-select",
2229
2234
  multiple: true,
2230
2235
  },
2231
- withSyncInfo.map((table) =>
2232
- option({
2233
- id: `${table.name}_synched_opt`,
2234
- value: table.name,
2235
- label: table.name,
2236
- hidden:
2236
+ withSyncInfo
2237
+ .filter(
2238
+ (table) =>
2237
2239
  builderSettings.synchedTables?.indexOf(
2238
2240
  table.name
2239
2241
  ) >= 0
2240
- ? false
2241
- : true,
2242
- })
2243
- )
2242
+ )
2243
+ .map((table) =>
2244
+ option({
2245
+ id: `${table.name}_synched_opt`,
2246
+ value: table.name,
2247
+ label: table.name,
2248
+ })
2249
+ )
2244
2250
  )
2245
2251
  )
2246
2252
  )
@@ -2875,13 +2881,6 @@ router.post(
2875
2881
  ) {
2876
2882
  spawnParams.push("--tenantAppName", db.getTenantSchema());
2877
2883
  }
2878
- const excludedPlugins = (await Plugin.find())
2879
- .filter(
2880
- (plugin) =>
2881
- ["base", "sbadmin2"].indexOf(plugin.name) < 0 &&
2882
- includedPlugins.indexOf(plugin.name) < 0
2883
- )
2884
- .map((plugin) => plugin.name);
2885
2884
 
2886
2885
  if (buildType) spawnParams.push("--buildType", buildType);
2887
2886
  if (keystoreFile) spawnParams.push("--androidKeystore", keystoreFile);
@@ -2889,28 +2888,6 @@ router.post(
2889
2888
  spawnParams.push("--androidKeyStoreAlias", keystoreAlias);
2890
2889
  if (keystorePassword)
2891
2890
  spawnParams.push("--androidKeystorePassword", keystorePassword);
2892
- await getState().setConfig("mobile_builder_settings", {
2893
- entryPoint,
2894
- entryPointType,
2895
- androidPlatform,
2896
- iOSPlatform,
2897
- useDocker,
2898
- appName,
2899
- appId,
2900
- appVersion,
2901
- appIcon,
2902
- serverURL,
2903
- splashPage,
2904
- autoPublicLogin,
2905
- allowOfflineMode,
2906
- synchedTables: synchedTables,
2907
- includedPlugins: includedPlugins,
2908
- excludedPlugins,
2909
- provisioningProfile,
2910
- keystoreFile,
2911
- keystoreAlias,
2912
- buildType,
2913
- });
2914
2891
  // end http call, return the out directory name
2915
2892
  // the gui polls for results
2916
2893
  res.json({ build_dir_name: outDirName });
@@ -3014,6 +2991,29 @@ router.get(
3014
2991
  })
3015
2992
  );
3016
2993
 
2994
+ router.post(
2995
+ "/mobile-app/save-config",
2996
+ isAdmin,
2997
+ error_catcher(async (req, res) => {
2998
+ try {
2999
+ const newCfg = { ...req.body };
3000
+ const excludedPlugins = (await Plugin.find())
3001
+ .filter(
3002
+ (plugin) =>
3003
+ ["base", "sbadmin2"].indexOf(plugin.name) < 0 &&
3004
+ newCfg.includedPlugins.indexOf(plugin.name) < 0
3005
+ )
3006
+ .map((plugin) => plugin.name);
3007
+ newCfg.excludedPlugins = excludedPlugins;
3008
+ await getState().setConfig("mobile_builder_settings", newCfg);
3009
+ res.json({ success: true });
3010
+ } catch (e) {
3011
+ getState().log(1, `Unable to save mobile builder config: ${e.message}`);
3012
+ res.json({ error: e.message });
3013
+ }
3014
+ })
3015
+ );
3016
+
3017
3017
  /**
3018
3018
  * Do Clear All
3019
3019
  * @function
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"),
@@ -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
+ );