@saltcorn/server 0.9.4-beta.1 → 0.9.4-beta.11
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/help/JavaScript action code.tmd +1 -0
- package/locales/en.json +18 -1
- package/package.json +8 -8
- package/public/saltcorn-builder.css +37 -2
- package/public/saltcorn.css +4 -0
- package/public/saltcorn.js +13 -9
- package/restart_watcher.js +1 -0
- package/routes/actions.js +24 -3
- package/routes/common_lists.js +305 -136
- package/routes/fields.js +11 -2
- package/routes/files.js +3 -1
- package/routes/list.js +5 -0
- package/routes/page.js +30 -13
- package/routes/page_groupedit.js +104 -83
- package/routes/pageedit.js +20 -5
- package/routes/tables.js +29 -6
- package/routes/tag_entries.js +12 -4
- package/routes/tags.js +61 -12
- package/routes/utils.js +19 -0
- package/routes/view.js +20 -2
- package/routes/viewedit.js +63 -4
- package/tests/page_group.test.js +1 -0
- package/tests/view.test.js +115 -15
- package/tests/viewedit.test.js +0 -21
- package/wrapper.js +2 -2
- package/public/relation_helpers.js +0 -351
package/routes/page_groupedit.js
CHANGED
|
@@ -29,9 +29,17 @@ const groupPropsForm = async (req, isNew) => {
|
|
|
29
29
|
...(isNew
|
|
30
30
|
? {}
|
|
31
31
|
: {
|
|
32
|
-
onChange: `
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
onChange: `
|
|
33
|
+
saveAndContinue(this, (res) => {
|
|
34
|
+
history.replaceState(null, '', res.responseJSON.row.name);
|
|
35
|
+
const arrowsVisible = $('#upDownArrowsId').length > 0;
|
|
36
|
+
if (
|
|
37
|
+
arrowsVisible && res.responseJSON.row.random_allocation ||
|
|
38
|
+
!arrowsVisible && !res.responseJSON.row.random_allocation
|
|
39
|
+
) {
|
|
40
|
+
window.location.reload();
|
|
41
|
+
}
|
|
42
|
+
});`,
|
|
35
43
|
}),
|
|
36
44
|
noSubmitButton: !isNew,
|
|
37
45
|
fields: [
|
|
@@ -68,65 +76,78 @@ const groupPropsForm = async (req, isNew) => {
|
|
|
68
76
|
input_type: "select",
|
|
69
77
|
options: roles.map((r) => ({ value: r.id, label: r.role })),
|
|
70
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: "random_allocation",
|
|
81
|
+
label: req.__("Random allocation"),
|
|
82
|
+
type: "Bool",
|
|
83
|
+
sublabel: req.__(
|
|
84
|
+
"Serve a random page, ignoring the eligible formula. " +
|
|
85
|
+
"Within a session, reloads will always deliver the same page. " +
|
|
86
|
+
"This is a basic requirement for A/B testing."
|
|
87
|
+
),
|
|
88
|
+
},
|
|
71
89
|
],
|
|
72
90
|
});
|
|
73
91
|
};
|
|
74
92
|
|
|
75
|
-
const memberForm = async (action, req,
|
|
93
|
+
const memberForm = async (action, req, group, pageValidator) => {
|
|
76
94
|
const pageOptions = (await Page.find()).map((p) => p.name);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
attributes: {
|
|
88
|
-
options: pageOptions,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: "description",
|
|
93
|
-
label: req.__("Description"),
|
|
94
|
-
type: "String",
|
|
95
|
-
sublabel: req.__("A description of the group member"),
|
|
95
|
+
const fields = [
|
|
96
|
+
{
|
|
97
|
+
name: "page_name",
|
|
98
|
+
label: req.__("Page"),
|
|
99
|
+
sublabel: req.__("Page to be served"),
|
|
100
|
+
type: "String",
|
|
101
|
+
required: true,
|
|
102
|
+
validator: pageValidator,
|
|
103
|
+
attributes: {
|
|
104
|
+
options: pageOptions,
|
|
96
105
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "description",
|
|
109
|
+
label: req.__("Description"),
|
|
110
|
+
type: "String",
|
|
111
|
+
sublabel: req.__("A description of the group member"),
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
if (!group.random_allocation) {
|
|
115
|
+
fields.push({
|
|
116
|
+
name: "eligible_formula",
|
|
117
|
+
label: req.__("Eligible Formula"),
|
|
118
|
+
sublabel:
|
|
119
|
+
req.__("Formula to determine if this page should be served.") +
|
|
120
|
+
br() +
|
|
121
|
+
span(
|
|
122
|
+
"Variables in scope: ",
|
|
123
|
+
[
|
|
124
|
+
"width",
|
|
125
|
+
"height",
|
|
126
|
+
"innerWidth",
|
|
127
|
+
"innerHeight",
|
|
128
|
+
"user",
|
|
129
|
+
"locale",
|
|
130
|
+
"device",
|
|
131
|
+
]
|
|
132
|
+
.map((f) => code(f))
|
|
133
|
+
.join(", ")
|
|
134
|
+
),
|
|
135
|
+
help: {
|
|
136
|
+
topic: "Eligible Formula",
|
|
123
137
|
},
|
|
124
|
-
|
|
138
|
+
type: "String",
|
|
139
|
+
required: true,
|
|
140
|
+
class: "validate-expression",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return new Form({
|
|
144
|
+
action,
|
|
145
|
+
fields,
|
|
125
146
|
additionalButtons: [
|
|
126
147
|
{
|
|
127
148
|
label: req.__("Cancel"),
|
|
128
149
|
class: "btn btn-primary",
|
|
129
|
-
onclick: `cancelMemberEdit('${
|
|
150
|
+
onclick: `cancelMemberEdit('${group.name}');`,
|
|
130
151
|
},
|
|
131
152
|
],
|
|
132
153
|
});
|
|
@@ -142,7 +163,7 @@ const editMemberForm = async (member, req) => {
|
|
|
142
163
|
return await memberForm(
|
|
143
164
|
`/page_groupedit/edit-member/${member.id}`,
|
|
144
165
|
req,
|
|
145
|
-
group
|
|
166
|
+
group,
|
|
146
167
|
validator
|
|
147
168
|
);
|
|
148
169
|
};
|
|
@@ -156,7 +177,7 @@ const addMemberForm = async (group, req) => {
|
|
|
156
177
|
return await memberForm(
|
|
157
178
|
`/page_groupedit/add-member/${group.name}`,
|
|
158
179
|
req,
|
|
159
|
-
group
|
|
180
|
+
group,
|
|
160
181
|
validator
|
|
161
182
|
);
|
|
162
183
|
};
|
|
@@ -224,7 +245,7 @@ const pageGroupMembers = async (pageGroup, req) => {
|
|
|
224
245
|
if (members.length <= 1) return "";
|
|
225
246
|
else
|
|
226
247
|
return div(
|
|
227
|
-
{ class: "container" },
|
|
248
|
+
{ class: "container", id: "upDownArrowsId" },
|
|
228
249
|
div(
|
|
229
250
|
{ class: "row" },
|
|
230
251
|
div(
|
|
@@ -266,38 +287,38 @@ const pageGroupMembers = async (pageGroup, req) => {
|
|
|
266
287
|
)
|
|
267
288
|
);
|
|
268
289
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
key: (member) =>
|
|
284
|
-
link(`/page_groupedit/edit-member/${member.id}`, req.__("Edit")),
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
label: req.__("Delete"),
|
|
288
|
-
key: (member) =>
|
|
289
|
-
post_delete_btn(
|
|
290
|
-
`/page_groupedit/remove-member/${member.id}`,
|
|
291
|
-
req,
|
|
292
|
-
req.__("Member %s", member.sequence)
|
|
293
|
-
),
|
|
294
|
-
},
|
|
295
|
-
],
|
|
296
|
-
members,
|
|
290
|
+
const tblArr = [
|
|
291
|
+
{
|
|
292
|
+
label: req.__("Page"),
|
|
293
|
+
key: (r) =>
|
|
294
|
+
link(`/page/${pageIdToName[r.page_id]}`, pageIdToName[r.page_id]),
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
if (!pageGroup.random_allocation) {
|
|
298
|
+
tblArr.push({
|
|
299
|
+
label: "",
|
|
300
|
+
key: (r) => upDownBtns(r, req),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
tblArr.push(
|
|
297
304
|
{
|
|
298
|
-
|
|
305
|
+
label: req.__("Edit"),
|
|
306
|
+
key: (member) =>
|
|
307
|
+
link(`/page_groupedit/edit-member/${member.id}`, req.__("Edit")),
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
label: req.__("Delete"),
|
|
311
|
+
key: (member) =>
|
|
312
|
+
post_delete_btn(
|
|
313
|
+
`/page_groupedit/remove-member/${member.id}`,
|
|
314
|
+
req,
|
|
315
|
+
req.__("Member %s", member.sequence)
|
|
316
|
+
),
|
|
299
317
|
}
|
|
300
318
|
);
|
|
319
|
+
return mkTable(tblArr, members, {
|
|
320
|
+
hover: true,
|
|
321
|
+
});
|
|
301
322
|
};
|
|
302
323
|
|
|
303
324
|
/**
|
package/routes/pageedit.js
CHANGED
|
@@ -21,6 +21,8 @@ const { getViews, traverseSync } = require("@saltcorn/data/models/layout");
|
|
|
21
21
|
const { add_to_menu } = require("@saltcorn/admin-models/models/pack");
|
|
22
22
|
const db = require("@saltcorn/data/db");
|
|
23
23
|
const { getPageList, getPageGroupList } = require("./common_lists");
|
|
24
|
+
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
25
|
+
const Tag = require("@saltcorn/data/models/tag");
|
|
24
26
|
|
|
25
27
|
const {
|
|
26
28
|
isAdmin,
|
|
@@ -293,7 +295,18 @@ router.get(
|
|
|
293
295
|
"/",
|
|
294
296
|
isAdmin,
|
|
295
297
|
error_catcher(async (req, res) => {
|
|
296
|
-
const
|
|
298
|
+
const pageq = {};
|
|
299
|
+
let filterOnTag;
|
|
300
|
+
|
|
301
|
+
if (req.query._tag) {
|
|
302
|
+
const tagEntries = await TagEntry.find({
|
|
303
|
+
tag_id: +req.query._tag,
|
|
304
|
+
not: { page_id: null },
|
|
305
|
+
});
|
|
306
|
+
pageq.id = { in: tagEntries.map((te) => te.page_id).filter(Boolean) };
|
|
307
|
+
filterOnTag = await Tag.findOne({ id: +req.query._tag });
|
|
308
|
+
}
|
|
309
|
+
const pages = await Page.find(pageq, { orderBy: "name", nocase: true });
|
|
297
310
|
const pageGroups = await PageGroup.find(
|
|
298
311
|
{},
|
|
299
312
|
{ orderBy: "name", nocase: true }
|
|
@@ -311,7 +324,7 @@ router.get(
|
|
|
311
324
|
title: req.__("Your pages"),
|
|
312
325
|
class: "mt-0",
|
|
313
326
|
contents: div(
|
|
314
|
-
getPageList(pages, roles, req),
|
|
327
|
+
await getPageList(pages, roles, req, { filterOnTag }),
|
|
315
328
|
a(
|
|
316
329
|
{
|
|
317
330
|
href: `/pageedit/new`,
|
|
@@ -370,7 +383,7 @@ const wrap = (contents, noCard, req, page) => ({
|
|
|
370
383
|
crumbs: [
|
|
371
384
|
{ text: req.__("Pages"), href: "/pageedit" },
|
|
372
385
|
page
|
|
373
|
-
? { href: `/page/${page.name}`, text: page.name }
|
|
386
|
+
? { href: `/page/${encodeURIComponent(page.name)}`, text: page.name }
|
|
374
387
|
: { text: req.__("New") },
|
|
375
388
|
],
|
|
376
389
|
},
|
|
@@ -677,9 +690,11 @@ router.post(
|
|
|
677
690
|
|
|
678
691
|
if (id && req.body.layout) {
|
|
679
692
|
await Page.update(+id, { layout: req.body.layout });
|
|
680
|
-
res.json({
|
|
693
|
+
res.json({
|
|
694
|
+
success: "ok",
|
|
695
|
+
});
|
|
681
696
|
} else {
|
|
682
|
-
res.json({ error: "
|
|
697
|
+
res.json({ error: req.__("Unable to save: No page or no layout") });
|
|
683
698
|
}
|
|
684
699
|
})
|
|
685
700
|
);
|
package/routes/tables.js
CHANGED
|
@@ -13,6 +13,7 @@ const View = require("@saltcorn/data/models/view");
|
|
|
13
13
|
const User = require("@saltcorn/data/models/user");
|
|
14
14
|
const Model = require("@saltcorn/data/models/model");
|
|
15
15
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
16
|
+
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
16
17
|
const {
|
|
17
18
|
mkTable,
|
|
18
19
|
renderForm,
|
|
@@ -59,10 +60,12 @@ const { tablesList, viewsList } = require("./common_lists");
|
|
|
59
60
|
const {
|
|
60
61
|
InvalidConfiguration,
|
|
61
62
|
removeAllWhiteSpace,
|
|
63
|
+
comparingCaseInsensitive,
|
|
62
64
|
} = require("@saltcorn/data/utils");
|
|
63
65
|
const { EOL } = require("os");
|
|
64
66
|
|
|
65
67
|
const path = require("path");
|
|
68
|
+
const Tag = require("@saltcorn/data/models/tag");
|
|
66
69
|
/**
|
|
67
70
|
* @type {object}
|
|
68
71
|
* @const
|
|
@@ -714,6 +717,7 @@ router.get(
|
|
|
714
717
|
...new Set(child_relations.map(({ table }) => table.name)),
|
|
715
718
|
];
|
|
716
719
|
const triggers = table.id ? Trigger.find({ table_id: table.id }) : [];
|
|
720
|
+
triggers.sort(comparingCaseInsensitive("name"));
|
|
717
721
|
let fieldCard;
|
|
718
722
|
if (fields.length === 0) {
|
|
719
723
|
fieldCard = [
|
|
@@ -785,7 +789,16 @@ router.get(
|
|
|
785
789
|
triggers.length
|
|
786
790
|
? req.__("Table triggers: ") +
|
|
787
791
|
triggers
|
|
788
|
-
.map((t) =>
|
|
792
|
+
.map((t) =>
|
|
793
|
+
link(
|
|
794
|
+
`/actions/configure/${
|
|
795
|
+
t.id
|
|
796
|
+
}?on_done_redirect=${encodeURIComponent(
|
|
797
|
+
`table/${table.name}`
|
|
798
|
+
)}`,
|
|
799
|
+
t.name
|
|
800
|
+
)
|
|
801
|
+
)
|
|
789
802
|
.join(", ") +
|
|
790
803
|
"<br>"
|
|
791
804
|
: "",
|
|
@@ -1211,13 +1224,23 @@ router.get(
|
|
|
1211
1224
|
"/",
|
|
1212
1225
|
isAdmin,
|
|
1213
1226
|
error_catcher(async (req, res) => {
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1227
|
+
const tblq = {};
|
|
1228
|
+
let filterOnTag;
|
|
1229
|
+
if (req.query._tag) {
|
|
1230
|
+
const tagEntries = await TagEntry.find({
|
|
1231
|
+
tag_id: +req.query._tag,
|
|
1232
|
+
not: { table_id: null },
|
|
1233
|
+
});
|
|
1234
|
+
tblq.id = { in: tagEntries.map((te) => te.table_id).filter(Boolean) };
|
|
1235
|
+
filterOnTag = await Tag.findOne({ id: +req.query._tag });
|
|
1236
|
+
}
|
|
1237
|
+
const rows = await Table.find_with_external(tblq, {
|
|
1238
|
+
orderBy: "name",
|
|
1239
|
+
nocase: true,
|
|
1240
|
+
});
|
|
1218
1241
|
const roles = await User.get_roles();
|
|
1219
1242
|
const getRole = (rid) => roles.find((r) => r.id === rid).role;
|
|
1220
|
-
const mainCard = await tablesList(rows, req);
|
|
1243
|
+
const mainCard = await tablesList(rows, req, { filterOnTag });
|
|
1221
1244
|
const createCard = div(
|
|
1222
1245
|
a(
|
|
1223
1246
|
{ href: `/table/new`, class: "btn btn-primary mt-1 me-3" },
|
package/routes/tag_entries.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
select,
|
|
6
6
|
option,
|
|
7
7
|
label,
|
|
8
|
+
text,
|
|
8
9
|
} = require("@saltcorn/markup/tags");
|
|
9
10
|
|
|
10
11
|
const Tag = require("@saltcorn/data/models/tag");
|
|
@@ -94,15 +95,21 @@ router.get(
|
|
|
94
95
|
isAdmin,
|
|
95
96
|
error_catcher(async (req, res) => {
|
|
96
97
|
const { entry_type, tag_id } = req.params;
|
|
97
|
-
|
|
98
|
+
const tag = await Tag.findOne({ id: tag_id });
|
|
99
|
+
|
|
100
|
+
res.sendWrap(req.__("Add %s to tag %s", entry_type, tag.name), {
|
|
98
101
|
above: [
|
|
99
102
|
{
|
|
100
103
|
type: "breadcrumbs",
|
|
101
|
-
crumbs: [
|
|
104
|
+
crumbs: [
|
|
105
|
+
{ text: req.__(`Tags`), href: "/tag" },
|
|
106
|
+
{ text: tag.name, href: `/tag/${tag.id}` },
|
|
107
|
+
{ text: req.__(`Add %s`, text(entry_type)) },
|
|
108
|
+
],
|
|
102
109
|
},
|
|
103
110
|
{
|
|
104
111
|
type: "card",
|
|
105
|
-
title: req.__(`Add entries to tag
|
|
112
|
+
title: req.__(`Add entries to tag %s`, tag.name),
|
|
106
113
|
contents: buildForm(
|
|
107
114
|
entry_type,
|
|
108
115
|
tag_id,
|
|
@@ -148,9 +155,10 @@ router.post(
|
|
|
148
155
|
req.flash("error", req.__("Please select at least one item"));
|
|
149
156
|
return res.redirect(`/tag-entries/add/${entry_type}/${tag_id}`);
|
|
150
157
|
}
|
|
158
|
+
const ids_array = Array.isArray(ids) ? ids : [ids];
|
|
151
159
|
const fieldName = idField(entry_type);
|
|
152
160
|
const tag = await Tag.findOne({ id: tag_id });
|
|
153
|
-
for (const id of
|
|
161
|
+
for (const id of ids_array) {
|
|
154
162
|
await tag.addEntry({ [fieldName]: id });
|
|
155
163
|
}
|
|
156
164
|
res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
|
package/routes/tags.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
const { a, text } = require("@saltcorn/markup/tags");
|
|
1
|
+
const { a, text, i } = require("@saltcorn/markup/tags");
|
|
2
2
|
|
|
3
3
|
const Tag = require("@saltcorn/data/models/tag");
|
|
4
4
|
const Router = require("express-promise-router");
|
|
5
5
|
const Form = require("@saltcorn/data/models/form");
|
|
6
6
|
const User = require("@saltcorn/data/models/user");
|
|
7
|
+
const stream = require("stream");
|
|
7
8
|
|
|
8
9
|
const { isAdmin, error_catcher, csrfField } = require("./utils");
|
|
9
10
|
const { send_infoarch_page } = require("../markup/admin");
|
|
@@ -23,6 +24,10 @@ const {
|
|
|
23
24
|
getTriggerList,
|
|
24
25
|
} = require("./common_lists");
|
|
25
26
|
|
|
27
|
+
const db = require("@saltcorn/data/db");
|
|
28
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
29
|
+
const { create_pack_from_tag } = require("@saltcorn/admin-models/models/pack");
|
|
30
|
+
|
|
26
31
|
const router = new Router();
|
|
27
32
|
module.exports = router;
|
|
28
33
|
|
|
@@ -42,7 +47,7 @@ router.get(
|
|
|
42
47
|
mkTable(
|
|
43
48
|
[
|
|
44
49
|
{
|
|
45
|
-
label: req.__("
|
|
50
|
+
label: req.__("Tag name"),
|
|
46
51
|
key: (r) =>
|
|
47
52
|
link(`/tag/${r.id || r.name}?show_list=tables`, text(r.name)),
|
|
48
53
|
},
|
|
@@ -57,7 +62,7 @@ router.get(
|
|
|
57
62
|
a(
|
|
58
63
|
{
|
|
59
64
|
href: `/tag/new`,
|
|
60
|
-
class: "btn btn-primary",
|
|
65
|
+
class: "btn btn-primary mt-3",
|
|
61
66
|
},
|
|
62
67
|
req.__("Create tag")
|
|
63
68
|
),
|
|
@@ -73,6 +78,13 @@ router.get(
|
|
|
73
78
|
error_catcher(async (req, res) => {
|
|
74
79
|
res.sendWrap(req.__(`New tag`), {
|
|
75
80
|
above: [
|
|
81
|
+
{
|
|
82
|
+
type: "breadcrumbs",
|
|
83
|
+
crumbs: [
|
|
84
|
+
{ text: req.__(`Tags`), href: "/tag" },
|
|
85
|
+
{ text: req.__(`New`) },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
76
88
|
{
|
|
77
89
|
type: "card",
|
|
78
90
|
title: req.__(`New tag`),
|
|
@@ -97,7 +109,26 @@ router.get(
|
|
|
97
109
|
})
|
|
98
110
|
);
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
router.get(
|
|
113
|
+
"/download-pack/:idorname",
|
|
114
|
+
isAdmin,
|
|
115
|
+
error_catcher(async (req, res) => {
|
|
116
|
+
const { idorname } = req.params;
|
|
117
|
+
const id = parseInt(idorname);
|
|
118
|
+
const tag = await Tag.findOne(id ? { id } : { name: idorname });
|
|
119
|
+
if (!tag) {
|
|
120
|
+
req.flash("error", req.__("Tag not found"));
|
|
121
|
+
return res.redirect(`/tag`);
|
|
122
|
+
}
|
|
123
|
+
const pack = await create_pack_from_tag(tag);
|
|
124
|
+
const readStream = new stream.PassThrough();
|
|
125
|
+
readStream.end(JSON.stringify(pack));
|
|
126
|
+
res.type("application/json");
|
|
127
|
+
res.attachment(`${tag.name}-pack.json`);
|
|
128
|
+
readStream.pipe(res);
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
const headerWithCollapser = (title, cardId, showList, count) =>
|
|
101
132
|
a(
|
|
102
133
|
{
|
|
103
134
|
class: `card-header-left-collapse ${!showList ? "collapsed" : ""} ps-3`,
|
|
@@ -107,7 +138,8 @@ const headerWithCollapser = (title, cardId, showList) =>
|
|
|
107
138
|
"aria-controls": cardId,
|
|
108
139
|
role: "button",
|
|
109
140
|
},
|
|
110
|
-
title
|
|
141
|
+
title,
|
|
142
|
+
` (${count})`
|
|
111
143
|
);
|
|
112
144
|
|
|
113
145
|
const isShowList = (showList, listType) => showList === listType;
|
|
@@ -139,14 +171,15 @@ router.get(
|
|
|
139
171
|
above: [
|
|
140
172
|
{
|
|
141
173
|
type: "breadcrumbs",
|
|
142
|
-
crumbs: [{ text: req.__(`
|
|
174
|
+
crumbs: [{ text: req.__(`Tags`), href: "/tag" }, { text: tag.name }],
|
|
143
175
|
},
|
|
144
176
|
{
|
|
145
177
|
type: "card",
|
|
146
178
|
title: headerWithCollapser(
|
|
147
179
|
req.__("Tables"),
|
|
148
180
|
tablesDomId,
|
|
149
|
-
isShowList(show_list, "tables")
|
|
181
|
+
isShowList(show_list, "tables"),
|
|
182
|
+
tables.length
|
|
150
183
|
),
|
|
151
184
|
contents: [
|
|
152
185
|
await tablesList(tables, req, {
|
|
@@ -168,7 +201,8 @@ router.get(
|
|
|
168
201
|
title: headerWithCollapser(
|
|
169
202
|
req.__("Views"),
|
|
170
203
|
viewsDomId,
|
|
171
|
-
isShowList(show_list, "views")
|
|
204
|
+
isShowList(show_list, "views"),
|
|
205
|
+
views.length
|
|
172
206
|
),
|
|
173
207
|
contents: [
|
|
174
208
|
await viewsList(views, req, {
|
|
@@ -190,10 +224,11 @@ router.get(
|
|
|
190
224
|
title: headerWithCollapser(
|
|
191
225
|
req.__("Pages"),
|
|
192
226
|
pagesDomId,
|
|
193
|
-
isShowList(show_list, "pages")
|
|
227
|
+
isShowList(show_list, "pages"),
|
|
228
|
+
pages.length
|
|
194
229
|
),
|
|
195
230
|
contents: [
|
|
196
|
-
getPageList(pages, roles, req, {
|
|
231
|
+
await getPageList(pages, roles, req, {
|
|
197
232
|
tagId: tag.id,
|
|
198
233
|
domId: pagesDomId,
|
|
199
234
|
showList: isShowList(show_list, "pages"),
|
|
@@ -213,10 +248,11 @@ router.get(
|
|
|
213
248
|
title: headerWithCollapser(
|
|
214
249
|
req.__("Triggers"),
|
|
215
250
|
triggersDomId,
|
|
216
|
-
isShowList(show_list, "triggers")
|
|
251
|
+
isShowList(show_list, "triggers"),
|
|
252
|
+
triggers.length
|
|
217
253
|
),
|
|
218
254
|
contents: [
|
|
219
|
-
getTriggerList(triggers, req, {
|
|
255
|
+
await getTriggerList(triggers, req, {
|
|
220
256
|
tagId: tag.id,
|
|
221
257
|
domId: triggersDomId,
|
|
222
258
|
showList: isShowList(show_list, "triggers"),
|
|
@@ -230,6 +266,19 @@ router.get(
|
|
|
230
266
|
),
|
|
231
267
|
],
|
|
232
268
|
},
|
|
269
|
+
{
|
|
270
|
+
type: "card",
|
|
271
|
+
contents: [
|
|
272
|
+
a(
|
|
273
|
+
{
|
|
274
|
+
class: "btn btn-outline-primary",
|
|
275
|
+
href: `/tag/download-pack/${tag.id}`,
|
|
276
|
+
},
|
|
277
|
+
i({ class: "fas fa-download me-2" }),
|
|
278
|
+
"Download pack"
|
|
279
|
+
),
|
|
280
|
+
],
|
|
281
|
+
},
|
|
233
282
|
],
|
|
234
283
|
});
|
|
235
284
|
})
|
package/routes/utils.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
features,
|
|
14
14
|
} = require("@saltcorn/data/db/state");
|
|
15
15
|
const { get_base_url } = require("@saltcorn/data/models/config");
|
|
16
|
+
const { hash } = require("@saltcorn/data/utils");
|
|
16
17
|
const { input, script, domReady } = require("@saltcorn/markup/tags");
|
|
17
18
|
const session = require("express-session");
|
|
18
19
|
const cookieSession = require("cookie-session");
|
|
@@ -21,6 +22,7 @@ const { validateHeaderName, validateHeaderValue } = require("http");
|
|
|
21
22
|
const Crash = require("@saltcorn/data/models/crash");
|
|
22
23
|
const File = require("@saltcorn/data/models/file");
|
|
23
24
|
const User = require("@saltcorn/data/models/user");
|
|
25
|
+
const Page = require("@saltcorn/data/models/page");
|
|
24
26
|
const si = require("systeminformation");
|
|
25
27
|
const {
|
|
26
28
|
config_fields_form,
|
|
@@ -30,6 +32,7 @@ const {
|
|
|
30
32
|
} = require("../markup/admin.js");
|
|
31
33
|
const path = require("path");
|
|
32
34
|
const { UAParser } = require("ua-parser-js");
|
|
35
|
+
const crypto = require("crypto");
|
|
33
36
|
|
|
34
37
|
const get_sys_info = async () => {
|
|
35
38
|
const disks = await si.fsSize();
|
|
@@ -505,6 +508,21 @@ const getEligiblePage = async (pageGroup, req, res) => {
|
|
|
505
508
|
}
|
|
506
509
|
};
|
|
507
510
|
|
|
511
|
+
/**
|
|
512
|
+
* @param {PageGroup} pageGroup
|
|
513
|
+
* @param {any} req
|
|
514
|
+
* @returns the page, null or an error msg
|
|
515
|
+
*/
|
|
516
|
+
const getRandomPage = (pageGroup, req) => {
|
|
517
|
+
if (pageGroup.members.length === 0)
|
|
518
|
+
return req.__("Pagegroup %s has no members", pageGroup.name);
|
|
519
|
+
const hash = crypto.createHash("sha1").update(req.sessionID).digest("hex");
|
|
520
|
+
const idx =
|
|
521
|
+
parseInt(hash.substring(hash.length - 4), 16) % pageGroup.members.length;
|
|
522
|
+
const sessionMember = pageGroup.members[idx];
|
|
523
|
+
return Page.findOne({ id: sessionMember.page_id });
|
|
524
|
+
};
|
|
525
|
+
|
|
508
526
|
module.exports = {
|
|
509
527
|
sqlsanitize,
|
|
510
528
|
csrfField,
|
|
@@ -524,4 +542,5 @@ module.exports = {
|
|
|
524
542
|
sendHtmlFile,
|
|
525
543
|
setRole,
|
|
526
544
|
getEligiblePage,
|
|
545
|
+
getRandomPage,
|
|
527
546
|
};
|
package/routes/view.js
CHANGED
|
@@ -71,10 +71,14 @@ router.get(
|
|
|
71
71
|
const isModal = req.headers?.saltcornmodalrequest;
|
|
72
72
|
|
|
73
73
|
const contents0 = await view.run_possibly_on_page(query, req, res);
|
|
74
|
-
|
|
74
|
+
let title =
|
|
75
75
|
isModal && view.attributes?.popup_title
|
|
76
76
|
? view.attributes?.popup_title
|
|
77
|
-
:
|
|
77
|
+
: view.attributes?.page_title ||
|
|
78
|
+
scan_for_page_title(contents0, view.name); //legacy
|
|
79
|
+
if ((title || "").includes("{{")) {
|
|
80
|
+
title = await view.interpolate_title_string(title, query);
|
|
81
|
+
}
|
|
78
82
|
if (isModal && view.attributes?.popup_width)
|
|
79
83
|
res.set(
|
|
80
84
|
"SaltcornModalWidth",
|
|
@@ -82,10 +86,24 @@ router.get(
|
|
|
82
86
|
view.attributes?.popup_width_units || "px"
|
|
83
87
|
}`
|
|
84
88
|
);
|
|
89
|
+
if (isModal && view.attributes?.popup_minwidth)
|
|
90
|
+
res.set(
|
|
91
|
+
"SaltcornModalMinWidth",
|
|
92
|
+
`${view.attributes?.popup_minwidth}${
|
|
93
|
+
view.attributes?.popup_minwidth_units || "px"
|
|
94
|
+
}`
|
|
95
|
+
);
|
|
85
96
|
if (isModal && view.attributes?.popup_save_indicator)
|
|
86
97
|
res.set("SaltcornModalSaveIndicator", `true`);
|
|
87
98
|
if (isModal && view.attributes?.popup_link_out)
|
|
88
99
|
res.set("SaltcornModalLinkOut", `true`);
|
|
100
|
+
if (view.attributes?.page_description) {
|
|
101
|
+
let description = view.attributes?.page_description;
|
|
102
|
+
if ((description || "").includes("{{")) {
|
|
103
|
+
description = await view.interpolate_title_string(description, query);
|
|
104
|
+
}
|
|
105
|
+
title = { title, description };
|
|
106
|
+
}
|
|
89
107
|
const tock = new Date();
|
|
90
108
|
const ms = tock.getTime() - tic.getTime();
|
|
91
109
|
if (!isTest())
|