@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 +5 -1
- package/package.json +7 -6
- package/public/saltcorn.js +7 -5
- package/restart_watcher.js +0 -4
- package/routes/homepage.js +22 -21
- package/routes/index.js +37 -36
- package/routes/plugins.js +1 -1
- package/routes/view.js +14 -7
- package/routes/viewedit.js +33 -7
- package/serve.js +4 -0
- package/tests/view.test.js +38 -0
- package/wrapper.js +3 -0
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.
|
|
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.
|
|
10
|
-
"@saltcorn/builder": "0.6.
|
|
11
|
-
"@saltcorn/data": "0.6.
|
|
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.
|
|
14
|
-
"@saltcorn/sbadmin2": "0.6.
|
|
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
|
}
|
package/public/saltcorn.js
CHANGED
|
@@ -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()
|
|
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 = $("
|
|
589
|
-
$("
|
|
590
|
-
var d0pos = $("
|
|
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", "
|
|
594
|
+
$(document).on("click", "#dm" + id, function (e) {
|
|
593
595
|
e.stopPropagation();
|
|
594
596
|
});
|
|
595
597
|
}
|
package/restart_watcher.js
CHANGED
|
@@ -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
|
)
|
package/routes/homepage.js
CHANGED
|
@@ -455,25 +455,26 @@ const get_config_response = async (role_id, res, req) => {
|
|
|
455
455
|
}
|
|
456
456
|
};
|
|
457
457
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
app
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
174
|
+
await view.runPost(query, req.body, { res, req });
|
|
168
175
|
}
|
|
169
176
|
})
|
|
170
177
|
);
|
package/routes/viewedit.js
CHANGED
|
@@ -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") + " »",
|
|
@@ -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)
|
|
462
|
-
|
|
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
|
package/tests/view.test.js
CHANGED
|
@@ -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),
|