@saltcorn/server 0.9.6-beta.15 → 0.9.6-beta.16
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 +4 -2
- package/package.json +9 -9
- package/public/saltcorn-common.js +3 -1
- package/routes/menu.js +57 -4
- package/routes/plugins.js +5 -1
- package/routes/search.js +10 -4
- package/tests/jsdom.test.js +159 -0
package/locales/en.json
CHANGED
|
@@ -1436,5 +1436,7 @@
|
|
|
1436
1436
|
"Older": "Older",
|
|
1437
1437
|
"Newest": "Newest",
|
|
1438
1438
|
"Delete all read": "Delete all read",
|
|
1439
|
-
"Trigger %s duplicated as %s": "Trigger %s duplicated as %s"
|
|
1440
|
-
|
|
1439
|
+
"Trigger %s duplicated as %s": "Trigger %s duplicated as %s",
|
|
1440
|
+
"Tooltip": "Tooltip",
|
|
1441
|
+
"Tooltip formula": "Tooltip formula"
|
|
1442
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.6-beta.
|
|
3
|
+
"version": "0.9.6-beta.16",
|
|
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.
|
|
11
|
-
"@saltcorn/builder": "0.9.6-beta.
|
|
12
|
-
"@saltcorn/data": "0.9.6-beta.
|
|
13
|
-
"@saltcorn/admin-models": "0.9.6-beta.
|
|
14
|
-
"@saltcorn/filemanager": "0.9.6-beta.
|
|
15
|
-
"@saltcorn/markup": "0.9.6-beta.
|
|
16
|
-
"@saltcorn/plugins-loader": "0.9.6-beta.
|
|
17
|
-
"@saltcorn/sbadmin2": "0.9.6-beta.
|
|
10
|
+
"@saltcorn/base-plugin": "0.9.6-beta.16",
|
|
11
|
+
"@saltcorn/builder": "0.9.6-beta.16",
|
|
12
|
+
"@saltcorn/data": "0.9.6-beta.16",
|
|
13
|
+
"@saltcorn/admin-models": "0.9.6-beta.16",
|
|
14
|
+
"@saltcorn/filemanager": "0.9.6-beta.16",
|
|
15
|
+
"@saltcorn/markup": "0.9.6-beta.16",
|
|
16
|
+
"@saltcorn/plugins-loader": "0.9.6-beta.16",
|
|
17
|
+
"@saltcorn/sbadmin2": "0.9.6-beta.16",
|
|
18
18
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
19
19
|
"@socket.io/sticky": "^1.0.1",
|
|
20
20
|
"adm-zip": "0.5.10",
|
|
@@ -1317,10 +1317,12 @@ async function common_done(res, viewnameOrElem, isWeb = true) {
|
|
|
1317
1317
|
}
|
|
1318
1318
|
};
|
|
1319
1319
|
if (res.notify) await handle(res.notify, notifyAlert);
|
|
1320
|
-
if (res.error)
|
|
1320
|
+
if (res.error) {
|
|
1321
|
+
if (window._sc_loglevel > 4) console.trace("error response", res.error);
|
|
1321
1322
|
await handle(res.error, (text) =>
|
|
1322
1323
|
notifyAlert({ type: "danger", text: text })
|
|
1323
1324
|
);
|
|
1325
|
+
}
|
|
1324
1326
|
if (res.notify_success)
|
|
1325
1327
|
await handle(res.notify_success, (text) =>
|
|
1326
1328
|
notifyAlert({ type: "success", text: text })
|
package/routes/menu.js
CHANGED
|
@@ -15,6 +15,7 @@ const { getState } = require("@saltcorn/data/db/state");
|
|
|
15
15
|
const User = require("@saltcorn/data/models/user");
|
|
16
16
|
const View = require("@saltcorn/data/models/view");
|
|
17
17
|
const Page = require("@saltcorn/data/models/page");
|
|
18
|
+
const PageGroup = require("@saltcorn/data/models/page_group");
|
|
18
19
|
const { save_menu_items } = require("@saltcorn/data/models/config");
|
|
19
20
|
const db = require("@saltcorn/data/db");
|
|
20
21
|
|
|
@@ -43,6 +44,10 @@ module.exports = router;
|
|
|
43
44
|
const menuForm = async (req) => {
|
|
44
45
|
const views = await View.find({}, { orderBy: "name", nocase: true });
|
|
45
46
|
const pages = await Page.find({}, { orderBy: "name", nocase: true });
|
|
47
|
+
const pageGroups = await PageGroup.find(
|
|
48
|
+
{},
|
|
49
|
+
{ orderBy: "name", nocase: true }
|
|
50
|
+
);
|
|
46
51
|
const roles = await User.get_roles();
|
|
47
52
|
const tables = await Table.find_with_external({});
|
|
48
53
|
const dynTableOptions = tables.map((t) => t.name);
|
|
@@ -101,6 +106,7 @@ const menuForm = async (req) => {
|
|
|
101
106
|
options: [
|
|
102
107
|
"View",
|
|
103
108
|
"Page",
|
|
109
|
+
"Page Group",
|
|
104
110
|
"Link",
|
|
105
111
|
"Header",
|
|
106
112
|
"Dynamic",
|
|
@@ -141,6 +147,14 @@ const menuForm = async (req) => {
|
|
|
141
147
|
attributes: { options: views.map((r) => r.select_option) },
|
|
142
148
|
showIf: { type: "View" },
|
|
143
149
|
},
|
|
150
|
+
{
|
|
151
|
+
name: "page_group",
|
|
152
|
+
label: req.__("Page group"),
|
|
153
|
+
input_type: "select",
|
|
154
|
+
class: "item-menu",
|
|
155
|
+
options: pageGroups.map((r) => r.name),
|
|
156
|
+
showIf: { type: "Page Group" },
|
|
157
|
+
},
|
|
144
158
|
{
|
|
145
159
|
name: "action_name",
|
|
146
160
|
label: req.__("Action"),
|
|
@@ -194,6 +208,14 @@ const menuForm = async (req) => {
|
|
|
194
208
|
required: true,
|
|
195
209
|
showIf: { type: "Dynamic" },
|
|
196
210
|
},
|
|
211
|
+
{
|
|
212
|
+
name: "dyn_tooltip_fml",
|
|
213
|
+
label: req.__("Tooltip formula"),
|
|
214
|
+
class: "item-menu",
|
|
215
|
+
type: "String",
|
|
216
|
+
required: false,
|
|
217
|
+
showIf: { type: "Dynamic" },
|
|
218
|
+
},
|
|
197
219
|
{
|
|
198
220
|
name: "dyn_url_fml",
|
|
199
221
|
label: req.__("URL formula"),
|
|
@@ -223,6 +245,7 @@ const menuForm = async (req) => {
|
|
|
223
245
|
type: [
|
|
224
246
|
"View",
|
|
225
247
|
"Page",
|
|
248
|
+
"Page Group",
|
|
226
249
|
"Link",
|
|
227
250
|
"Header",
|
|
228
251
|
"Dynamic",
|
|
@@ -238,13 +261,34 @@ const menuForm = async (req) => {
|
|
|
238
261
|
attributes: {
|
|
239
262
|
html: `<button type="button" id="myEditor_icon" class="btn btn-outline-secondary"></button>`,
|
|
240
263
|
},
|
|
241
|
-
showIf: {
|
|
264
|
+
showIf: {
|
|
265
|
+
type: ["View", "Page", "Page Group", "Link", "Header", "Action"],
|
|
266
|
+
},
|
|
242
267
|
},
|
|
243
268
|
{
|
|
244
269
|
name: "icon",
|
|
245
270
|
class: "item-menu",
|
|
246
271
|
input_type: "hidden",
|
|
247
272
|
},
|
|
273
|
+
{
|
|
274
|
+
name: "tooltip",
|
|
275
|
+
label: req.__("Tooltip"),
|
|
276
|
+
class: "item-menu",
|
|
277
|
+
input_type: "text",
|
|
278
|
+
required: false,
|
|
279
|
+
showIf: {
|
|
280
|
+
type: [
|
|
281
|
+
"View",
|
|
282
|
+
"Page",
|
|
283
|
+
"Page Group",
|
|
284
|
+
"Link",
|
|
285
|
+
"Header",
|
|
286
|
+
"Dynamic",
|
|
287
|
+
"Search",
|
|
288
|
+
"Action",
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
248
292
|
{
|
|
249
293
|
name: "min_role",
|
|
250
294
|
label: req.__("Minimum role"),
|
|
@@ -277,7 +321,7 @@ const menuForm = async (req) => {
|
|
|
277
321
|
type: "Bool",
|
|
278
322
|
required: false,
|
|
279
323
|
class: "item-menu",
|
|
280
|
-
showIf: { type: ["View", "Page", "Link"] },
|
|
324
|
+
showIf: { type: ["View", "Page", "Page Group", "Link"] },
|
|
281
325
|
},
|
|
282
326
|
{
|
|
283
327
|
name: "in_modal",
|
|
@@ -285,7 +329,7 @@ const menuForm = async (req) => {
|
|
|
285
329
|
type: "Bool",
|
|
286
330
|
required: false,
|
|
287
331
|
class: "item-menu",
|
|
288
|
-
showIf: { type: ["View", "Page", "Link"] },
|
|
332
|
+
showIf: { type: ["View", "Page", "Page Group", "Link"] },
|
|
289
333
|
},
|
|
290
334
|
{
|
|
291
335
|
name: "style",
|
|
@@ -295,7 +339,15 @@ const menuForm = async (req) => {
|
|
|
295
339
|
type: "String",
|
|
296
340
|
required: true,
|
|
297
341
|
showIf: {
|
|
298
|
-
type: [
|
|
342
|
+
type: [
|
|
343
|
+
"View",
|
|
344
|
+
"Page",
|
|
345
|
+
"Page Group",
|
|
346
|
+
"Link",
|
|
347
|
+
"Header",
|
|
348
|
+
"Dynamic",
|
|
349
|
+
"Action",
|
|
350
|
+
],
|
|
299
351
|
},
|
|
300
352
|
attributes: {
|
|
301
353
|
options: [
|
|
@@ -322,6 +374,7 @@ const menuForm = async (req) => {
|
|
|
322
374
|
type: [
|
|
323
375
|
"View",
|
|
324
376
|
"Page",
|
|
377
|
+
"Page Group",
|
|
325
378
|
"Link",
|
|
326
379
|
"Header",
|
|
327
380
|
"Dynamic",
|
package/routes/plugins.js
CHANGED
|
@@ -865,9 +865,13 @@ router.get(
|
|
|
865
865
|
if (!module) {
|
|
866
866
|
module = getState().plugins[getState().plugin_module_names[plugin.name]];
|
|
867
867
|
}
|
|
868
|
+
const userLayout =
|
|
869
|
+
user._attributes?.layout?.plugin === plugin.name
|
|
870
|
+
? user._attributes.layout.config || {}
|
|
871
|
+
: {};
|
|
868
872
|
const form = await module.user_config_form({
|
|
869
873
|
...(plugin.configuration || {}),
|
|
870
|
-
...
|
|
874
|
+
...userLayout,
|
|
871
875
|
});
|
|
872
876
|
form.action = `/plugins/user_configure/${encodeURIComponent(plugin.name)}`;
|
|
873
877
|
form.onChange = `applyViewConfig(this, '/plugins/user_saveconfig/${encodeURIComponent(
|
package/routes/search.js
CHANGED
|
@@ -202,7 +202,8 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
202
202
|
let tablesWithResults = [];
|
|
203
203
|
let tablesConfigured = 0;
|
|
204
204
|
for (const [tableName, viewName] of Object.entries(cfg)) {
|
|
205
|
-
if (!viewName || viewName === "")
|
|
205
|
+
if (!viewName || viewName === "" || viewName === "search_table_description")
|
|
206
|
+
continue;
|
|
206
207
|
tablesConfigured += 1;
|
|
207
208
|
if (table && tableName !== table) continue;
|
|
208
209
|
let sectionHeader = tableName;
|
|
@@ -232,7 +233,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
if (vresps.length > 0) {
|
|
235
|
-
tablesWithResults.push(tableName);
|
|
236
|
+
tablesWithResults.push({ tableName, label: sectionHeader });
|
|
236
237
|
resp.push({
|
|
237
238
|
type: "card",
|
|
238
239
|
title: span({ id: tableName }, sectionHeader),
|
|
@@ -273,8 +274,13 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
273
274
|
req.__("Show only matches in table:"),
|
|
274
275
|
" ",
|
|
275
276
|
tablesWithResults
|
|
276
|
-
.map((
|
|
277
|
-
a(
|
|
277
|
+
.map(({ tableName, label }) =>
|
|
278
|
+
a(
|
|
279
|
+
{
|
|
280
|
+
href: `javascript:set_state_field('table', '${tableName}')`,
|
|
281
|
+
},
|
|
282
|
+
label
|
|
283
|
+
)
|
|
278
284
|
)
|
|
279
285
|
.join(" | ")
|
|
280
286
|
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const request = require("supertest");
|
|
2
|
+
const getApp = require("../app");
|
|
3
|
+
const { resetToFixtures } = require("../auth/testhelp");
|
|
4
|
+
const db = require("@saltcorn/data/db");
|
|
5
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
6
|
+
const View = require("@saltcorn/data/models/view");
|
|
7
|
+
const Table = require("@saltcorn/data/models/table");
|
|
8
|
+
|
|
9
|
+
const { plugin_with_routes, sleep } = require("@saltcorn/data/tests/mocks");
|
|
10
|
+
const jsdom = require("jsdom");
|
|
11
|
+
const { JSDOM, ResourceLoader } = jsdom;
|
|
12
|
+
afterAll(db.close);
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
await resetToFixtures();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
jest.setTimeout(30000);
|
|
18
|
+
|
|
19
|
+
const load_url_dom = async (url) => {
|
|
20
|
+
const app = await getApp({ disableCsrf: true });
|
|
21
|
+
class CustomResourceLoader extends ResourceLoader {
|
|
22
|
+
async fetch(url, options) {
|
|
23
|
+
const url1 = url.replace("http://localhost", "");
|
|
24
|
+
//console.log("fetching", url, url1);
|
|
25
|
+
const res = await request(app).get(url1);
|
|
26
|
+
|
|
27
|
+
return Buffer.from(res.text);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const reqres = await request(app).get(url);
|
|
31
|
+
//console.log("rr1", reqres.text);
|
|
32
|
+
const virtualConsole = new jsdom.VirtualConsole();
|
|
33
|
+
virtualConsole.sendTo(console);
|
|
34
|
+
const dom = new JSDOM(reqres.text, {
|
|
35
|
+
url: "http://localhost" + url,
|
|
36
|
+
runScripts: "dangerously",
|
|
37
|
+
resources: new CustomResourceLoader(),
|
|
38
|
+
pretendToBeVisual: true,
|
|
39
|
+
virtualConsole,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
class FakeXHR {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.readyState = 0;
|
|
45
|
+
this.requestHeaders = [];
|
|
46
|
+
//return traceMethodCalls(this);
|
|
47
|
+
}
|
|
48
|
+
open(method, url) {
|
|
49
|
+
//console.log("open xhr", method, url);
|
|
50
|
+
this.method = method;
|
|
51
|
+
this.url = url;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addEventListener(ev, reqListener) {
|
|
55
|
+
if (ev === "load") this.reqListener = reqListener;
|
|
56
|
+
}
|
|
57
|
+
setRequestHeader(k, v) {
|
|
58
|
+
this.requestHeaders.push([k, v]);
|
|
59
|
+
}
|
|
60
|
+
overrideMimeType() {}
|
|
61
|
+
async send() {
|
|
62
|
+
//console.log("send1", this.url);
|
|
63
|
+
const url1 = this.url.replace("http://localhost", "");
|
|
64
|
+
//console.log("xhr fetching", url1);
|
|
65
|
+
let req = request(app).get(url1);
|
|
66
|
+
for (const [k, v] of this.requestHeaders) {
|
|
67
|
+
req = req.set(k, v);
|
|
68
|
+
}
|
|
69
|
+
const res = await req;
|
|
70
|
+
this.response = res.text;
|
|
71
|
+
this.responseText = res.text;
|
|
72
|
+
this.status = res.status;
|
|
73
|
+
this.statusText = "OK";
|
|
74
|
+
this.readyState = 4;
|
|
75
|
+
if (this.reqListener) this.reqListener(res.text);
|
|
76
|
+
if (this.onload) this.onload(res.text);
|
|
77
|
+
//console.log("agent res", res);
|
|
78
|
+
//console.log("xhr", this);
|
|
79
|
+
}
|
|
80
|
+
getAllResponseHeaders() {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
dom.window.XMLHttpRequest = FakeXHR;
|
|
85
|
+
await new Promise(function (resolve, reject) {
|
|
86
|
+
dom.window.addEventListener("DOMContentLoaded", (event) => {
|
|
87
|
+
resolve();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
return dom;
|
|
91
|
+
};
|
|
92
|
+
function traceMethodCalls(obj) {
|
|
93
|
+
let handler = {
|
|
94
|
+
get(target, propKey, receiver) {
|
|
95
|
+
console.log(propKey);
|
|
96
|
+
const origMethod = target[propKey];
|
|
97
|
+
return function (...args) {
|
|
98
|
+
let result = origMethod.apply(this, args);
|
|
99
|
+
console.log(
|
|
100
|
+
propKey + JSON.stringify(args) + " -> " + JSON.stringify(result)
|
|
101
|
+
);
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
return new Proxy(obj, handler);
|
|
107
|
+
}
|
|
108
|
+
describe("JSDOM test", () => {
|
|
109
|
+
it("should load authorlist", async () => {
|
|
110
|
+
const dom = await load_url_dom("/view/authorlist");
|
|
111
|
+
//console.log("dom", dom);
|
|
112
|
+
});
|
|
113
|
+
it("should user filter to change url", async () => {
|
|
114
|
+
await View.create({
|
|
115
|
+
viewtemplate: "Filter",
|
|
116
|
+
description: "",
|
|
117
|
+
min_role: 100,
|
|
118
|
+
name: `authorfilter1`,
|
|
119
|
+
table_id: Table.findOne("books")?.id,
|
|
120
|
+
default_render_page: "",
|
|
121
|
+
slug: {},
|
|
122
|
+
attributes: {},
|
|
123
|
+
configuration: {
|
|
124
|
+
layout: {
|
|
125
|
+
type: "field",
|
|
126
|
+
block: false,
|
|
127
|
+
fieldview: "edit",
|
|
128
|
+
textStyle: "",
|
|
129
|
+
field_name: "author",
|
|
130
|
+
configuration: {},
|
|
131
|
+
},
|
|
132
|
+
columns: [
|
|
133
|
+
{
|
|
134
|
+
type: "Field",
|
|
135
|
+
block: false,
|
|
136
|
+
fieldview: "edit",
|
|
137
|
+
textStyle: "",
|
|
138
|
+
field_name: "author",
|
|
139
|
+
configuration: {},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const dom = await load_url_dom("/view/authorfilter1");
|
|
145
|
+
expect(dom.window.location.href).toBe(
|
|
146
|
+
"http://localhost/view/authorfilter1"
|
|
147
|
+
);
|
|
148
|
+
//console.log(dom.serialize());
|
|
149
|
+
const input = dom.window.document.querySelector("input[name=author]");
|
|
150
|
+
input.value = "Leo";
|
|
151
|
+
input.dispatchEvent(new dom.window.Event("change"));
|
|
152
|
+
await sleep(2000);
|
|
153
|
+
expect(dom.window.location.href).toBe(
|
|
154
|
+
"http://localhost/view/authorfilter1?author=Leo"
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
//console.log("dom", dom);
|
|
158
|
+
});
|
|
159
|
+
});
|