@saltcorn/server 1.1.1-beta.8 → 1.1.1-rc.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/CHANGELOG.md +37 -1
- package/auth/admin.js +1 -0
- package/locales/en.json +9 -1
- package/markup/admin.js +15 -10
- package/package.json +9 -9
- package/public/saltcorn-common.js +85 -1
- package/public/saltcorn.css +39 -0
- package/public/saltcorn.js +17 -4
- package/routes/actions.js +17 -1
- package/routes/admin.js +29 -25
- package/routes/api.js +68 -5
- package/routes/common_lists.js +67 -16
- package/routes/fields.js +113 -2
- package/routes/list.js +9 -8
- package/routes/menu.js +3 -3
- package/routes/search.js +91 -11
- package/routes/tables.js +18 -4
- package/routes/tag_entries.js +43 -1
- package/routes/tags.js +1 -0
- package/routes/utils.js +15 -0
- package/serve.js +17 -13
- package/tests/api.test.js +13 -0
- package/tests/clientjs.test.js +20 -0
- package/tests/files.test.js +1 -1
- package/wrapper.js +31 -1
package/routes/common_lists.js
CHANGED
|
@@ -13,7 +13,17 @@ const {
|
|
|
13
13
|
} = require("@saltcorn/markup");
|
|
14
14
|
const { get_base_url } = require("./utils.js");
|
|
15
15
|
const { getState } = require("@saltcorn/data/db/state");
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
h4,
|
|
18
|
+
p,
|
|
19
|
+
div,
|
|
20
|
+
a,
|
|
21
|
+
i,
|
|
22
|
+
text,
|
|
23
|
+
span,
|
|
24
|
+
nbsp,
|
|
25
|
+
button,
|
|
26
|
+
} = require("@saltcorn/markup/tags");
|
|
17
27
|
|
|
18
28
|
/**
|
|
19
29
|
* @param {string} col
|
|
@@ -63,9 +73,11 @@ const tablesList = async (
|
|
|
63
73
|
getState().getConfig("min_role_edit_tables", 1) >= req.user.role_id;
|
|
64
74
|
const tagBadges = (table) => {
|
|
65
75
|
const myTags = tag_entries.filter((te) => te.table_id === table.id);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.join(nbsp)
|
|
76
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
77
|
+
return (
|
|
78
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "tables")).join(nbsp) +
|
|
79
|
+
mkAddBtn(tags, "tables", table.id, req, myTagIds)
|
|
80
|
+
);
|
|
69
81
|
};
|
|
70
82
|
|
|
71
83
|
return (
|
|
@@ -278,6 +290,38 @@ const tagsDropdown = (tags, altHeader) =>
|
|
|
278
290
|
)
|
|
279
291
|
);
|
|
280
292
|
|
|
293
|
+
const mkAddBtn = (tags, entityType, id, req, myTagIds) =>
|
|
294
|
+
div(
|
|
295
|
+
{ class: "dropdown d-inline ms-1" },
|
|
296
|
+
span(
|
|
297
|
+
{
|
|
298
|
+
class: "badge bg-secondary add-tag",
|
|
299
|
+
"data-bs-toggle": "dropdown",
|
|
300
|
+
"aria-haspopup": "true",
|
|
301
|
+
"aria-expanded": "false",
|
|
302
|
+
"data-boundary": "viewport",
|
|
303
|
+
},
|
|
304
|
+
i({ class: "fas fa-plus fa-sm" })
|
|
305
|
+
),
|
|
306
|
+
div(
|
|
307
|
+
{
|
|
308
|
+
class: "dropdown-menu dropdown-menu-end",
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
tags
|
|
312
|
+
.filter((t) => !myTagIds.has(t.id))
|
|
313
|
+
.map((t) =>
|
|
314
|
+
post_dropdown_item(
|
|
315
|
+
`/tag-entries/add-tag-entity/${encodeURIComponent(
|
|
316
|
+
t.name
|
|
317
|
+
)}/${entityType}/${id}`,
|
|
318
|
+
t.name,
|
|
319
|
+
req
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
|
|
281
325
|
const viewsList = async (
|
|
282
326
|
views,
|
|
283
327
|
req,
|
|
@@ -300,9 +344,12 @@ const viewsList = async (
|
|
|
300
344
|
|
|
301
345
|
const tagBadges = (view) => {
|
|
302
346
|
const myTags = tag_entries.filter((te) => te.view_id === view.id);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
347
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
348
|
+
const addBtn = mkAddBtn(tags, "views", view.id, req, myTagIds);
|
|
349
|
+
return (
|
|
350
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "views")).join(nbsp) +
|
|
351
|
+
addBtn
|
|
352
|
+
);
|
|
306
353
|
};
|
|
307
354
|
|
|
308
355
|
return (
|
|
@@ -504,9 +551,11 @@ const getPageList = async (
|
|
|
504
551
|
|
|
505
552
|
const tagBadges = (page) => {
|
|
506
553
|
const myTags = tag_entries.filter((te) => te.page_id === page.id);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.join(nbsp)
|
|
554
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
555
|
+
return (
|
|
556
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "pages")).join(nbsp) +
|
|
557
|
+
mkAddBtn(tags, "pages", page.id, req, myTagIds)
|
|
558
|
+
);
|
|
510
559
|
};
|
|
511
560
|
return mkTable(
|
|
512
561
|
[
|
|
@@ -602,7 +651,7 @@ const getPageGroupList = (rows, roles, req) => {
|
|
|
602
651
|
);
|
|
603
652
|
};
|
|
604
653
|
|
|
605
|
-
const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
|
|
654
|
+
const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
|
|
606
655
|
settingsDropdown(`dropdownMenuButton${trigger.id}`, [
|
|
607
656
|
a(
|
|
608
657
|
{
|
|
@@ -643,8 +692,8 @@ const getTriggerList = async (
|
|
|
643
692
|
const base_url = get_base_url(req);
|
|
644
693
|
const tags = await Tag.find();
|
|
645
694
|
const on_done_redirect_str = on_done_redirect
|
|
646
|
-
|
|
647
|
-
|
|
695
|
+
? `?on_done_redirect=${on_done_redirect}`
|
|
696
|
+
: "";
|
|
648
697
|
const tag_entries = await TagEntry.find({
|
|
649
698
|
not: { trigger_id: null },
|
|
650
699
|
});
|
|
@@ -657,9 +706,11 @@ const getTriggerList = async (
|
|
|
657
706
|
|
|
658
707
|
const tagBadges = (trigger) => {
|
|
659
708
|
const myTags = tag_entries.filter((te) => te.trigger_id === trigger.id);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
.join(nbsp)
|
|
709
|
+
const myTagIds = new Set(myTags.map((t) => t.tag_id));
|
|
710
|
+
return (
|
|
711
|
+
myTags.map((te) => tagBadge(tagsById[te.tag_id], "triggers")).join(nbsp) +
|
|
712
|
+
mkAddBtn(tags, "triggers", trigger.id, req, myTagIds)
|
|
713
|
+
);
|
|
663
714
|
};
|
|
664
715
|
return mkTable(
|
|
665
716
|
[
|
package/routes/fields.js
CHANGED
|
@@ -37,8 +37,8 @@ const {
|
|
|
37
37
|
} = require("@saltcorn/data/plugin-helper");
|
|
38
38
|
const { wizardCardTitle } = require("../markup/forms.js");
|
|
39
39
|
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
40
|
-
const { applyAsync } = require("@saltcorn/data/utils");
|
|
41
|
-
const { text } = require("@saltcorn/markup/tags");
|
|
40
|
+
const { applyAsync, isWeb } = require("@saltcorn/data/utils");
|
|
41
|
+
const { text, div } = require("@saltcorn/markup/tags");
|
|
42
42
|
const { mkFormContentNoLayout } = require("@saltcorn/markup/form");
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -1301,6 +1301,19 @@ router.post(
|
|
|
1301
1301
|
// - disabled inputs do not dispactch click events
|
|
1302
1302
|
const firefox = true;
|
|
1303
1303
|
const fv = fieldviews[fieldview];
|
|
1304
|
+
field.fieldview === fieldview;
|
|
1305
|
+
field.fieldviewObj = fv;
|
|
1306
|
+
field.attributes = { ...configuration, ...field.attributes };
|
|
1307
|
+
if (field.type === "Key")
|
|
1308
|
+
await field.fill_fkey_options(
|
|
1309
|
+
false,
|
|
1310
|
+
{},
|
|
1311
|
+
{},
|
|
1312
|
+
undefined,
|
|
1313
|
+
undefined,
|
|
1314
|
+
undefined,
|
|
1315
|
+
req.user
|
|
1316
|
+
);
|
|
1304
1317
|
if (!fv && field.type === "Key" && fieldview === "select")
|
|
1305
1318
|
res.send(
|
|
1306
1319
|
`<input ${
|
|
@@ -1422,3 +1435,101 @@ router.post(
|
|
|
1422
1435
|
res.send(mkFormContentNoLayout(form));
|
|
1423
1436
|
})
|
|
1424
1437
|
);
|
|
1438
|
+
|
|
1439
|
+
router.post(
|
|
1440
|
+
"/edit-get-fieldview",
|
|
1441
|
+
error_catcher(async (req, res) => {
|
|
1442
|
+
const { field_name, table_name, pk, fieldview, configuration } = req.body;
|
|
1443
|
+
const table = Table.findOne({ name: table_name });
|
|
1444
|
+
const row = await table.getRow(
|
|
1445
|
+
{ [table.pk_name]: pk },
|
|
1446
|
+
{ forUser: req.user, forPublic: !req.user }
|
|
1447
|
+
);
|
|
1448
|
+
const field = table.getField(field_name);
|
|
1449
|
+
let fv;
|
|
1450
|
+
if (field.is_fkey) {
|
|
1451
|
+
await field.fill_fkey_options(
|
|
1452
|
+
false,
|
|
1453
|
+
undefined,
|
|
1454
|
+
undefined,
|
|
1455
|
+
undefined,
|
|
1456
|
+
undefined,
|
|
1457
|
+
row[field_name],
|
|
1458
|
+
req.user
|
|
1459
|
+
);
|
|
1460
|
+
fv = getState().keyFieldviews.select;
|
|
1461
|
+
} else if (fieldview === "subfield" && field.type?.name === "JSON") {
|
|
1462
|
+
fv = field.type.fieldviews.edit_subfield;
|
|
1463
|
+
} else {
|
|
1464
|
+
//TODO: json subfield is special
|
|
1465
|
+
const fieldviews = field.type.fieldviews;
|
|
1466
|
+
fv = Object.values(fieldviews).find((v) => v.isEdit);
|
|
1467
|
+
}
|
|
1468
|
+
res.send(
|
|
1469
|
+
fv.run(
|
|
1470
|
+
field_name,
|
|
1471
|
+
row[field_name],
|
|
1472
|
+
{
|
|
1473
|
+
...field.attributes,
|
|
1474
|
+
...configuration,
|
|
1475
|
+
},
|
|
1476
|
+
"",
|
|
1477
|
+
false,
|
|
1478
|
+
field
|
|
1479
|
+
)
|
|
1480
|
+
);
|
|
1481
|
+
})
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
router.post(
|
|
1485
|
+
"/save-click-edit",
|
|
1486
|
+
error_catcher(async (req, res) => {
|
|
1487
|
+
const fielddata = JSON.parse(decodeURIComponent(req.body._fielddata));
|
|
1488
|
+
const { field_name, table_name, pk, fieldview, configuration, join_field } =
|
|
1489
|
+
fielddata;
|
|
1490
|
+
const table = Table.findOne({ name: table_name });
|
|
1491
|
+
const field = table.getField(field_name);
|
|
1492
|
+
let val = field.type?.read
|
|
1493
|
+
? field.type?.read(req.body[field_name])
|
|
1494
|
+
: req.body[field_name];
|
|
1495
|
+
await table.updateRow({ [field_name]: val }, pk, req.user);
|
|
1496
|
+
let fv;
|
|
1497
|
+
if (field.is_fkey) {
|
|
1498
|
+
if (join_field) {
|
|
1499
|
+
const refTable = Table.findOne({ name: field.reftable_name });
|
|
1500
|
+
const refRow = await refTable.getRow({ [refTable.pk_name]: val });
|
|
1501
|
+
val = refRow[join_field];
|
|
1502
|
+
const targetField = refTable.getField(join_field);
|
|
1503
|
+
const fieldviews = targetField.type.fieldviews;
|
|
1504
|
+
|
|
1505
|
+
fv = fieldviews[fieldview];
|
|
1506
|
+
} else fv = { run: (v) => `${v}` };
|
|
1507
|
+
} else {
|
|
1508
|
+
const fieldviews = field.type.fieldviews;
|
|
1509
|
+
|
|
1510
|
+
fv = fieldviews[fieldview];
|
|
1511
|
+
|
|
1512
|
+
if (!fv) {
|
|
1513
|
+
const fv1 = Object.values(fieldviews).find(
|
|
1514
|
+
(v) => !v.isEdit && !v.isFilter
|
|
1515
|
+
);
|
|
1516
|
+
fv = fv1;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
res.send(
|
|
1521
|
+
div(
|
|
1522
|
+
{
|
|
1523
|
+
"data-inline-edit-fielddata": req.body._fielddata,
|
|
1524
|
+
"data-inline-edit-ajax": "true",
|
|
1525
|
+
"data-inline-edit-dest-url": `/api/${table.name}/${pk}`,
|
|
1526
|
+
class: !isWeb(req) ? "mobile-data-inline-edit" : "",
|
|
1527
|
+
},
|
|
1528
|
+
fv.run(val, req, {
|
|
1529
|
+
...field.attributes,
|
|
1530
|
+
...configuration,
|
|
1531
|
+
})
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
})
|
|
1535
|
+
);
|
package/routes/list.js
CHANGED
|
@@ -402,25 +402,26 @@ router.get(
|
|
|
402
402
|
})
|
|
403
403
|
})
|
|
404
404
|
window.tabulator_table = new Tabulator("#jsGrid", {
|
|
405
|
-
ajaxURL:"/api/${encodeURIComponent(
|
|
406
|
-
|
|
405
|
+
ajaxURL:"/api/${encodeURIComponent(
|
|
406
|
+
table.name
|
|
407
|
+
)}?tabulator_pagination_format=true${
|
|
408
|
+
table.versioned ? "&versioncount=on" : ""
|
|
407
409
|
}",
|
|
408
410
|
layout:"fitColumns",
|
|
409
411
|
columns,
|
|
410
412
|
height:"100%",
|
|
411
413
|
pagination:true,
|
|
412
|
-
|
|
414
|
+
paginationMode:"remote",
|
|
415
|
+
paginationSize:10,
|
|
413
416
|
clipboard:true,
|
|
414
417
|
persistence:true,
|
|
415
418
|
persistenceID:"table_tab_${table.name}",
|
|
416
419
|
movableColumns: true,
|
|
420
|
+
ajaxContentType:"json",
|
|
421
|
+
sortMode:"remote",
|
|
417
422
|
initialSort:[
|
|
418
423
|
{column:"id", dir:"asc"},
|
|
419
|
-
],
|
|
420
|
-
ajaxResponse:function(url, params, response){
|
|
421
|
-
|
|
422
|
-
return response.success; //return the tableData property of a response json object
|
|
423
|
-
},
|
|
424
|
+
],
|
|
424
425
|
});
|
|
425
426
|
window.tabulator_table.on("cellEdited", function(cell){
|
|
426
427
|
const row = cell.getRow().getData()
|
package/routes/menu.js
CHANGED
|
@@ -9,7 +9,7 @@ const Router = require("express-promise-router");
|
|
|
9
9
|
|
|
10
10
|
//const Field = require("@saltcorn/data/models/field");
|
|
11
11
|
const Form = require("@saltcorn/data/models/form");
|
|
12
|
-
const { isAdmin, error_catcher } = require("./utils.js");
|
|
12
|
+
const { isAdmin, error_catcher, isAdminOrHasConfigMinRole } = require("./utils.js");
|
|
13
13
|
const { getState } = require("@saltcorn/data/db/state");
|
|
14
14
|
//const File = require("@saltcorn/data/models/file");
|
|
15
15
|
const User = require("@saltcorn/data/models/user");
|
|
@@ -490,7 +490,7 @@ const menuTojQME = (menu_items) =>
|
|
|
490
490
|
*/
|
|
491
491
|
router.get(
|
|
492
492
|
"/",
|
|
493
|
-
|
|
493
|
+
isAdminOrHasConfigMinRole("min_role_edit_menu"),
|
|
494
494
|
error_catcher(async (req, res) => {
|
|
495
495
|
const form = await menuForm(req);
|
|
496
496
|
const state = getState();
|
|
@@ -566,7 +566,7 @@ const jQMEtoMenu = (menu_items) =>
|
|
|
566
566
|
*/
|
|
567
567
|
router.post(
|
|
568
568
|
"/",
|
|
569
|
-
|
|
569
|
+
isAdminOrHasConfigMinRole("min_role_edit_menu"),
|
|
570
570
|
error_catcher(async (req, res) => {
|
|
571
571
|
const new_menu = req.body;
|
|
572
572
|
const menu_items = jQMEtoMenu(new_menu);
|
package/routes/search.js
CHANGED
|
@@ -8,6 +8,8 @@ const Router = require("express-promise-router");
|
|
|
8
8
|
const { span, h5, h4, nbsp, p, a, div } = require("@saltcorn/markup/tags");
|
|
9
9
|
|
|
10
10
|
const { getState } = require("@saltcorn/data/db/state");
|
|
11
|
+
const db = require("@saltcorn/data/db");
|
|
12
|
+
|
|
11
13
|
const { isAdmin, error_catcher } = require("./utils.js");
|
|
12
14
|
const Form = require("@saltcorn/data/models/form");
|
|
13
15
|
const Table = require("@saltcorn/data/models/table");
|
|
@@ -58,6 +60,13 @@ const searchConfigForm = (tables, views, req) => {
|
|
|
58
60
|
sublabel: req.__("Use table description instead of name as header"),
|
|
59
61
|
type: "Bool",
|
|
60
62
|
});
|
|
63
|
+
fields.push({
|
|
64
|
+
name: "search_results_decoration",
|
|
65
|
+
label: req.__("Show results in"),
|
|
66
|
+
sublabel: req.__("Show results from each table in this type of element"),
|
|
67
|
+
input_type: "select",
|
|
68
|
+
options: ["Cards", "Tabs"],
|
|
69
|
+
});
|
|
61
70
|
const blurb1 = req.__(
|
|
62
71
|
`Choose views for <a href="/search">search results</a> for each table.<br/>Set to blank to omit table from global search.`
|
|
63
72
|
);
|
|
@@ -95,6 +104,10 @@ router.get(
|
|
|
95
104
|
"search_table_description",
|
|
96
105
|
false
|
|
97
106
|
);
|
|
107
|
+
form.values.search_results_decoration = getState().getConfig(
|
|
108
|
+
"search_results_decoration",
|
|
109
|
+
"Cards"
|
|
110
|
+
);
|
|
98
111
|
send_infoarch_page({
|
|
99
112
|
res,
|
|
100
113
|
req,
|
|
@@ -126,13 +139,20 @@ router.post(
|
|
|
126
139
|
const result = form.validate(req.body);
|
|
127
140
|
|
|
128
141
|
if (result.success) {
|
|
142
|
+
const dbversion = await db.getVersion(true);
|
|
129
143
|
const search_table_description =
|
|
130
144
|
!!result.success.search_table_description;
|
|
131
145
|
await getState().setConfig(
|
|
132
146
|
"search_table_description",
|
|
133
147
|
search_table_description
|
|
134
148
|
);
|
|
149
|
+
await getState().setConfig(
|
|
150
|
+
"search_results_decoration",
|
|
151
|
+
result.success.search_results_decoration || "Cards"
|
|
152
|
+
);
|
|
153
|
+
await getState().setConfig("search_use_websearch", +dbversion >= 11.0);
|
|
135
154
|
delete result.success.search_table_description;
|
|
155
|
+
delete result.success.search_results_decoration;
|
|
136
156
|
await getState().setConfig("globalSearch", result.success);
|
|
137
157
|
if (!req.xhr) res.redirect("/search/config");
|
|
138
158
|
else res.json({ success: "ok" });
|
|
@@ -196,9 +216,12 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
196
216
|
"search_table_description",
|
|
197
217
|
false
|
|
198
218
|
);
|
|
219
|
+
const search_results_decoration = getState().getConfig(
|
|
220
|
+
"search_results_decoration",
|
|
221
|
+
"Cards"
|
|
222
|
+
);
|
|
199
223
|
const current_page = parseInt(_page) || 1;
|
|
200
224
|
const offset = (current_page - 1) * page_size;
|
|
201
|
-
let resp = [];
|
|
202
225
|
let tablesWithResults = [];
|
|
203
226
|
let tablesConfigured = 0;
|
|
204
227
|
for (const [tableName, viewName] of Object.entries(cfg)) {
|
|
@@ -206,7 +229,9 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
206
229
|
!viewName ||
|
|
207
230
|
viewName === "" ||
|
|
208
231
|
viewName === "search_table_description" ||
|
|
209
|
-
tableName === "search_table_description"
|
|
232
|
+
tableName === "search_table_description" ||
|
|
233
|
+
viewName === "search_results_decoration" ||
|
|
234
|
+
tableName === "search_results_decoration"
|
|
210
235
|
)
|
|
211
236
|
continue;
|
|
212
237
|
tablesConfigured += 1;
|
|
@@ -238,10 +263,9 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
238
263
|
}
|
|
239
264
|
|
|
240
265
|
if (vresps.length > 0) {
|
|
241
|
-
tablesWithResults.push({
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
title: span({ id: tableName }, sectionHeader),
|
|
266
|
+
tablesWithResults.push({
|
|
267
|
+
tableName,
|
|
268
|
+
label: sectionHeader,
|
|
245
269
|
contents: vresps.map((vr) => vr.html).join("<hr>") + paginate,
|
|
246
270
|
});
|
|
247
271
|
}
|
|
@@ -251,17 +275,36 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
251
275
|
const form = searchForm();
|
|
252
276
|
form.validate({ q });
|
|
253
277
|
|
|
278
|
+
const mkResultDisplay = () => {
|
|
279
|
+
switch (search_results_decoration) {
|
|
280
|
+
case "Tabs":
|
|
281
|
+
const tabContents = {};
|
|
282
|
+
tablesWithResults.forEach((tblRes) => {
|
|
283
|
+
tabContents[tblRes.label] = tblRes.contents;
|
|
284
|
+
});
|
|
285
|
+
return [{ type: "card", tabContents }];
|
|
286
|
+
|
|
287
|
+
default:
|
|
288
|
+
return tablesWithResults.map((tblRes) => ({
|
|
289
|
+
type: "card",
|
|
290
|
+
title: span({ id: tblRes.tableName }, tblRes.label),
|
|
291
|
+
contents: tblRes.contents,
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
254
296
|
// Prepare search result visualization
|
|
255
297
|
const searchResult =
|
|
256
|
-
|
|
298
|
+
tablesWithResults.length === 0
|
|
257
299
|
? [{ type: "card", contents: req.__("Not found") }]
|
|
258
|
-
:
|
|
300
|
+
: mkResultDisplay();
|
|
259
301
|
res.sendWrap(req.__("Search all tables"), {
|
|
260
302
|
above: [
|
|
261
303
|
{
|
|
262
304
|
type: "card",
|
|
263
305
|
contents: div(
|
|
264
306
|
renderForm(form, false),
|
|
307
|
+
syntax_help_link(req),
|
|
265
308
|
typeof table !== "undefined" &&
|
|
266
309
|
tablesConfigured > 1 &&
|
|
267
310
|
div(
|
|
@@ -296,6 +339,19 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
296
339
|
});
|
|
297
340
|
};
|
|
298
341
|
|
|
342
|
+
const syntax_help_link = (req) => {
|
|
343
|
+
const use_websearch = getState().getConfig("search_use_websearch", false);
|
|
344
|
+
if (use_websearch)
|
|
345
|
+
return a(
|
|
346
|
+
{
|
|
347
|
+
href: "javascript:void(0);",
|
|
348
|
+
onclick: "ajax_modal('/search/syntax-help')",
|
|
349
|
+
},
|
|
350
|
+
req.__("Search syntax")
|
|
351
|
+
);
|
|
352
|
+
else return "";
|
|
353
|
+
};
|
|
354
|
+
|
|
299
355
|
/**
|
|
300
356
|
* Execute search or only show search form
|
|
301
357
|
* @name get
|
|
@@ -327,9 +383,33 @@ router.get(
|
|
|
327
383
|
}
|
|
328
384
|
|
|
329
385
|
const form = searchForm();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
386
|
+
res.sendWrap(req.__("Search all tables"), {
|
|
387
|
+
type: "card",
|
|
388
|
+
contents: renderForm(form, false) + syntax_help_link(req),
|
|
389
|
+
});
|
|
333
390
|
}
|
|
334
391
|
})
|
|
335
392
|
);
|
|
393
|
+
|
|
394
|
+
router.get(
|
|
395
|
+
"/syntax-help",
|
|
396
|
+
error_catcher(async (req, res) => {
|
|
397
|
+
res.sendWrap(
|
|
398
|
+
req.__("Search syntax help"),
|
|
399
|
+
div(
|
|
400
|
+
p(
|
|
401
|
+
`Individual words matched independently. Example <code>large cat</code>`
|
|
402
|
+
),
|
|
403
|
+
p(
|
|
404
|
+
`Double quotes to match phrase as a single unit. Example <code>"large cat"</code> matches "the large cat sat..." but not "the large brown cat".`
|
|
405
|
+
),
|
|
406
|
+
p(
|
|
407
|
+
`"or" to match either of two phrases. Example <code>cat or mouse</code>`
|
|
408
|
+
),
|
|
409
|
+
p(
|
|
410
|
+
`"-" to exclude a word or phrase. Example <code>cat -mouse</code> does not match "cat and mouse"`
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
})
|
|
415
|
+
);
|
package/routes/tables.js
CHANGED
|
@@ -1533,6 +1533,8 @@ router.get(
|
|
|
1533
1533
|
key: (r) =>
|
|
1534
1534
|
r.type === "Unique"
|
|
1535
1535
|
? r.configuration.fields.join(", ")
|
|
1536
|
+
: r.type === "Index" && r.configuration?.field === "_fts"
|
|
1537
|
+
? "Full text search"
|
|
1536
1538
|
: r.type === "Index"
|
|
1537
1539
|
? r.configuration.field
|
|
1538
1540
|
: r.type === "Formula"
|
|
@@ -1629,11 +1631,23 @@ const constraintForm = (req, table, fields, type) => {
|
|
|
1629
1631
|
],
|
|
1630
1632
|
});
|
|
1631
1633
|
case "Index":
|
|
1634
|
+
const fieldopts = fields.map((f) => ({ label: f.label, name: f.name }));
|
|
1635
|
+
const hasIncludeFts = fields.filter((f) => f.attributes?.include_fts);
|
|
1636
|
+
if (!db.isSQLite && !hasIncludeFts.length)
|
|
1637
|
+
fieldopts.push({ label: "Full-text search", name: "_fts" });
|
|
1632
1638
|
return new Form({
|
|
1633
1639
|
action: `/table/add-constraint/${table.id}/${type}`,
|
|
1634
|
-
blurb:
|
|
1635
|
-
|
|
1636
|
-
|
|
1640
|
+
blurb:
|
|
1641
|
+
req.__(
|
|
1642
|
+
"Choose the field to be indexed. This make searching the table faster."
|
|
1643
|
+
) +
|
|
1644
|
+
" " +
|
|
1645
|
+
(hasIncludeFts.length
|
|
1646
|
+
? req.__(
|
|
1647
|
+
`Full-text search index is not available as the table contains Key fields (%s) with the "Include in full-text search" option enabled. Disable this before creating a Full-text search index`,
|
|
1648
|
+
hasIncludeFts.map((f) => f.name).join(",")
|
|
1649
|
+
)
|
|
1650
|
+
: ""),
|
|
1637
1651
|
fields: [
|
|
1638
1652
|
{
|
|
1639
1653
|
type: "String",
|
|
@@ -1641,7 +1655,7 @@ const constraintForm = (req, table, fields, type) => {
|
|
|
1641
1655
|
label: "Field",
|
|
1642
1656
|
required: true,
|
|
1643
1657
|
attributes: {
|
|
1644
|
-
options:
|
|
1658
|
+
options: fieldopts,
|
|
1645
1659
|
},
|
|
1646
1660
|
},
|
|
1647
1661
|
],
|
package/routes/tag_entries.js
CHANGED
|
@@ -12,7 +12,13 @@ const Tag = require("@saltcorn/data/models/tag");
|
|
|
12
12
|
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
13
13
|
const Router = require("express-promise-router");
|
|
14
14
|
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
isAdmin,
|
|
17
|
+
error_catcher,
|
|
18
|
+
csrfField,
|
|
19
|
+
isAdminOrHasConfigMinRole,
|
|
20
|
+
checkEditPermission,
|
|
21
|
+
} = require("./utils");
|
|
16
22
|
|
|
17
23
|
const Table = require("@saltcorn/data/models/table");
|
|
18
24
|
const View = require("@saltcorn/data/models/view");
|
|
@@ -170,6 +176,42 @@ router.post(
|
|
|
170
176
|
})
|
|
171
177
|
);
|
|
172
178
|
|
|
179
|
+
router.post(
|
|
180
|
+
"/add-tag-entity/:tagname/:entitytype/:entityid",
|
|
181
|
+
isAdminOrHasConfigMinRole([
|
|
182
|
+
"min_role_edit_tables",
|
|
183
|
+
"min_role_edit_views",
|
|
184
|
+
"min_role_edit_pages",
|
|
185
|
+
"min_role_edit_triggers",
|
|
186
|
+
]),
|
|
187
|
+
error_catcher(async (req, res) => {
|
|
188
|
+
const { tagname, entitytype, entityid } = req.params;
|
|
189
|
+
const tag = await Tag.findOne({ name: tagname });
|
|
190
|
+
|
|
191
|
+
const fieldName = idField(entitytype);
|
|
192
|
+
const auth = checkEditPermission(entitytype, req.user);
|
|
193
|
+
if (!auth) req.flash("error", "Not authorized");
|
|
194
|
+
else await tag.addEntry({ [fieldName]: +entityid });
|
|
195
|
+
switch (entitytype) {
|
|
196
|
+
case "views":
|
|
197
|
+
res.redirect(`/viewedit`);
|
|
198
|
+
break;
|
|
199
|
+
case "pages":
|
|
200
|
+
res.redirect(`/pageedit`);
|
|
201
|
+
break;
|
|
202
|
+
case "tables":
|
|
203
|
+
res.redirect(`/table`);
|
|
204
|
+
break;
|
|
205
|
+
case "triggers":
|
|
206
|
+
res.redirect(`/actions`);
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
|
|
173
215
|
// add one object to multiple tags
|
|
174
216
|
router.post(
|
|
175
217
|
"/add/multiple_tags/:entry_type/:object_id",
|
package/routes/tags.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { a, text, i, div } = require("@saltcorn/markup/tags");
|
|
2
2
|
|
|
3
3
|
const Tag = require("@saltcorn/data/models/tag");
|
|
4
|
+
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
4
5
|
const Router = require("express-promise-router");
|
|
5
6
|
const Form = require("@saltcorn/data/models/form");
|
|
6
7
|
const User = require("@saltcorn/data/models/user");
|
package/routes/utils.js
CHANGED
|
@@ -611,6 +611,20 @@ const getRandomPage = (pageGroup, req) => {
|
|
|
611
611
|
return Page.findOne({ id: sessionMember.page_id });
|
|
612
612
|
};
|
|
613
613
|
|
|
614
|
+
const checkEditPermission = (type, user) => {
|
|
615
|
+
if (user.role_id === 1) return true;
|
|
616
|
+
switch (type) {
|
|
617
|
+
case "views":
|
|
618
|
+
return getState().getConfig("min_role_edit_views", 1) >= user.role_id;
|
|
619
|
+
case "pages":
|
|
620
|
+
return getState().getConfig("min_role_edit_pages", 1) >= user.role_id;
|
|
621
|
+
case "triggers":
|
|
622
|
+
return getState().getConfig("min_role_edit_triggers", 1) >= user.role_id;
|
|
623
|
+
default:
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
614
628
|
module.exports = {
|
|
615
629
|
sqlsanitize,
|
|
616
630
|
csrfField,
|
|
@@ -634,4 +648,5 @@ module.exports = {
|
|
|
634
648
|
getEligiblePage,
|
|
635
649
|
getRandomPage,
|
|
636
650
|
tenant_letsencrypt_name,
|
|
651
|
+
checkEditPermission,
|
|
637
652
|
};
|