@saltcorn/server 0.9.4-beta.2 → 0.9.4-beta.21
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/app.js +16 -1
- package/auth/admin.js +19 -3
- package/auth/routes.js +8 -2
- package/help/JavaScript action code.tmd +1 -0
- package/load_plugins.js +8 -2
- package/locales/en.json +34 -1
- package/markup/admin.js +22 -18
- package/package.json +10 -9
- package/public/dayjslocales/af.js +1 -0
- package/public/dayjslocales/am.js +1 -0
- package/public/dayjslocales/ar-dz.js +1 -0
- package/public/dayjslocales/ar-iq.js +1 -0
- package/public/dayjslocales/ar-kw.js +1 -0
- package/public/dayjslocales/ar-ly.js +1 -0
- package/public/dayjslocales/ar-ma.js +1 -0
- package/public/dayjslocales/ar-sa.js +1 -0
- package/public/dayjslocales/ar-tn.js +1 -0
- package/public/dayjslocales/ar.js +1 -0
- package/public/dayjslocales/az.js +1 -0
- package/public/dayjslocales/be.js +1 -0
- package/public/dayjslocales/bg.js +1 -0
- package/public/dayjslocales/bi.js +1 -0
- package/public/dayjslocales/bm.js +1 -0
- package/public/dayjslocales/bn-bd.js +1 -0
- package/public/dayjslocales/bn.js +1 -0
- package/public/dayjslocales/bo.js +1 -0
- package/public/dayjslocales/br.js +1 -0
- package/public/dayjslocales/bs.js +1 -0
- package/public/dayjslocales/ca.js +1 -0
- package/public/dayjslocales/cs.js +1 -0
- package/public/dayjslocales/cv.js +1 -0
- package/public/dayjslocales/cy.js +1 -0
- package/public/dayjslocales/da.js +1 -0
- package/public/dayjslocales/de-at.js +1 -0
- package/public/dayjslocales/de-ch.js +1 -0
- package/public/dayjslocales/de.js +1 -0
- package/public/dayjslocales/dv.js +1 -0
- package/public/dayjslocales/el.js +1 -0
- package/public/dayjslocales/en-au.js +1 -0
- package/public/dayjslocales/en-ca.js +1 -0
- package/public/dayjslocales/en-gb.js +1 -0
- package/public/dayjslocales/en-ie.js +1 -0
- package/public/dayjslocales/en-il.js +1 -0
- package/public/dayjslocales/en-in.js +1 -0
- package/public/dayjslocales/en-nz.js +1 -0
- package/public/dayjslocales/en-sg.js +1 -0
- package/public/dayjslocales/en-tt.js +1 -0
- package/public/dayjslocales/en.js +1 -0
- package/public/dayjslocales/eo.js +1 -0
- package/public/dayjslocales/es-do.js +1 -0
- package/public/dayjslocales/es-mx.js +1 -0
- package/public/dayjslocales/es-pr.js +1 -0
- package/public/dayjslocales/es-us.js +1 -0
- package/public/dayjslocales/es.js +1 -0
- package/public/dayjslocales/et.js +1 -0
- package/public/dayjslocales/eu.js +1 -0
- package/public/dayjslocales/fa.js +1 -0
- package/public/dayjslocales/fi.js +1 -0
- package/public/dayjslocales/fo.js +1 -0
- package/public/dayjslocales/fr-ca.js +1 -0
- package/public/dayjslocales/fr-ch.js +1 -0
- package/public/dayjslocales/fr.js +1 -0
- package/public/dayjslocales/fy.js +1 -0
- package/public/dayjslocales/ga.js +1 -0
- package/public/dayjslocales/gd.js +1 -0
- package/public/dayjslocales/gl.js +1 -0
- package/public/dayjslocales/gom-latn.js +1 -0
- package/public/dayjslocales/gu.js +1 -0
- package/public/dayjslocales/he.js +1 -0
- package/public/dayjslocales/hi.js +1 -0
- package/public/dayjslocales/hr.js +1 -0
- package/public/dayjslocales/ht.js +1 -0
- package/public/dayjslocales/hu.js +1 -0
- package/public/dayjslocales/hy-am.js +1 -0
- package/public/dayjslocales/id.js +1 -0
- package/public/dayjslocales/is.js +1 -0
- package/public/dayjslocales/it-ch.js +1 -0
- package/public/dayjslocales/it.js +1 -0
- package/public/dayjslocales/ja.js +1 -0
- package/public/dayjslocales/jv.js +1 -0
- package/public/dayjslocales/ka.js +1 -0
- package/public/dayjslocales/kk.js +1 -0
- package/public/dayjslocales/km.js +1 -0
- package/public/dayjslocales/kn.js +1 -0
- package/public/dayjslocales/ko.js +1 -0
- package/public/dayjslocales/ku.js +1 -0
- package/public/dayjslocales/ky.js +1 -0
- package/public/dayjslocales/lb.js +1 -0
- package/public/dayjslocales/lo.js +1 -0
- package/public/dayjslocales/lt.js +1 -0
- package/public/dayjslocales/lv.js +1 -0
- package/public/dayjslocales/me.js +1 -0
- package/public/dayjslocales/mi.js +1 -0
- package/public/dayjslocales/mk.js +1 -0
- package/public/dayjslocales/ml.js +1 -0
- package/public/dayjslocales/mn.js +1 -0
- package/public/dayjslocales/mr.js +1 -0
- package/public/dayjslocales/ms-my.js +1 -0
- package/public/dayjslocales/ms.js +1 -0
- package/public/dayjslocales/mt.js +1 -0
- package/public/dayjslocales/my.js +1 -0
- package/public/dayjslocales/nb.js +1 -0
- package/public/dayjslocales/ne.js +1 -0
- package/public/dayjslocales/nl-be.js +1 -0
- package/public/dayjslocales/nl.js +1 -0
- package/public/dayjslocales/nn.js +1 -0
- package/public/dayjslocales/oc-lnc.js +1 -0
- package/public/dayjslocales/pa-in.js +1 -0
- package/public/dayjslocales/pl.js +1 -0
- package/public/dayjslocales/pt-br.js +1 -0
- package/public/dayjslocales/pt.js +1 -0
- package/public/dayjslocales/rn.js +1 -0
- package/public/dayjslocales/ro.js +1 -0
- package/public/dayjslocales/ru.js +1 -0
- package/public/dayjslocales/rw.js +1 -0
- package/public/dayjslocales/sd.js +1 -0
- package/public/dayjslocales/se.js +1 -0
- package/public/dayjslocales/si.js +1 -0
- package/public/dayjslocales/sk.js +1 -0
- package/public/dayjslocales/sl.js +1 -0
- package/public/dayjslocales/sq.js +1 -0
- package/public/dayjslocales/sr-cyrl.js +1 -0
- package/public/dayjslocales/sr.js +1 -0
- package/public/dayjslocales/ss.js +1 -0
- package/public/dayjslocales/sv-fi.js +1 -0
- package/public/dayjslocales/sv.js +1 -0
- package/public/dayjslocales/sw.js +1 -0
- package/public/dayjslocales/ta.js +1 -0
- package/public/dayjslocales/te.js +1 -0
- package/public/dayjslocales/tet.js +1 -0
- package/public/dayjslocales/tg.js +1 -0
- package/public/dayjslocales/th.js +1 -0
- package/public/dayjslocales/tk.js +1 -0
- package/public/dayjslocales/tl-ph.js +1 -0
- package/public/dayjslocales/tlh.js +1 -0
- package/public/dayjslocales/tr.js +1 -0
- package/public/dayjslocales/tzl.js +1 -0
- package/public/dayjslocales/tzm-latn.js +1 -0
- package/public/dayjslocales/tzm.js +1 -0
- package/public/dayjslocales/ug-cn.js +1 -0
- package/public/dayjslocales/uk.js +1 -0
- package/public/dayjslocales/ur.js +1 -0
- package/public/dayjslocales/uz-latn.js +1 -0
- package/public/dayjslocales/uz.js +1 -0
- package/public/dayjslocales/vi.js +1 -0
- package/public/dayjslocales/x-pseudo.js +1 -0
- package/public/dayjslocales/yo.js +1 -0
- package/public/dayjslocales/zh-cn.js +1 -0
- package/public/dayjslocales/zh-hk.js +1 -0
- package/public/dayjslocales/zh-tw.js +1 -0
- package/public/dayjslocales/zh.js +1 -0
- package/public/gridedit.js +2 -2
- package/public/log_viewer_utils.js +156 -0
- package/public/saltcorn-builder.css +62 -3
- package/public/saltcorn-common.js +8 -0
- package/public/saltcorn.js +30 -9
- package/public/tabulator_bootstrap5.min.css +1 -0
- package/restart_watcher.js +1 -0
- package/routes/actions.js +175 -18
- package/routes/admin.js +77 -5
- package/routes/common_lists.js +344 -152
- package/routes/fields.js +29 -5
- package/routes/files.js +3 -1
- package/routes/homepage.js +2 -1
- package/routes/list.js +5 -0
- package/routes/page.js +30 -13
- package/routes/page_groupedit.js +104 -83
- package/routes/pageedit.js +23 -7
- package/routes/tables.js +56 -6
- package/routes/tag_entries.js +18 -5
- package/routes/tags.js +65 -12
- package/routes/utils.js +23 -2
- package/routes/view.js +21 -2
- package/routes/viewedit.js +70 -4
- package/serve.js +177 -10
- package/tests/admin.test.js +17 -11
- package/tests/page_group.test.js +1 -0
- package/tests/table.test.js +1 -5
- package/tests/view.test.js +115 -15
- package/tests/viewedit.test.js +52 -29
- package/wrapper.js +11 -3
- package/public/relation_helpers.js +0 -351
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,11 @@ 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
|
+
const Table = require("@saltcorn/data/models/table");
|
|
31
|
+
|
|
26
32
|
const router = new Router();
|
|
27
33
|
module.exports = router;
|
|
28
34
|
|
|
@@ -42,7 +48,7 @@ router.get(
|
|
|
42
48
|
mkTable(
|
|
43
49
|
[
|
|
44
50
|
{
|
|
45
|
-
label: req.__("
|
|
51
|
+
label: req.__("Tag name"),
|
|
46
52
|
key: (r) =>
|
|
47
53
|
link(`/tag/${r.id || r.name}?show_list=tables`, text(r.name)),
|
|
48
54
|
},
|
|
@@ -57,7 +63,7 @@ router.get(
|
|
|
57
63
|
a(
|
|
58
64
|
{
|
|
59
65
|
href: `/tag/new`,
|
|
60
|
-
class: "btn btn-primary",
|
|
66
|
+
class: "btn btn-primary mt-3",
|
|
61
67
|
},
|
|
62
68
|
req.__("Create tag")
|
|
63
69
|
),
|
|
@@ -73,6 +79,13 @@ router.get(
|
|
|
73
79
|
error_catcher(async (req, res) => {
|
|
74
80
|
res.sendWrap(req.__(`New tag`), {
|
|
75
81
|
above: [
|
|
82
|
+
{
|
|
83
|
+
type: "breadcrumbs",
|
|
84
|
+
crumbs: [
|
|
85
|
+
{ text: req.__(`Tags`), href: "/tag" },
|
|
86
|
+
{ text: req.__(`New`) },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
76
89
|
{
|
|
77
90
|
type: "card",
|
|
78
91
|
title: req.__(`New tag`),
|
|
@@ -97,7 +110,26 @@ router.get(
|
|
|
97
110
|
})
|
|
98
111
|
);
|
|
99
112
|
|
|
100
|
-
|
|
113
|
+
router.get(
|
|
114
|
+
"/download-pack/:idorname",
|
|
115
|
+
isAdmin,
|
|
116
|
+
error_catcher(async (req, res) => {
|
|
117
|
+
const { idorname } = req.params;
|
|
118
|
+
const id = parseInt(idorname);
|
|
119
|
+
const tag = await Tag.findOne(id ? { id } : { name: idorname });
|
|
120
|
+
if (!tag) {
|
|
121
|
+
req.flash("error", req.__("Tag not found"));
|
|
122
|
+
return res.redirect(`/tag`);
|
|
123
|
+
}
|
|
124
|
+
const pack = await create_pack_from_tag(tag);
|
|
125
|
+
const readStream = new stream.PassThrough();
|
|
126
|
+
readStream.end(JSON.stringify(pack));
|
|
127
|
+
res.type("application/json");
|
|
128
|
+
res.attachment(`${tag.name}-pack.json`);
|
|
129
|
+
readStream.pipe(res);
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
const headerWithCollapser = (title, cardId, showList, count) =>
|
|
101
133
|
a(
|
|
102
134
|
{
|
|
103
135
|
class: `card-header-left-collapse ${!showList ? "collapsed" : ""} ps-3`,
|
|
@@ -107,7 +139,8 @@ const headerWithCollapser = (title, cardId, showList) =>
|
|
|
107
139
|
"aria-controls": cardId,
|
|
108
140
|
role: "button",
|
|
109
141
|
},
|
|
110
|
-
title
|
|
142
|
+
title,
|
|
143
|
+
` (${count})`
|
|
111
144
|
);
|
|
112
145
|
|
|
113
146
|
const isShowList = (showList, listType) => showList === listType;
|
|
@@ -129,6 +162,9 @@ router.get(
|
|
|
129
162
|
await setTableRefs(views);
|
|
130
163
|
const pages = await tag.getPages();
|
|
131
164
|
const triggers = await tag.getTriggers();
|
|
165
|
+
triggers.forEach((tr) => {
|
|
166
|
+
if (tr.table_id) tr.table_name = Table.findOne(tr.table_id)?.name;
|
|
167
|
+
});
|
|
132
168
|
const roles = await User.get_roles();
|
|
133
169
|
|
|
134
170
|
const tablesDomId = "tablesListId";
|
|
@@ -139,14 +175,15 @@ router.get(
|
|
|
139
175
|
above: [
|
|
140
176
|
{
|
|
141
177
|
type: "breadcrumbs",
|
|
142
|
-
crumbs: [{ text: req.__(`
|
|
178
|
+
crumbs: [{ text: req.__(`Tags`), href: "/tag" }, { text: tag.name }],
|
|
143
179
|
},
|
|
144
180
|
{
|
|
145
181
|
type: "card",
|
|
146
182
|
title: headerWithCollapser(
|
|
147
183
|
req.__("Tables"),
|
|
148
184
|
tablesDomId,
|
|
149
|
-
isShowList(show_list, "tables")
|
|
185
|
+
isShowList(show_list, "tables"),
|
|
186
|
+
tables.length
|
|
150
187
|
),
|
|
151
188
|
contents: [
|
|
152
189
|
await tablesList(tables, req, {
|
|
@@ -168,7 +205,8 @@ router.get(
|
|
|
168
205
|
title: headerWithCollapser(
|
|
169
206
|
req.__("Views"),
|
|
170
207
|
viewsDomId,
|
|
171
|
-
isShowList(show_list, "views")
|
|
208
|
+
isShowList(show_list, "views"),
|
|
209
|
+
views.length
|
|
172
210
|
),
|
|
173
211
|
contents: [
|
|
174
212
|
await viewsList(views, req, {
|
|
@@ -190,10 +228,11 @@ router.get(
|
|
|
190
228
|
title: headerWithCollapser(
|
|
191
229
|
req.__("Pages"),
|
|
192
230
|
pagesDomId,
|
|
193
|
-
isShowList(show_list, "pages")
|
|
231
|
+
isShowList(show_list, "pages"),
|
|
232
|
+
pages.length
|
|
194
233
|
),
|
|
195
234
|
contents: [
|
|
196
|
-
getPageList(pages, roles, req, {
|
|
235
|
+
await getPageList(pages, roles, req, {
|
|
197
236
|
tagId: tag.id,
|
|
198
237
|
domId: pagesDomId,
|
|
199
238
|
showList: isShowList(show_list, "pages"),
|
|
@@ -213,10 +252,11 @@ router.get(
|
|
|
213
252
|
title: headerWithCollapser(
|
|
214
253
|
req.__("Triggers"),
|
|
215
254
|
triggersDomId,
|
|
216
|
-
isShowList(show_list, "triggers")
|
|
255
|
+
isShowList(show_list, "triggers"),
|
|
256
|
+
triggers.length
|
|
217
257
|
),
|
|
218
258
|
contents: [
|
|
219
|
-
getTriggerList(triggers, req, {
|
|
259
|
+
await getTriggerList(triggers, req, {
|
|
220
260
|
tagId: tag.id,
|
|
221
261
|
domId: triggersDomId,
|
|
222
262
|
showList: isShowList(show_list, "triggers"),
|
|
@@ -230,6 +270,19 @@ router.get(
|
|
|
230
270
|
),
|
|
231
271
|
],
|
|
232
272
|
},
|
|
273
|
+
{
|
|
274
|
+
type: "card",
|
|
275
|
+
contents: [
|
|
276
|
+
a(
|
|
277
|
+
{
|
|
278
|
+
class: "btn btn-outline-primary",
|
|
279
|
+
href: `/tag/download-pack/${tag.id}`,
|
|
280
|
+
},
|
|
281
|
+
i({ class: "fas fa-download me-2" }),
|
|
282
|
+
"Download pack"
|
|
283
|
+
),
|
|
284
|
+
],
|
|
285
|
+
},
|
|
233
286
|
],
|
|
234
287
|
});
|
|
235
288
|
})
|
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();
|
|
@@ -144,9 +147,10 @@ const set_custom_http_headers = (res, req, state) => {
|
|
|
144
147
|
/**
|
|
145
148
|
* Tries to recognize tenant from HTTP Request
|
|
146
149
|
* @param {object} req
|
|
150
|
+
* @param {number|undefined} hostPartsOffset (optional) for socketIO, to get the tenant with localhost
|
|
147
151
|
* @returns {string}
|
|
148
152
|
*/
|
|
149
|
-
const get_tenant_from_req = (req) => {
|
|
153
|
+
const get_tenant_from_req = (req, hostPartsOffset) => {
|
|
150
154
|
if (req.subdomains && req.subdomains.length > 0)
|
|
151
155
|
return req.subdomains[req.subdomains.length - 1];
|
|
152
156
|
|
|
@@ -154,7 +158,8 @@ const get_tenant_from_req = (req) => {
|
|
|
154
158
|
return db.connectObj.default_schema;
|
|
155
159
|
if (!req.subdomains && req.headers.host) {
|
|
156
160
|
const parts = req.headers.host.split(".");
|
|
157
|
-
if (parts.length < 3
|
|
161
|
+
if (parts.length < (!hostPartsOffset ? 3 : 3 - hostPartsOffset))
|
|
162
|
+
return db.connectObj.default_schema;
|
|
158
163
|
else return parts[0];
|
|
159
164
|
}
|
|
160
165
|
};
|
|
@@ -505,6 +510,21 @@ const getEligiblePage = async (pageGroup, req, res) => {
|
|
|
505
510
|
}
|
|
506
511
|
};
|
|
507
512
|
|
|
513
|
+
/**
|
|
514
|
+
* @param {PageGroup} pageGroup
|
|
515
|
+
* @param {any} req
|
|
516
|
+
* @returns the page, null or an error msg
|
|
517
|
+
*/
|
|
518
|
+
const getRandomPage = (pageGroup, req) => {
|
|
519
|
+
if (pageGroup.members.length === 0)
|
|
520
|
+
return req.__("Pagegroup %s has no members", pageGroup.name);
|
|
521
|
+
const hash = crypto.createHash("sha1").update(req.sessionID).digest("hex");
|
|
522
|
+
const idx =
|
|
523
|
+
parseInt(hash.substring(hash.length - 4), 16) % pageGroup.members.length;
|
|
524
|
+
const sessionMember = pageGroup.members[idx];
|
|
525
|
+
return Page.findOne({ id: sessionMember.page_id });
|
|
526
|
+
};
|
|
527
|
+
|
|
508
528
|
module.exports = {
|
|
509
529
|
sqlsanitize,
|
|
510
530
|
csrfField,
|
|
@@ -524,4 +544,5 @@ module.exports = {
|
|
|
524
544
|
sendHtmlFile,
|
|
525
545
|
setRole,
|
|
526
546
|
getEligiblePage,
|
|
547
|
+
getRandomPage,
|
|
527
548
|
};
|
package/routes/view.js
CHANGED
|
@@ -74,10 +74,12 @@ router.get(
|
|
|
74
74
|
let title =
|
|
75
75
|
isModal && view.attributes?.popup_title
|
|
76
76
|
? view.attributes?.popup_title
|
|
77
|
-
:
|
|
78
|
-
|
|
77
|
+
: view.attributes?.page_title ||
|
|
78
|
+
scan_for_page_title(contents0, view.name); //legacy
|
|
79
|
+
if ((title || "").includes("{{")) {
|
|
79
80
|
title = await view.interpolate_title_string(title, query);
|
|
80
81
|
}
|
|
82
|
+
title = { title };
|
|
81
83
|
if (isModal && view.attributes?.popup_width)
|
|
82
84
|
res.set(
|
|
83
85
|
"SaltcornModalWidth",
|
|
@@ -85,10 +87,27 @@ router.get(
|
|
|
85
87
|
view.attributes?.popup_width_units || "px"
|
|
86
88
|
}`
|
|
87
89
|
);
|
|
90
|
+
if (isModal && view.attributes?.popup_minwidth)
|
|
91
|
+
res.set(
|
|
92
|
+
"SaltcornModalMinWidth",
|
|
93
|
+
`${view.attributes?.popup_minwidth}${
|
|
94
|
+
view.attributes?.popup_minwidth_units || "px"
|
|
95
|
+
}`
|
|
96
|
+
);
|
|
88
97
|
if (isModal && view.attributes?.popup_save_indicator)
|
|
89
98
|
res.set("SaltcornModalSaveIndicator", `true`);
|
|
90
99
|
if (isModal && view.attributes?.popup_link_out)
|
|
91
100
|
res.set("SaltcornModalLinkOut", `true`);
|
|
101
|
+
if (view.attributes?.page_description) {
|
|
102
|
+
let description = view.attributes?.page_description;
|
|
103
|
+
if ((description || "").includes("{{")) {
|
|
104
|
+
description = await view.interpolate_title_string(description, query);
|
|
105
|
+
}
|
|
106
|
+
title.description = description;
|
|
107
|
+
}
|
|
108
|
+
if (view.attributes?.no_menu) {
|
|
109
|
+
title.no_menu = true;
|
|
110
|
+
}
|
|
92
111
|
const tock = new Date();
|
|
93
112
|
const ms = tock.getTime() - tic.getTime();
|
|
94
113
|
if (!isTest())
|
package/routes/viewedit.js
CHANGED
|
@@ -29,6 +29,9 @@ const Workflow = require("@saltcorn/data/models/workflow");
|
|
|
29
29
|
const User = require("@saltcorn/data/models/user");
|
|
30
30
|
const Page = require("@saltcorn/data/models/page");
|
|
31
31
|
const File = require("@saltcorn/data/models/file");
|
|
32
|
+
const Tag = require("@saltcorn/data/models/tag");
|
|
33
|
+
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
34
|
+
|
|
32
35
|
const db = require("@saltcorn/data/db");
|
|
33
36
|
const { sleep } = require("@saltcorn/data/utils");
|
|
34
37
|
|
|
@@ -56,7 +59,18 @@ router.get(
|
|
|
56
59
|
error_catcher(async (req, res) => {
|
|
57
60
|
let orderBy = "name";
|
|
58
61
|
if (req.query._sortby === "viewtemplate") orderBy = "viewtemplate";
|
|
59
|
-
const
|
|
62
|
+
const viewq = {};
|
|
63
|
+
let filterOnTag;
|
|
64
|
+
if (req.query._tag) {
|
|
65
|
+
const tagEntries = await TagEntry.find({
|
|
66
|
+
tag_id: +req.query._tag,
|
|
67
|
+
not: { view_id: null },
|
|
68
|
+
});
|
|
69
|
+
viewq.id = { in: tagEntries.map((te) => te.view_id).filter(Boolean) };
|
|
70
|
+
filterOnTag = await Tag.findOne({ id: +req.query._tag });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const views = await View.find(viewq, { orderBy, nocase: true });
|
|
60
74
|
await setTableRefs(views);
|
|
61
75
|
|
|
62
76
|
if (req.query._sortby === "table")
|
|
@@ -64,7 +78,7 @@ router.get(
|
|
|
64
78
|
a.table.toLowerCase() > b.table.toLowerCase() ? 1 : -1
|
|
65
79
|
);
|
|
66
80
|
|
|
67
|
-
const viewMarkup = await viewsList(views, req);
|
|
81
|
+
const viewMarkup = await viewsList(views, req, { filterOnTag });
|
|
68
82
|
const tables = await Table.find();
|
|
69
83
|
|
|
70
84
|
res.sendWrap(req.__(`Views`), {
|
|
@@ -183,6 +197,27 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
|
183
197
|
options: roles.map((r) => ({ value: r.id, label: r.role })),
|
|
184
198
|
}),
|
|
185
199
|
new Field({
|
|
200
|
+
name: "page_title",
|
|
201
|
+
label: req.__("Page title"),
|
|
202
|
+
type: "String",
|
|
203
|
+
parent_field: "attributes",
|
|
204
|
+
tab: "View settings",
|
|
205
|
+
sublabel: req.__(
|
|
206
|
+
"Some view patterns accept interpolations. Ex: <code>{{ name }}</code> or <code>{{ row ? `Edit ${row.name}` : `New person` }}</code>"
|
|
207
|
+
),
|
|
208
|
+
}),
|
|
209
|
+
new Field({
|
|
210
|
+
name: "page_description",
|
|
211
|
+
label: req.__("Page description"),
|
|
212
|
+
type: "String",
|
|
213
|
+
parent_field: "attributes",
|
|
214
|
+
tab: "View settings",
|
|
215
|
+
sublabel: req.__(
|
|
216
|
+
"For search engines. Some view patterns accept interpolations."
|
|
217
|
+
),
|
|
218
|
+
}),
|
|
219
|
+
new Field({
|
|
220
|
+
// legacy
|
|
186
221
|
name: "default_render_page",
|
|
187
222
|
label: req.__("Show on page"),
|
|
188
223
|
sublabel: req.__(
|
|
@@ -209,6 +244,14 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
|
209
244
|
},
|
|
210
245
|
showIf: { viewtemplate: hasTable },
|
|
211
246
|
}),
|
|
247
|
+
new Field({
|
|
248
|
+
name: "no_menu",
|
|
249
|
+
label: req.__("No menu"),
|
|
250
|
+
sublabel: req.__("Omit the menu from this view"),
|
|
251
|
+
tab: "View settings",
|
|
252
|
+
parent_field: "attributes",
|
|
253
|
+
type: "Bool",
|
|
254
|
+
}),
|
|
212
255
|
new Field({
|
|
213
256
|
name: "popup_title",
|
|
214
257
|
label: req.__("Title"),
|
|
@@ -238,6 +281,26 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
|
238
281
|
options: ["px", "%", "vw", "em", "rem"],
|
|
239
282
|
},
|
|
240
283
|
},
|
|
284
|
+
{
|
|
285
|
+
name: "popup_minwidth",
|
|
286
|
+
label: req.__("Popup min width"),
|
|
287
|
+
type: "Integer",
|
|
288
|
+
tab: "Popup settings",
|
|
289
|
+
parent_field: "attributes",
|
|
290
|
+
attributes: { asideNext: true },
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "popup_minwidth_units",
|
|
294
|
+
label: req.__("Units"),
|
|
295
|
+
type: "String",
|
|
296
|
+
tab: "Popup settings",
|
|
297
|
+
fieldview: "radio_group",
|
|
298
|
+
parent_field: "attributes",
|
|
299
|
+
attributes: {
|
|
300
|
+
inline: true,
|
|
301
|
+
options: ["px", "%", "vw", "em", "rem"],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
241
304
|
{
|
|
242
305
|
name: "popup_save_indicator",
|
|
243
306
|
label: req.__("Save indicator"),
|
|
@@ -571,7 +634,10 @@ const respondWorkflow = (view, wf, wfres, req, res, table) => {
|
|
|
571
634
|
else if (wfres.renderBuilder) {
|
|
572
635
|
wfres.renderBuilder.options.view_id = view.id;
|
|
573
636
|
res.sendWrap(
|
|
574
|
-
|
|
637
|
+
{
|
|
638
|
+
title: req.__(`%s configuration`, view.name),
|
|
639
|
+
requestFluidLayout: true,
|
|
640
|
+
},
|
|
575
641
|
wrap(renderBuilder(wfres.renderBuilder, req.csrfToken()), true)
|
|
576
642
|
);
|
|
577
643
|
} else res.redirect(wfres.redirect);
|
|
@@ -759,7 +825,7 @@ router.post(
|
|
|
759
825
|
await View.update({ configuration: newcfg }, +id);
|
|
760
826
|
res.json({ success: "ok" });
|
|
761
827
|
} else {
|
|
762
|
-
res.json({ error: "
|
|
828
|
+
res.json({ error: req.__("Unable to save: No view") });
|
|
763
829
|
}
|
|
764
830
|
})
|
|
765
831
|
);
|
package/serve.js
CHANGED
|
@@ -112,6 +112,10 @@ const initMaster = async ({ disableMigrate }, useClusterAdaptor = true) => {
|
|
|
112
112
|
const tenants = await getAllTenants();
|
|
113
113
|
await init_multi_tenant(loadAllPlugins, disableMigrate, tenants);
|
|
114
114
|
}
|
|
115
|
+
eachTenant(async () => {
|
|
116
|
+
const state = getState();
|
|
117
|
+
if (state) await state.setConfig("joined_log_socket_ids", []);
|
|
118
|
+
});
|
|
115
119
|
if (useClusterAdaptor) setupPrimary();
|
|
116
120
|
};
|
|
117
121
|
|
|
@@ -283,7 +287,7 @@ module.exports =
|
|
|
283
287
|
})
|
|
284
288
|
.ready((glx) => {
|
|
285
289
|
const httpsServer = glx.httpsServer();
|
|
286
|
-
setupSocket(httpsServer);
|
|
290
|
+
setupSocket(appargs?.subdomainOffset, httpsServer);
|
|
287
291
|
httpsServer.setTimeout(timeout * 1000);
|
|
288
292
|
process.on("message", workerDispatchMsg);
|
|
289
293
|
glx.serveApp(app);
|
|
@@ -350,7 +354,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
|
|
|
350
354
|
// todo timeout to config
|
|
351
355
|
httpServer.setTimeout(timeout * 1000);
|
|
352
356
|
httpsServer.setTimeout(timeout * 1000);
|
|
353
|
-
setupSocket(httpServer, httpsServer);
|
|
357
|
+
setupSocket(appargs?.subdomainOffset, httpServer, httpsServer);
|
|
354
358
|
httpServer.listen(port, () => {
|
|
355
359
|
console.log("HTTP Server running on port 80");
|
|
356
360
|
});
|
|
@@ -363,7 +367,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
|
|
|
363
367
|
// server with http only
|
|
364
368
|
const http = require("http");
|
|
365
369
|
const httpServer = http.createServer(app);
|
|
366
|
-
setupSocket(httpServer);
|
|
370
|
+
setupSocket(appargs?.subdomainOffset, httpServer);
|
|
367
371
|
|
|
368
372
|
// todo timeout to config
|
|
369
373
|
// todo refer in doc to httpserver doc
|
|
@@ -380,7 +384,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
|
|
|
380
384
|
*
|
|
381
385
|
* @param {...*} servers
|
|
382
386
|
*/
|
|
383
|
-
const setupSocket = (...servers) => {
|
|
387
|
+
const setupSocket = (subdomainOffset, ...servers) => {
|
|
384
388
|
// https://socket.io/docs/v4/middlewares/
|
|
385
389
|
const wrap = (middleware) => (socket, next) =>
|
|
386
390
|
middleware(socket.request, {}, next);
|
|
@@ -390,15 +394,29 @@ const setupSocket = (...servers) => {
|
|
|
390
394
|
io.attach(server);
|
|
391
395
|
}
|
|
392
396
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
+
const passportInit = passport.initialize();
|
|
398
|
+
const sessionStore = getSessionStore();
|
|
399
|
+
const setupNamespace = (namespace) => {
|
|
400
|
+
//io.of(namespace).use(wrap(setTenant));
|
|
401
|
+
io.of(namespace).use(wrap(sessionStore));
|
|
402
|
+
io.of(namespace).use(wrap(passportInit));
|
|
403
|
+
io.of(namespace).use(wrap(passport.authenticate(["jwt", "session"])));
|
|
404
|
+
};
|
|
405
|
+
setupNamespace("/");
|
|
406
|
+
setupNamespace("/datastream");
|
|
397
407
|
if (process.send && !cluster.isMaster) io.adapter(createAdapter());
|
|
398
408
|
getState().setRoomEmitter((tenant, viewname, room_id, msg) => {
|
|
399
|
-
io.to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
|
|
409
|
+
io.of("/").to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
|
|
400
410
|
});
|
|
401
|
-
|
|
411
|
+
|
|
412
|
+
getState().setLogEmitter((tenant, level, msg) => {
|
|
413
|
+
const time = new Date().valueOf();
|
|
414
|
+
io.of("/")
|
|
415
|
+
.to(`_logs_${tenant}_`)
|
|
416
|
+
.emit("log_msg", { text: msg, time, level });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
io.of("/").on("connection", (socket) => {
|
|
402
420
|
socket.on("join_room", ([viewname, room_id]) => {
|
|
403
421
|
const ten = get_tenant_from_req(socket.request) || "public";
|
|
404
422
|
const f = () => {
|
|
@@ -418,5 +436,154 @@ const setupSocket = (...servers) => {
|
|
|
418
436
|
if (ten && ten !== "public") db.runWithTenant(ten, f);
|
|
419
437
|
else f();
|
|
420
438
|
});
|
|
439
|
+
|
|
440
|
+
socket.on("join_log_room", async (callback) => {
|
|
441
|
+
const tenant =
|
|
442
|
+
get_tenant_from_req(socket.request, subdomainOffset) || "public";
|
|
443
|
+
const f = async () => {
|
|
444
|
+
try {
|
|
445
|
+
const user = socket.request.user;
|
|
446
|
+
if (!user || user.role_id !== 1) throw new Error("Not authorized");
|
|
447
|
+
else {
|
|
448
|
+
socket.join(`_logs_${tenant}_`);
|
|
449
|
+
const socketIds = await getState().getConfig(
|
|
450
|
+
"joined_log_socket_ids"
|
|
451
|
+
);
|
|
452
|
+
socketIds.push(socket.id);
|
|
453
|
+
await getState().setConfig("joined_log_socket_ids", [...socketIds]);
|
|
454
|
+
callback({ status: "ok" });
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
getState().log(1, `Socket join_logs stream: ${err.stack}`);
|
|
458
|
+
callback({ status: "error", msg: err.message || "unknown error" });
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
|
|
462
|
+
else await f();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
socket.on("disconnect", async () => {
|
|
466
|
+
const tenant =
|
|
467
|
+
get_tenant_from_req(socket.request, subdomainOffset) || "public";
|
|
468
|
+
const f = async () => {
|
|
469
|
+
const socketIds = await getState().getConfig("joined_log_socket_ids");
|
|
470
|
+
const newSocketIds = socketIds.filter((id) => id !== socket.id);
|
|
471
|
+
await getState().setConfig("joined_log_socket_ids", newSocketIds);
|
|
472
|
+
};
|
|
473
|
+
if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
|
|
474
|
+
else f();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
io.of("/datastream").on("connection", (socket) => {
|
|
479
|
+
let dataStream = null;
|
|
480
|
+
let dataTarget = null;
|
|
481
|
+
socket.on(
|
|
482
|
+
"open_data_stream",
|
|
483
|
+
async ([viewName, id, fieldName, fieldView, targetOpts], callback) => {
|
|
484
|
+
const tenant =
|
|
485
|
+
get_tenant_from_req(socket.request, subdomainOffset) || "public";
|
|
486
|
+
const f = async () => {
|
|
487
|
+
try {
|
|
488
|
+
const user = socket.request.user;
|
|
489
|
+
const view = View.findOne({ name: viewName });
|
|
490
|
+
if (view.viewtemplateObj.authorizeDataStream) {
|
|
491
|
+
const authorized = await view.viewtemplateObj.authorizeDataStream(
|
|
492
|
+
view,
|
|
493
|
+
id,
|
|
494
|
+
fieldName,
|
|
495
|
+
user,
|
|
496
|
+
targetOpts
|
|
497
|
+
);
|
|
498
|
+
if (!authorized) throw new Error("Not authorized");
|
|
499
|
+
}
|
|
500
|
+
const { stream, target } = await view.openDataStream(
|
|
501
|
+
id,
|
|
502
|
+
fieldName,
|
|
503
|
+
fieldView,
|
|
504
|
+
user,
|
|
505
|
+
targetOpts
|
|
506
|
+
);
|
|
507
|
+
dataStream = stream;
|
|
508
|
+
dataTarget = target;
|
|
509
|
+
getState().log(
|
|
510
|
+
5,
|
|
511
|
+
`opened data stram to: ${JSON.stringify(dataTarget)}`
|
|
512
|
+
);
|
|
513
|
+
callback({ status: "ok", target });
|
|
514
|
+
} catch (err) {
|
|
515
|
+
getState().log(
|
|
516
|
+
1,
|
|
517
|
+
`Socket open_data_stream: ${err.message || "unknown error"}`
|
|
518
|
+
);
|
|
519
|
+
callback({ status: "error", msg: err.message || "unknown error" });
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
|
|
523
|
+
else f();
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
socket.on("write_to_stream", async (data, callback) => {
|
|
527
|
+
if (!dataStream) {
|
|
528
|
+
getState().log(1, "Socket write_to_stream: No stream available");
|
|
529
|
+
callback({ status: "error", msg: "No stream available" });
|
|
530
|
+
} else
|
|
531
|
+
dataStream.write(data, (err) => {
|
|
532
|
+
if (err) {
|
|
533
|
+
getState().log(1, "Socket write_to_stream: No stream available");
|
|
534
|
+
callback({ status: "error", msg: err.message || "unknown error" });
|
|
535
|
+
} else callback({ status: "ok" });
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
socket.on("close_data_stream", async (callback) => {
|
|
540
|
+
if (!dataStream) {
|
|
541
|
+
getState().log(1, "Socket close_data_stream: No stream available");
|
|
542
|
+
callback({ status: "error", msg: "No stream available" });
|
|
543
|
+
} else {
|
|
544
|
+
dataStream.close((err) => {
|
|
545
|
+
if (err) {
|
|
546
|
+
getState().log(
|
|
547
|
+
1,
|
|
548
|
+
`Socket close_data_stream: ${err.message || "unknown error"}`
|
|
549
|
+
);
|
|
550
|
+
callback({ status: "error", msg: err.message || "unknown error" });
|
|
551
|
+
} else {
|
|
552
|
+
getState().log(
|
|
553
|
+
5,
|
|
554
|
+
`closed data stram of: ${JSON.stringify(dataTarget)}`
|
|
555
|
+
);
|
|
556
|
+
callback({ status: "ok" });
|
|
557
|
+
dataStream = null;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
socket.on("disconnect", async () => {
|
|
564
|
+
const tenant =
|
|
565
|
+
get_tenant_from_req(socket.request, subdomainOffset) || "public";
|
|
566
|
+
const f = async () => {
|
|
567
|
+
if (dataStream)
|
|
568
|
+
dataStream.close((err) => {
|
|
569
|
+
if (err) {
|
|
570
|
+
getState().log(
|
|
571
|
+
1,
|
|
572
|
+
`Socket disconnect close_data_stream: ${
|
|
573
|
+
err.message || "unknown error"
|
|
574
|
+
}`
|
|
575
|
+
);
|
|
576
|
+
} else {
|
|
577
|
+
getState().log(
|
|
578
|
+
5,
|
|
579
|
+
`closed data stram of: ${JSON.stringify(dataTarget)}`
|
|
580
|
+
);
|
|
581
|
+
dataStream = null;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
|
|
586
|
+
else f();
|
|
587
|
+
});
|
|
421
588
|
});
|
|
422
589
|
};
|