@saltcorn/server 0.9.6-beta.14 → 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 +9 -1
- package/package.json +9 -9
- package/public/saltcorn-common.js +23 -3
- package/public/saltcorn.css +22 -0
- package/restart_watcher.js +1 -0
- package/routes/actions.js +21 -0
- package/routes/common_lists.js +42 -32
- package/routes/menu.js +69 -4
- package/routes/notifications.js +82 -8
- package/routes/plugins.js +5 -1
- package/routes/search.js +10 -4
- package/routes/tables.js +8 -8
- package/tests/jsdom.test.js +159 -0
package/locales/en.json
CHANGED
|
@@ -1427,8 +1427,16 @@
|
|
|
1427
1427
|
"xcodebuild": "xcodebuild",
|
|
1428
1428
|
"Provisioning Profile": "Provisioning Profile",
|
|
1429
1429
|
"Registry editor": "Registry editor",
|
|
1430
|
+
"Mobile HTML": "Mobile HTML",
|
|
1431
|
+
"HTML for the item in the bottom navigation bar. Currently, only supported by the metronic theme.": "HTML for the item in the bottom navigation bar. Currently, only supported by the metronic theme.",
|
|
1430
1432
|
"A short name that will be in the page URL": "A short name that will be in the page URL",
|
|
1431
1433
|
"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
1434
|
"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>"
|
|
1435
|
+
"Example: <code>`/view/TheOtherView?id=${id}`</code>": "Example: <code>`/view/TheOtherView?id=${id}`</code>",
|
|
1436
|
+
"Older": "Older",
|
|
1437
|
+
"Newest": "Newest",
|
|
1438
|
+
"Delete all read": "Delete all read",
|
|
1439
|
+
"Trigger %s duplicated as %s": "Trigger %s duplicated as %s",
|
|
1440
|
+
"Tooltip": "Tooltip",
|
|
1441
|
+
"Tooltip formula": "Tooltip formula"
|
|
1434
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",
|
|
@@ -8,6 +8,23 @@ jQuery.fn.swapWith = function (to) {
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
function monospace_block_click(e) {
|
|
12
|
+
let e1 = $(e).next("pre");
|
|
13
|
+
let mine = $(e).html();
|
|
14
|
+
$(e).html($(e1).html());
|
|
15
|
+
$(e1).html(mine);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function copy_monospace_block(e) {
|
|
19
|
+
let e1 = $(e).next("pre");
|
|
20
|
+
let e2 = $(e1).next("pre");
|
|
21
|
+
if (!e2.length) return navigator.clipboard.writeText($(el).text());
|
|
22
|
+
const e1t = e1.text();
|
|
23
|
+
const e2t = e2.text();
|
|
24
|
+
if (e1t.length > e2t.length) return navigator.clipboard.writeText(e1t);
|
|
25
|
+
else return navigator.clipboard.writeText(e2t);
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
function setScreenInfoCookie() {
|
|
12
29
|
document.cookie = `_sc_screen_info_=${JSON.stringify({
|
|
13
30
|
width: window.screen.width,
|
|
@@ -837,11 +854,12 @@ function initialize_page() {
|
|
|
837
854
|
const url = new URL(path);
|
|
838
855
|
path = `${url.pathname}${url.search}`;
|
|
839
856
|
}
|
|
840
|
-
if (path.startsWith("/view/")) {
|
|
857
|
+
if (path.startsWith("/view/") || path.startsWith("/page/")) {
|
|
841
858
|
const jThis = $(this);
|
|
842
859
|
const skip = jThis.attr("skip-mobile-adjust");
|
|
843
860
|
if (!skip) {
|
|
844
|
-
jThis.
|
|
861
|
+
jThis.removeAttr("href");
|
|
862
|
+
jThis.attr("onclick", `execLink('${path}')`);
|
|
845
863
|
if (jThis.find("i,img").length === 0 && !jThis.css("color")) {
|
|
846
864
|
jThis.css(
|
|
847
865
|
"color",
|
|
@@ -1299,10 +1317,12 @@ async function common_done(res, viewnameOrElem, isWeb = true) {
|
|
|
1299
1317
|
}
|
|
1300
1318
|
};
|
|
1301
1319
|
if (res.notify) await handle(res.notify, notifyAlert);
|
|
1302
|
-
if (res.error)
|
|
1320
|
+
if (res.error) {
|
|
1321
|
+
if (window._sc_loglevel > 4) console.trace("error response", res.error);
|
|
1303
1322
|
await handle(res.error, (text) =>
|
|
1304
1323
|
notifyAlert({ type: "danger", text: text })
|
|
1305
1324
|
);
|
|
1325
|
+
}
|
|
1306
1326
|
if (res.notify_success)
|
|
1307
1327
|
await handle(res.notify_success, (text) =>
|
|
1308
1328
|
notifyAlert({ type: "success", text: text })
|
package/public/saltcorn.css
CHANGED
|
@@ -514,3 +514,25 @@ ul.katetree {
|
|
|
514
514
|
ul.katetree details ul {
|
|
515
515
|
list-style-type: none;
|
|
516
516
|
}
|
|
517
|
+
|
|
518
|
+
pre.monospace-block {
|
|
519
|
+
font-family: monospace;
|
|
520
|
+
color: unset;
|
|
521
|
+
background: unset;
|
|
522
|
+
padding: unset;
|
|
523
|
+
border-radius: unset;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.d-none-prefer {
|
|
527
|
+
display: none;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
button.monospace-copy-btn:has(+ pre.monospace-block:hover) {
|
|
531
|
+
display: block !important ;
|
|
532
|
+
}
|
|
533
|
+
button.monospace-copy-btn:hover {
|
|
534
|
+
display: block !important ;
|
|
535
|
+
}
|
|
536
|
+
button.monospace-copy-btn {
|
|
537
|
+
position: absolute;
|
|
538
|
+
}
|
package/restart_watcher.js
CHANGED
package/routes/actions.js
CHANGED
|
@@ -887,3 +887,24 @@ router.get(
|
|
|
887
887
|
}
|
|
888
888
|
})
|
|
889
889
|
);
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* @name post/clone/:id
|
|
893
|
+
* @function
|
|
894
|
+
* @memberof module:routes/actions~actionsRouter
|
|
895
|
+
* @function
|
|
896
|
+
*/
|
|
897
|
+
router.post(
|
|
898
|
+
"/clone/:id",
|
|
899
|
+
isAdmin,
|
|
900
|
+
error_catcher(async (req, res) => {
|
|
901
|
+
const { id } = req.params;
|
|
902
|
+
const trig = await Trigger.findOne({ id });
|
|
903
|
+
const newtrig = await trig.clone();
|
|
904
|
+
req.flash(
|
|
905
|
+
"success",
|
|
906
|
+
req.__("Trigger %s duplicated as %s", trig.name, newtrig.name)
|
|
907
|
+
);
|
|
908
|
+
res.redirect(`/actions`);
|
|
909
|
+
})
|
|
910
|
+
);
|
package/routes/common_lists.js
CHANGED
|
@@ -304,6 +304,18 @@ const viewsList = async (
|
|
|
304
304
|
? `set_state_field('_sortby', 'name', this)`
|
|
305
305
|
: undefined,
|
|
306
306
|
},
|
|
307
|
+
{
|
|
308
|
+
label: "",
|
|
309
|
+
key: (r) =>
|
|
310
|
+
r.id && r.viewtemplateObj?.configuration_workflow
|
|
311
|
+
? link(
|
|
312
|
+
`/viewedit/config/${encodeURIComponent(
|
|
313
|
+
r.name
|
|
314
|
+
)}${on_done_redirect_str}`,
|
|
315
|
+
req.__("Configure")
|
|
316
|
+
)
|
|
317
|
+
: "",
|
|
318
|
+
},
|
|
307
319
|
...(tagId
|
|
308
320
|
? []
|
|
309
321
|
: [
|
|
@@ -340,18 +352,6 @@ const viewsList = async (
|
|
|
340
352
|
? editViewRoleForm(row, roles, req, on_done_redirect_str)
|
|
341
353
|
: "admin",
|
|
342
354
|
},
|
|
343
|
-
{
|
|
344
|
-
label: "",
|
|
345
|
-
key: (r) =>
|
|
346
|
-
r.id && r.viewtemplateObj?.configuration_workflow
|
|
347
|
-
? link(
|
|
348
|
-
`/viewedit/config/${encodeURIComponent(
|
|
349
|
-
r.name
|
|
350
|
-
)}${on_done_redirect_str}`,
|
|
351
|
-
req.__("Configure")
|
|
352
|
-
)
|
|
353
|
-
: "",
|
|
354
|
-
},
|
|
355
355
|
!tagId
|
|
356
356
|
? {
|
|
357
357
|
label: "",
|
|
@@ -428,13 +428,6 @@ const page_dropdown = (page, req) =>
|
|
|
428
428
|
},
|
|
429
429
|
'<i class="fas fa-running"></i> ' + req.__("Run")
|
|
430
430
|
),
|
|
431
|
-
a(
|
|
432
|
-
{
|
|
433
|
-
class: "dropdown-item",
|
|
434
|
-
href: `/pageedit/edit-properties/${encodeURIComponent(page.name)}`,
|
|
435
|
-
},
|
|
436
|
-
'<i class="fas fa-edit"></i> ' + req.__("Edit properties")
|
|
437
|
-
),
|
|
438
431
|
post_dropdown_item(
|
|
439
432
|
`/pageedit/add-to-menu/${page.id}`,
|
|
440
433
|
'<i class="fas fa-bars"></i> ' + req.__("Add to menu"),
|
|
@@ -507,6 +500,22 @@ const getPageList = async (
|
|
|
507
500
|
label: req.__("Name"),
|
|
508
501
|
key: (r) => link(`/page/${encodeURIComponent(r.name)}`, r.name),
|
|
509
502
|
},
|
|
503
|
+
{
|
|
504
|
+
label: "",
|
|
505
|
+
key: (r) =>
|
|
506
|
+
link(
|
|
507
|
+
`/pageedit/edit/${encodeURIComponent(r.name)}`,
|
|
508
|
+
req.__("Configure")
|
|
509
|
+
),
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
label: "",
|
|
513
|
+
key: (r) =>
|
|
514
|
+
link(
|
|
515
|
+
`/pageedit/edit-properties/${encodeURIComponent(r.name)}`,
|
|
516
|
+
req.__("Edit")
|
|
517
|
+
),
|
|
518
|
+
},
|
|
510
519
|
...(tagId
|
|
511
520
|
? []
|
|
512
521
|
: [
|
|
@@ -522,11 +531,7 @@ const getPageList = async (
|
|
|
522
531
|
label: req.__("Role to access"),
|
|
523
532
|
key: (row) => editPageRoleForm(row, roles, req),
|
|
524
533
|
},
|
|
525
|
-
|
|
526
|
-
label: req.__("Edit"),
|
|
527
|
-
key: (r) =>
|
|
528
|
-
link(`/pageedit/edit/${encodeURIComponent(r.name)}`, req.__("Edit")),
|
|
529
|
-
},
|
|
534
|
+
|
|
530
535
|
!tagId
|
|
531
536
|
? {
|
|
532
537
|
label: "",
|
|
@@ -600,6 +605,11 @@ const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
|
|
|
600
605
|
},
|
|
601
606
|
'<i class="fas fa-undo-alt"></i> ' + req.__("Restore")
|
|
602
607
|
),
|
|
608
|
+
post_dropdown_item(
|
|
609
|
+
`/actions/clone/${trigger.id}`,
|
|
610
|
+
'<i class="far fa-copy"></i> ' + req.__("Duplicate"),
|
|
611
|
+
req
|
|
612
|
+
),
|
|
603
613
|
div({ class: "dropdown-divider" }),
|
|
604
614
|
|
|
605
615
|
post_dropdown_item(
|
|
@@ -634,6 +644,14 @@ const getTriggerList = async (
|
|
|
634
644
|
return mkTable(
|
|
635
645
|
[
|
|
636
646
|
{ label: req.__("Name"), key: "name" },
|
|
647
|
+
{
|
|
648
|
+
label: req.__("Test run"),
|
|
649
|
+
key: (r) => link(`/actions/testrun/${r.id}`, req.__("Test run")),
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
label: req.__("Configure"),
|
|
653
|
+
key: (r) => link(`/actions/configure/${r.id}`, req.__("Configure")),
|
|
654
|
+
},
|
|
637
655
|
...(tagId
|
|
638
656
|
? []
|
|
639
657
|
: [
|
|
@@ -667,14 +685,6 @@ const getTriggerList = async (
|
|
|
667
685
|
? a({ href: `/table/${r.table_name}` }, r.table_name)
|
|
668
686
|
: r.channel,
|
|
669
687
|
},
|
|
670
|
-
{
|
|
671
|
-
label: req.__("Test run"),
|
|
672
|
-
key: (r) => link(`/actions/testrun/${r.id}`, req.__("Test run")),
|
|
673
|
-
},
|
|
674
|
-
{
|
|
675
|
-
label: req.__("Configure"),
|
|
676
|
-
key: (r) => link(`/actions/configure/${r.id}`, req.__("Configure")),
|
|
677
|
-
},
|
|
678
688
|
!tagId
|
|
679
689
|
? {
|
|
680
690
|
label: "",
|
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"),
|
|
@@ -258,6 +302,18 @@ const menuForm = async (req) => {
|
|
|
258
302
|
type: "Bool",
|
|
259
303
|
class: "item-menu",
|
|
260
304
|
required: false,
|
|
305
|
+
default: false,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "mobile_item_html",
|
|
309
|
+
label: req.__("Mobile HTML"),
|
|
310
|
+
sublabel: req.__(
|
|
311
|
+
"HTML for the item in the bottom navigation bar. Currently, only supported by the metronic theme."
|
|
312
|
+
),
|
|
313
|
+
type: "String",
|
|
314
|
+
class: "item-menu",
|
|
315
|
+
input_type: "textarea",
|
|
316
|
+
showIf: { disable_on_mobile: false, location: "Mobile Bottom" },
|
|
261
317
|
},
|
|
262
318
|
{
|
|
263
319
|
name: "target_blank",
|
|
@@ -265,7 +321,7 @@ const menuForm = async (req) => {
|
|
|
265
321
|
type: "Bool",
|
|
266
322
|
required: false,
|
|
267
323
|
class: "item-menu",
|
|
268
|
-
showIf: { type: ["View", "Page", "Link"] },
|
|
324
|
+
showIf: { type: ["View", "Page", "Page Group", "Link"] },
|
|
269
325
|
},
|
|
270
326
|
{
|
|
271
327
|
name: "in_modal",
|
|
@@ -273,7 +329,7 @@ const menuForm = async (req) => {
|
|
|
273
329
|
type: "Bool",
|
|
274
330
|
required: false,
|
|
275
331
|
class: "item-menu",
|
|
276
|
-
showIf: { type: ["View", "Page", "Link"] },
|
|
332
|
+
showIf: { type: ["View", "Page", "Page Group", "Link"] },
|
|
277
333
|
},
|
|
278
334
|
{
|
|
279
335
|
name: "style",
|
|
@@ -283,7 +339,15 @@ const menuForm = async (req) => {
|
|
|
283
339
|
type: "String",
|
|
284
340
|
required: true,
|
|
285
341
|
showIf: {
|
|
286
|
-
type: [
|
|
342
|
+
type: [
|
|
343
|
+
"View",
|
|
344
|
+
"Page",
|
|
345
|
+
"Page Group",
|
|
346
|
+
"Link",
|
|
347
|
+
"Header",
|
|
348
|
+
"Dynamic",
|
|
349
|
+
"Action",
|
|
350
|
+
],
|
|
287
351
|
},
|
|
288
352
|
attributes: {
|
|
289
353
|
options: [
|
|
@@ -310,6 +374,7 @@ const menuForm = async (req) => {
|
|
|
310
374
|
type: [
|
|
311
375
|
"View",
|
|
312
376
|
"Page",
|
|
377
|
+
"Page Group",
|
|
313
378
|
"Link",
|
|
314
379
|
"Header",
|
|
315
380
|
"Dynamic",
|
package/routes/notifications.js
CHANGED
|
@@ -13,7 +13,7 @@ const { getState } = require("@saltcorn/data/db/state");
|
|
|
13
13
|
const Form = require("@saltcorn/data/models/form");
|
|
14
14
|
const File = require("@saltcorn/data/models/file");
|
|
15
15
|
const User = require("@saltcorn/data/models/user");
|
|
16
|
-
const { renderForm } = require("@saltcorn/markup");
|
|
16
|
+
const { renderForm, post_btn } = require("@saltcorn/markup");
|
|
17
17
|
|
|
18
18
|
const router = new Router();
|
|
19
19
|
module.exports = router;
|
|
@@ -31,22 +31,50 @@ router.get(
|
|
|
31
31
|
"/",
|
|
32
32
|
loggedIn,
|
|
33
33
|
error_catcher(async (req, res) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const { after } = req.query;
|
|
35
|
+
const where = { user_id: req.user.id };
|
|
36
|
+
if (after) where.id = { lt: after };
|
|
37
|
+
const nots = await Notification.find(where, {
|
|
38
|
+
orderBy: "id",
|
|
39
|
+
orderDesc: true,
|
|
40
|
+
limit: 20,
|
|
41
|
+
});
|
|
38
42
|
await Notification.mark_as_read({
|
|
39
43
|
id: { in: nots.filter((n) => !n.read).map((n) => n.id) },
|
|
40
44
|
});
|
|
45
|
+
const form = notificationSettingsForm();
|
|
46
|
+
const user = await User.findOne({ id: req.user?.id });
|
|
47
|
+
form.values = { notify_email: user?._attributes?.notify_email };
|
|
41
48
|
const notifyCards = nots.length
|
|
42
49
|
? nots.map((not) => ({
|
|
43
50
|
type: "card",
|
|
44
51
|
class: [!not.read && "unread-notify"],
|
|
52
|
+
id: `notify-${not.id}`,
|
|
45
53
|
contents: [
|
|
46
54
|
div(
|
|
47
55
|
{ class: "d-flex" },
|
|
48
56
|
span({ class: "fw-bold" }, not.title),
|
|
49
|
-
span(
|
|
57
|
+
span(
|
|
58
|
+
{
|
|
59
|
+
class: "ms-2 text-muted",
|
|
60
|
+
title: not.created.toLocaleString(req.getLocale()),
|
|
61
|
+
},
|
|
62
|
+
moment(not.created).fromNow()
|
|
63
|
+
),
|
|
64
|
+
div(
|
|
65
|
+
{ class: "ms-auto" },
|
|
66
|
+
post_btn(
|
|
67
|
+
`/notifications/delete/${not.id}`,
|
|
68
|
+
"",
|
|
69
|
+
req.csrfToken(),
|
|
70
|
+
{
|
|
71
|
+
icon: "fas fa-times-circle",
|
|
72
|
+
klass: "btn-link text-muted text-decoration-none p-0",
|
|
73
|
+
ajax: true,
|
|
74
|
+
onClick: `$('#notify-${not.id}').remove()`,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
)
|
|
50
78
|
),
|
|
51
79
|
not.body && p(not.body),
|
|
52
80
|
not.link && a({ href: not.link }, "Link"),
|
|
@@ -58,6 +86,35 @@ router.get(
|
|
|
58
86
|
contents: [h5(req.__("No notifications"))],
|
|
59
87
|
},
|
|
60
88
|
];
|
|
89
|
+
const pageLinks = div(
|
|
90
|
+
{ class: "d-flex mt-3 mb-3" },
|
|
91
|
+
nots.length == 20
|
|
92
|
+
? div(
|
|
93
|
+
after &&
|
|
94
|
+
a(
|
|
95
|
+
{ href: `/notifications`, class: "me-2" },
|
|
96
|
+
"← " + req.__("Newest")
|
|
97
|
+
),
|
|
98
|
+
a(
|
|
99
|
+
{ href: `/notifications?after=${nots[19].id}` },
|
|
100
|
+
req.__("Older") + " →"
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
: div(),
|
|
104
|
+
nots.length > 0 &&
|
|
105
|
+
div(
|
|
106
|
+
{ class: "ms-auto" },
|
|
107
|
+
post_btn(
|
|
108
|
+
`/notifications/delete/read`,
|
|
109
|
+
req.__("Delete all read"),
|
|
110
|
+
req.csrfToken(),
|
|
111
|
+
{
|
|
112
|
+
icon: "fas fa-trash",
|
|
113
|
+
klass: "btn-sm btn-danger",
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
);
|
|
61
118
|
res.sendWrap(req.__("Notifications"), {
|
|
62
119
|
above: [
|
|
63
120
|
{
|
|
@@ -72,10 +129,10 @@ router.get(
|
|
|
72
129
|
type: "card",
|
|
73
130
|
contents: [
|
|
74
131
|
req.__("Receive notifications by:"),
|
|
75
|
-
renderForm(
|
|
132
|
+
renderForm(form, req.csrfToken()),
|
|
76
133
|
],
|
|
77
134
|
},
|
|
78
|
-
{ above: notifyCards },
|
|
135
|
+
{ above: [...notifyCards, pageLinks] },
|
|
79
136
|
],
|
|
80
137
|
},
|
|
81
138
|
],
|
|
@@ -109,6 +166,23 @@ router.post(
|
|
|
109
166
|
})
|
|
110
167
|
);
|
|
111
168
|
|
|
169
|
+
router.post(
|
|
170
|
+
"/delete/:idlike",
|
|
171
|
+
loggedIn,
|
|
172
|
+
error_catcher(async (req, res) => {
|
|
173
|
+
const { idlike } = req.params;
|
|
174
|
+
if (idlike == "read") {
|
|
175
|
+
await Notification.deleteRead(req.user.id);
|
|
176
|
+
} else {
|
|
177
|
+
const id = +idlike;
|
|
178
|
+
const notif = await Notification.findOne({ id });
|
|
179
|
+
if (notif?.user_id == req.user?.id) await notif.delete();
|
|
180
|
+
}
|
|
181
|
+
if (req.xhr) res.json({ success: "ok" });
|
|
182
|
+
else res.redirect("/notifications");
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
|
|
112
186
|
router.get(
|
|
113
187
|
"/manifest.json",
|
|
114
188
|
error_catcher(async (req, res) => {
|
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
|
)
|
package/routes/tables.js
CHANGED
|
@@ -754,6 +754,14 @@ router.get(
|
|
|
754
754
|
r.typename +
|
|
755
755
|
span({ class: "badge bg-danger ms-1" }, "Unknown type"),
|
|
756
756
|
},
|
|
757
|
+
...(table.external
|
|
758
|
+
? []
|
|
759
|
+
: [
|
|
760
|
+
{
|
|
761
|
+
label: req.__("Edit"),
|
|
762
|
+
key: (r) => link(`/field/${r.id}`, req.__("Edit")),
|
|
763
|
+
},
|
|
764
|
+
]),
|
|
757
765
|
{
|
|
758
766
|
label: "",
|
|
759
767
|
key: (r) => typeBadges(r, req),
|
|
@@ -763,14 +771,6 @@ router.get(
|
|
|
763
771
|
key: (r) => attribBadges(r),
|
|
764
772
|
},
|
|
765
773
|
{ label: req.__("Variable name"), key: (t) => code(t.name) },
|
|
766
|
-
...(table.external
|
|
767
|
-
? []
|
|
768
|
-
: [
|
|
769
|
-
{
|
|
770
|
-
label: req.__("Edit"),
|
|
771
|
-
key: (r) => link(`/field/${r.id}`, req.__("Edit")),
|
|
772
|
-
},
|
|
773
|
-
]),
|
|
774
774
|
...(table.external || db.isSQLite
|
|
775
775
|
? []
|
|
776
776
|
: [
|
|
@@ -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
|
+
});
|