@saltcorn/server 0.6.2-beta.5 → 0.6.3-beta.2

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/locales/en.json CHANGED
@@ -843,5 +843,9 @@
843
843
  "Optional. String type with options, each of which will become a menu section": "Optional. String type with options, each of which will become a menu section",
844
844
  "Role to generate API keys": "Role to generate API keys",
845
845
  "User should have this role or higher to generate API keys in their user settings": "User should have this role or higher to generate API keys in their user settings",
846
- "API token removed": "API token removed"
846
+ "API token removed": "API token removed",
847
+ "Row inclusion formula": "Row inclusion formula",
848
+ "Only include rows where this formula is true": "Only include rows where this formula is true",
849
+ "Slug": "Slug",
850
+ "Field that can be used for a prettier URL structure": "Field that can be used for a prettier URL structure"
847
851
  }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.6.2-beta.5",
3
+ "version": "0.6.3-beta.2",
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.6.2-beta.5",
10
- "@saltcorn/builder": "0.6.2-beta.5",
11
- "@saltcorn/data": "0.6.2-beta.5",
9
+ "@saltcorn/base-plugin": "0.6.3-beta.2",
10
+ "@saltcorn/builder": "0.6.3-beta.2",
11
+ "@saltcorn/data": "0.6.3-beta.2",
12
12
  "greenlock-express": "^4.0.3",
13
- "@saltcorn/markup": "0.6.2-beta.5",
14
- "@saltcorn/sbadmin2": "0.6.2-beta.5",
13
+ "@saltcorn/markup": "0.6.3-beta.2",
14
+ "@saltcorn/sbadmin2": "0.6.3-beta.2",
15
15
  "@socket.io/cluster-adapter": "^0.1.0",
16
16
  "@socket.io/sticky": "^1.0.1",
17
17
  "connect-flash": "^0.1.1",
@@ -75,6 +75,7 @@
75
75
  "@saltcorn/sqlite/(.*)": "@saltcorn/sqlite/dist/$1",
76
76
  "@saltcorn/db-common/(.*)": "@saltcorn/db-common/dist/$1",
77
77
  "@saltcorn/data/(.*)": "@saltcorn/data/dist/$1",
78
+ "@saltcorn/types/(.*)": "@saltcorn/types/dist/$1",
78
79
  "@saltcorn/markup$": "@saltcorn/markup/dist",
79
80
  "@saltcorn/markup/(.*)": "@saltcorn/markup/dist/$1"
80
81
  }
@@ -32,7 +32,9 @@ function apply_showif() {
32
32
  var e = $(element);
33
33
  var to_show = new Function("e", "return " + e.attr("data-show-if"));
34
34
  if (to_show(e))
35
- e.show().find("input, textarea, button, select").prop("disabled", false);
35
+ e.show()
36
+ .find("input, textarea, button, select")
37
+ .prop("disabled", e.attr("data-disabled") || false);
36
38
  else
37
39
  e.hide().find("input, textarea, button, select").prop("disabled", true);
38
40
  });
@@ -585,11 +587,11 @@ function test_formula(tablename, stored) {
585
587
  function align_dropdown(id) {
586
588
  setTimeout(() => {
587
589
  if ($("#dm" + id).hasClass("show")) {
588
- var inputWidth = $(".input-group.search-bar").outerWidth();
589
- $(".dropdown-menu.search-bar").css("width", inputWidth);
590
- var d0pos = $(".input-group.search-bar").offset();
590
+ var inputWidth = $("#search-input-group-" + id).outerWidth();
591
+ $("#dm" + id).css("width", inputWidth);
592
+ var d0pos = $("#search-input-group-" + id).offset();
591
593
  $("#dm" + id).offset({ left: d0pos.left });
592
- $(document).on("click", ".dropdown-menu.search-bar", function (e) {
594
+ $(document).on("click", "#dm" + id, function (e) {
593
595
  e.stopPropagation();
594
596
  });
595
597
  }
@@ -120,10 +120,6 @@ const listenForChanges = (projectDirs, pluginDirs) => {
120
120
  (event, file) => {
121
121
  console.log("'%s' changed \n re-starting now", file);
122
122
  closeWatchers();
123
- spawnSync("npm", ["run", "tsc"], {
124
- stdio: "inherit",
125
- cwd: projectRoot,
126
- });
127
123
  process.exit();
128
124
  }
129
125
  )
@@ -455,25 +455,26 @@ const get_config_response = async (role_id, res, req) => {
455
455
  }
456
456
  };
457
457
 
458
- /**
459
- * Function assigned to 'module.exports'.
460
- * @param {object} req
461
- * @param {object} res
462
- * @returns {Promise<void>}
463
- */
464
- module.exports = async (req, res) => {
465
- const isAuth = req.isAuthenticated();
466
- const role_id = req.user ? req.user.role_id : 10;
467
- const cfgResp = await get_config_response(role_id, res, req);
468
- if (cfgResp) return;
458
+ module.exports =
459
+ /**
460
+ * Function assigned to 'module.exports'.
461
+ * @param {object} req
462
+ * @param {object} res
463
+ * @returns {Promise<void>}
464
+ */
465
+ async (req, res) => {
466
+ const isAuth = req.isAuthenticated();
467
+ const role_id = req.user ? req.user.role_id : 10;
468
+ const cfgResp = await get_config_response(role_id, res, req);
469
+ if (cfgResp) return;
469
470
 
470
- if (!isAuth) {
471
- const hasUsers = await User.nonEmpty();
472
- if (!hasUsers) {
473
- res.redirect("/auth/create_first_user");
474
- return;
475
- } else res.redirect("/auth/login");
476
- } else {
477
- await no_views_logged_in(req, res);
478
- }
479
- };
471
+ if (!isAuth) {
472
+ const hasUsers = await User.nonEmpty();
473
+ if (!hasUsers) {
474
+ res.redirect("/auth/create_first_user");
475
+ return;
476
+ } else res.redirect("/auth/login");
477
+ } else {
478
+ await no_views_logged_in(req, res);
479
+ }
480
+ };
package/routes/index.js CHANGED
@@ -36,7 +36,7 @@
36
36
  * @property {module:routes/utils} utils
37
37
  * @property {module:routes/view} view
38
38
  * @property {module:routes/viewedit} viewedit
39
- *
39
+ *
40
40
  * @category server
41
41
  * @subcategory routes
42
42
  */
@@ -71,38 +71,39 @@ const useradmin = require("../auth/admin");
71
71
  const roleadmin = require("../auth/roleadmin");
72
72
  const scapi = require("./scapi");
73
73
 
74
- /**
75
- * Function assigned to 'module.exports'
76
- * @returns {void}
77
- */
78
- module.exports = (app) => {
79
- app.use("/table", table);
80
- app.use("/field", field);
81
- app.use("/files", files);
82
- app.use("/list", list);
83
- app.use("/edit", edit);
84
- app.use("/config", config);
85
- app.use("/plugins", plugins);
86
- app.use("/packs", packs);
87
- app.use("/menu", menu);
88
- app.use("/view", view);
89
- app.use("/crashlog", crashlog);
90
- app.use("/events", events);
91
- app.use("/page", page);
92
- app.use("/settings", settings);
93
- app.use("/pageedit", pageedit);
94
- app.use("/actions", actions);
95
- app.use("/eventlog", eventlog);
96
- app.use("/library", library);
97
- app.use("/site-structure", infoarch);
98
- app.use("/search", search);
99
- app.use("/admin", admin);
100
- app.use("/tenant", tenant);
101
- app.use("/api", api);
102
- app.use("/viewedit", viewedit);
103
- app.use("/delete", del);
104
- app.use("/auth", auth);
105
- app.use("/useradmin", useradmin);
106
- app.use("/roleadmin", roleadmin);
107
- app.use("/scapi", scapi);
108
- };
74
+ module.exports =
75
+ /**
76
+ * Function assigned to 'module.exports'
77
+ * @returns {void}
78
+ */
79
+ (app) => {
80
+ app.use("/table", table);
81
+ app.use("/field", field);
82
+ app.use("/files", files);
83
+ app.use("/list", list);
84
+ app.use("/edit", edit);
85
+ app.use("/config", config);
86
+ app.use("/plugins", plugins);
87
+ app.use("/packs", packs);
88
+ app.use("/menu", menu);
89
+ app.use("/view", view);
90
+ app.use("/crashlog", crashlog);
91
+ app.use("/events", events);
92
+ app.use("/page", page);
93
+ app.use("/settings", settings);
94
+ app.use("/pageedit", pageedit);
95
+ app.use("/actions", actions);
96
+ app.use("/eventlog", eventlog);
97
+ app.use("/library", library);
98
+ app.use("/site-structure", infoarch);
99
+ app.use("/search", search);
100
+ app.use("/admin", admin);
101
+ app.use("/tenant", tenant);
102
+ app.use("/api", api);
103
+ app.use("/viewedit", viewedit);
104
+ app.use("/delete", del);
105
+ app.use("/auth", auth);
106
+ app.use("/useradmin", useradmin);
107
+ app.use("/roleadmin", roleadmin);
108
+ app.use("/scapi", scapi);
109
+ };
package/routes/plugins.js CHANGED
@@ -511,7 +511,7 @@ const plugin_store_html = (items, req) => {
511
511
  },
512
512
  {
513
513
  besides: items.map(store_item_html(req)),
514
- widths: items.map((item) => 4), // todo warning that item uis unused
514
+ widths: items.map(() => 4),
515
515
  },
516
516
  ],
517
517
  };
package/routes/view.js CHANGED
@@ -37,10 +37,10 @@ module.exports = router;
37
37
  * @function
38
38
  */
39
39
  router.get(
40
- "/:viewname",
40
+ ["/:viewname", "/:viewname/*"],
41
41
  error_catcher(async (req, res) => {
42
42
  const { viewname } = req.params;
43
-
43
+ const query = { ...req.query };
44
44
  const view = await View.findOne({ name: viewname });
45
45
  const role = req.isAuthenticated() ? req.user.role_id : 10;
46
46
 
@@ -49,15 +49,17 @@ router.get(
49
49
  res.redirect("/");
50
50
  return;
51
51
  }
52
+
53
+ view.rewrite_query_from_slug(query, req.params);
52
54
  if (
53
55
  role > view.min_role &&
54
- !(await view.authorise_get({ query: req.query, req, ...view }))
56
+ !(await view.authorise_get({ query, req, ...view }))
55
57
  ) {
56
58
  req.flash("danger", req.__("Not authorized"));
57
59
  res.redirect("/");
58
60
  return;
59
61
  }
60
- const contents = await view.run_possibly_on_page(req.query, req, res);
62
+ const contents = await view.run_possibly_on_page(query, req, res);
61
63
  const title = scan_for_page_title(contents, view.name);
62
64
  res.sendWrap(
63
65
  title,
@@ -142,16 +144,21 @@ router.post(
142
144
  * @function
143
145
  */
144
146
  router.post(
145
- "/:viewname",
147
+ ["/:viewname", "/:viewname/*"],
146
148
  error_catcher(async (req, res) => {
147
149
  const { viewname } = req.params;
148
150
  const role = req.isAuthenticated() ? req.user.role_id : 10;
151
+ const query = { ...req.query };
149
152
 
150
153
  const view = await View.findOne({ name: viewname });
151
154
  if (!view) {
152
155
  req.flash("danger", req.__(`No such view: %s`, text(viewname)));
153
156
  res.redirect("/");
154
- } else if (
157
+ return;
158
+ }
159
+ view.rewrite_query_from_slug(query, req.params);
160
+
161
+ if (
155
162
  role > view.min_role &&
156
163
  !(await view.authorise_post({ body: req.body, req, ...view }))
157
164
  ) {
@@ -164,7 +171,7 @@ router.post(
164
171
  } does not supply a POST handler`
165
172
  );
166
173
  } else {
167
- await view.runPost(req.query, req.body, { res, req });
174
+ await view.runPost(query, req.body, { res, req });
168
175
  }
169
176
  })
170
177
  );
@@ -232,12 +232,13 @@ const mapObjectValues = (o, f) =>
232
232
  * @param {object} values
233
233
  * @returns {Form}
234
234
  */
235
- const viewForm = (req, tableOptions, roles, pages, values) => {
235
+ const viewForm = async (req, tableOptions, roles, pages, values) => {
236
236
  const isEdit =
237
237
  values && values.id && !getState().getConfig("development_mode", false);
238
238
  const hasTable = Object.entries(getState().viewtemplates)
239
239
  .filter(([k, v]) => !v.tableless)
240
240
  .map(([k, v]) => k);
241
+ const slugOptions = await Table.allSlugOptions();
241
242
  return new Form({
242
243
  action: "/viewedit/save",
243
244
  submitLabel: req.__("Configure") + " &raquo;",
@@ -302,6 +303,19 @@ const viewForm = (req, tableOptions, roles, pages, values) => {
302
303
  ...pages.map((p) => ({ value: p.name, label: p.name })),
303
304
  ],
304
305
  }),
306
+ new Field({
307
+ name: "slug",
308
+ label: req.__("Slug"),
309
+ sublabel: req.__("Field that can be used for a prettier URL structure"),
310
+ type: "String",
311
+ attributes: {
312
+ calcOptions: [
313
+ "table_name",
314
+ mapObjectValues(slugOptions, (lvs) => lvs.map((lv) => lv.label)),
315
+ ],
316
+ },
317
+ showIf: { viewtemplate: hasTable },
318
+ }),
305
319
  ...(isEdit
306
320
  ? [
307
321
  new Field({
@@ -342,10 +356,15 @@ router.get(
342
356
  (t) => t.id === viewrow.table_id || t.name === viewrow.exttable_name
343
357
  );
344
358
  viewrow.table_name = currentTable && currentTable.name;
359
+ if (viewrow.slug && currentTable) {
360
+ const slugOptions = await currentTable.slug_options();
361
+ const slug = slugOptions.find((so) => so.label === viewrow.slug.label);
362
+ if (slug) viewrow.slug = slug.label;
363
+ }
345
364
  const tableOptions = tables.map((t) => t.name);
346
365
  const roles = await User.get_roles();
347
366
  const pages = await Page.find();
348
- const form = viewForm(req, tableOptions, roles, pages, viewrow);
367
+ const form = await viewForm(req, tableOptions, roles, pages, viewrow);
349
368
  form.hidden("id");
350
369
  res.sendWrap(req.__(`Edit view`), {
351
370
  above: [
@@ -380,7 +399,7 @@ router.get(
380
399
  const tableOptions = tables.map((t) => t.name);
381
400
  const roles = await User.get_roles();
382
401
  const pages = await Page.find();
383
- const form = viewForm(req, tableOptions, roles, pages);
402
+ const form = await viewForm(req, tableOptions, roles, pages);
384
403
  if (req.query && req.query.table) {
385
404
  form.values.table_name = req.query.table;
386
405
  }
@@ -417,7 +436,7 @@ router.post(
417
436
  const tableOptions = tables.map((t) => t.name);
418
437
  const roles = await User.get_roles();
419
438
  const pages = await Page.find();
420
- const form = viewForm(req, tableOptions, roles, pages);
439
+ const form = await viewForm(req, tableOptions, roles, pages);
421
440
  const result = form.validate(req.body);
422
441
 
423
442
  const sendForm = (form) => {
@@ -458,11 +477,18 @@ router.post(
458
477
  const v = result.success;
459
478
  if (v.table_name) {
460
479
  const table = await Table.findOne({ name: v.table_name });
461
- if (table && table.id) v.table_id = table.id;
462
- else if (table && table.external) v.exttable_name = v.table_name;
480
+ if (table && table.id) {
481
+ v.table_id = table.id;
482
+ } else if (table && table.external) v.exttable_name = v.table_name;
463
483
  }
484
+ if (v.table_id) {
485
+ const table = await Table.findOne({ id: v.table_id });
486
+ const slugOptions = await table.slug_options();
487
+ const slug = slugOptions.find((so) => so.label === v.slug);
488
+ v.slug = slug || null;
489
+ }
490
+ const table = await Table.findOne({ name: v.table_name });
464
491
  delete v.table_name;
465
-
466
492
  if (req.body.id) {
467
493
  await View.update(v, +req.body.id);
468
494
  } else {
package/serve.js CHANGED
@@ -38,6 +38,7 @@ const {
38
38
  getRelevantPackages,
39
39
  getPluginDirectories,
40
40
  } = require("./restart_watcher");
41
+ const { spawnSync } = require("child_process");
41
42
 
42
43
  // helpful https://gist.github.com/jpoehls/2232358
43
44
  /**
@@ -162,6 +163,9 @@ module.exports =
162
163
  ...appargs
163
164
  } = {}) => {
164
165
  if (dev && cluster.isMaster) {
166
+ spawnSync("npm", ["run", "tsc"], {
167
+ stdio: "inherit",
168
+ });
165
169
  listenForChanges(getRelevantPackages(), await getPluginDirectories());
166
170
  }
167
171
  const useNCpus = process.env.SALTCORN_NWORKERS
@@ -137,3 +137,41 @@ describe("render view on page", () => {
137
137
  .expect(toNotInclude("Herman Melville"));
138
138
  });
139
139
  });
140
+
141
+ describe("render view with slug", () => {
142
+ it("should show with id slug in list", async () => {
143
+ const view = await View.findOne({ name: "authorshow" });
144
+ const table = await Table.findOne({ name: "books" });
145
+ const slugOpts = await table.slug_options();
146
+ const slugOpt = slugOpts.find((so) => so.label === "/:id");
147
+ expect(!!slugOpt).toBe(true);
148
+ View.update({ default_render_page: null, slug: slugOpt }, view.id);
149
+ const app = await getApp({ disableCsrf: true });
150
+ await request(app)
151
+ .get("/view/authorlist")
152
+ .expect(toInclude(`/view/authorshow/1`));
153
+ await request(app)
154
+ .get("/view/authorshow/1")
155
+ .expect(toInclude(`Herman Melville`));
156
+ });
157
+ it("should show with name slug in list", async () => {
158
+ const view = await View.findOne({ name: "authorshow" });
159
+ const table0 = await Table.findOne({ name: "books" });
160
+ const fields = await table0.getFields();
161
+ const field = fields.find((f) => f.name === "author");
162
+ await field.update({ is_unique: true });
163
+ const table = await Table.findOne({ name: "books" });
164
+
165
+ const slugOpts = await table.slug_options();
166
+ const slugOpt = slugOpts.find((so) => so.label === "/slugify-author");
167
+ expect(!!slugOpt).toBe(true);
168
+ View.update({ default_render_page: null, slug: slugOpt }, view.id);
169
+ const app = await getApp({ disableCsrf: true });
170
+ await request(app)
171
+ .get("/view/authorlist")
172
+ .expect(toInclude(`/view/authorshow/herman-melville`));
173
+ await request(app)
174
+ .get("/view/authorshow/herman-melville")
175
+ .expect(toInclude(`Herman Melville`));
176
+ });
177
+ });
package/wrapper.js CHANGED
@@ -239,6 +239,7 @@ module.exports = (version_tag) =>
239
239
  brand: get_brand(state),
240
240
  menu: get_menu(req),
241
241
  currentUrl,
242
+ originalUrl: req.originalUrl,
242
243
  alerts: getFlashes(req),
243
244
  body,
244
245
  headers: get_headers(req, version_tag),
@@ -283,6 +284,8 @@ module.exports = (version_tag) =>
283
284
  brand: get_brand(state),
284
285
  menu: get_menu(req),
285
286
  currentUrl,
287
+ originalUrl: req.originalUrl,
288
+
286
289
  alerts,
287
290
  body: html.length === 1 ? html[0] : html.join(""),
288
291
  headers: get_headers(req, version_tag, opts.description, pageHeaders),