@saltcorn/server 0.9.4-beta.8 → 0.9.4

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.
Files changed (184) hide show
  1. package/app.js +16 -1
  2. package/auth/admin.js +19 -3
  3. package/auth/routes.js +16 -4
  4. package/auth/testhelp.js +17 -1
  5. package/load_plugins.js +8 -2
  6. package/locales/en.json +29 -1
  7. package/markup/admin.js +22 -18
  8. package/package.json +10 -9
  9. package/public/dayjslocales/af.js +1 -0
  10. package/public/dayjslocales/am.js +1 -0
  11. package/public/dayjslocales/ar-dz.js +1 -0
  12. package/public/dayjslocales/ar-iq.js +1 -0
  13. package/public/dayjslocales/ar-kw.js +1 -0
  14. package/public/dayjslocales/ar-ly.js +1 -0
  15. package/public/dayjslocales/ar-ma.js +1 -0
  16. package/public/dayjslocales/ar-sa.js +1 -0
  17. package/public/dayjslocales/ar-tn.js +1 -0
  18. package/public/dayjslocales/ar.js +1 -0
  19. package/public/dayjslocales/az.js +1 -0
  20. package/public/dayjslocales/be.js +1 -0
  21. package/public/dayjslocales/bg.js +1 -0
  22. package/public/dayjslocales/bi.js +1 -0
  23. package/public/dayjslocales/bm.js +1 -0
  24. package/public/dayjslocales/bn-bd.js +1 -0
  25. package/public/dayjslocales/bn.js +1 -0
  26. package/public/dayjslocales/bo.js +1 -0
  27. package/public/dayjslocales/br.js +1 -0
  28. package/public/dayjslocales/bs.js +1 -0
  29. package/public/dayjslocales/ca.js +1 -0
  30. package/public/dayjslocales/cs.js +1 -0
  31. package/public/dayjslocales/cv.js +1 -0
  32. package/public/dayjslocales/cy.js +1 -0
  33. package/public/dayjslocales/da.js +1 -0
  34. package/public/dayjslocales/de-at.js +1 -0
  35. package/public/dayjslocales/de-ch.js +1 -0
  36. package/public/dayjslocales/de.js +1 -0
  37. package/public/dayjslocales/dv.js +1 -0
  38. package/public/dayjslocales/el.js +1 -0
  39. package/public/dayjslocales/en-au.js +1 -0
  40. package/public/dayjslocales/en-ca.js +1 -0
  41. package/public/dayjslocales/en-gb.js +1 -0
  42. package/public/dayjslocales/en-ie.js +1 -0
  43. package/public/dayjslocales/en-il.js +1 -0
  44. package/public/dayjslocales/en-in.js +1 -0
  45. package/public/dayjslocales/en-nz.js +1 -0
  46. package/public/dayjslocales/en-sg.js +1 -0
  47. package/public/dayjslocales/en-tt.js +1 -0
  48. package/public/dayjslocales/en.js +1 -0
  49. package/public/dayjslocales/eo.js +1 -0
  50. package/public/dayjslocales/es-do.js +1 -0
  51. package/public/dayjslocales/es-mx.js +1 -0
  52. package/public/dayjslocales/es-pr.js +1 -0
  53. package/public/dayjslocales/es-us.js +1 -0
  54. package/public/dayjslocales/es.js +1 -0
  55. package/public/dayjslocales/et.js +1 -0
  56. package/public/dayjslocales/eu.js +1 -0
  57. package/public/dayjslocales/fa.js +1 -0
  58. package/public/dayjslocales/fi.js +1 -0
  59. package/public/dayjslocales/fo.js +1 -0
  60. package/public/dayjslocales/fr-ca.js +1 -0
  61. package/public/dayjslocales/fr-ch.js +1 -0
  62. package/public/dayjslocales/fr.js +1 -0
  63. package/public/dayjslocales/fy.js +1 -0
  64. package/public/dayjslocales/ga.js +1 -0
  65. package/public/dayjslocales/gd.js +1 -0
  66. package/public/dayjslocales/gl.js +1 -0
  67. package/public/dayjslocales/gom-latn.js +1 -0
  68. package/public/dayjslocales/gu.js +1 -0
  69. package/public/dayjslocales/he.js +1 -0
  70. package/public/dayjslocales/hi.js +1 -0
  71. package/public/dayjslocales/hr.js +1 -0
  72. package/public/dayjslocales/ht.js +1 -0
  73. package/public/dayjslocales/hu.js +1 -0
  74. package/public/dayjslocales/hy-am.js +1 -0
  75. package/public/dayjslocales/id.js +1 -0
  76. package/public/dayjslocales/is.js +1 -0
  77. package/public/dayjslocales/it-ch.js +1 -0
  78. package/public/dayjslocales/it.js +1 -0
  79. package/public/dayjslocales/ja.js +1 -0
  80. package/public/dayjslocales/jv.js +1 -0
  81. package/public/dayjslocales/ka.js +1 -0
  82. package/public/dayjslocales/kk.js +1 -0
  83. package/public/dayjslocales/km.js +1 -0
  84. package/public/dayjslocales/kn.js +1 -0
  85. package/public/dayjslocales/ko.js +1 -0
  86. package/public/dayjslocales/ku.js +1 -0
  87. package/public/dayjslocales/ky.js +1 -0
  88. package/public/dayjslocales/lb.js +1 -0
  89. package/public/dayjslocales/lo.js +1 -0
  90. package/public/dayjslocales/lt.js +1 -0
  91. package/public/dayjslocales/lv.js +1 -0
  92. package/public/dayjslocales/me.js +1 -0
  93. package/public/dayjslocales/mi.js +1 -0
  94. package/public/dayjslocales/mk.js +1 -0
  95. package/public/dayjslocales/ml.js +1 -0
  96. package/public/dayjslocales/mn.js +1 -0
  97. package/public/dayjslocales/mr.js +1 -0
  98. package/public/dayjslocales/ms-my.js +1 -0
  99. package/public/dayjslocales/ms.js +1 -0
  100. package/public/dayjslocales/mt.js +1 -0
  101. package/public/dayjslocales/my.js +1 -0
  102. package/public/dayjslocales/nb.js +1 -0
  103. package/public/dayjslocales/ne.js +1 -0
  104. package/public/dayjslocales/nl-be.js +1 -0
  105. package/public/dayjslocales/nl.js +1 -0
  106. package/public/dayjslocales/nn.js +1 -0
  107. package/public/dayjslocales/oc-lnc.js +1 -0
  108. package/public/dayjslocales/pa-in.js +1 -0
  109. package/public/dayjslocales/pl.js +1 -0
  110. package/public/dayjslocales/pt-br.js +1 -0
  111. package/public/dayjslocales/pt.js +1 -0
  112. package/public/dayjslocales/rn.js +1 -0
  113. package/public/dayjslocales/ro.js +1 -0
  114. package/public/dayjslocales/ru.js +1 -0
  115. package/public/dayjslocales/rw.js +1 -0
  116. package/public/dayjslocales/sd.js +1 -0
  117. package/public/dayjslocales/se.js +1 -0
  118. package/public/dayjslocales/si.js +1 -0
  119. package/public/dayjslocales/sk.js +1 -0
  120. package/public/dayjslocales/sl.js +1 -0
  121. package/public/dayjslocales/sq.js +1 -0
  122. package/public/dayjslocales/sr-cyrl.js +1 -0
  123. package/public/dayjslocales/sr.js +1 -0
  124. package/public/dayjslocales/ss.js +1 -0
  125. package/public/dayjslocales/sv-fi.js +1 -0
  126. package/public/dayjslocales/sv.js +1 -0
  127. package/public/dayjslocales/sw.js +1 -0
  128. package/public/dayjslocales/ta.js +1 -0
  129. package/public/dayjslocales/te.js +1 -0
  130. package/public/dayjslocales/tet.js +1 -0
  131. package/public/dayjslocales/tg.js +1 -0
  132. package/public/dayjslocales/th.js +1 -0
  133. package/public/dayjslocales/tk.js +1 -0
  134. package/public/dayjslocales/tl-ph.js +1 -0
  135. package/public/dayjslocales/tlh.js +1 -0
  136. package/public/dayjslocales/tr.js +1 -0
  137. package/public/dayjslocales/tzl.js +1 -0
  138. package/public/dayjslocales/tzm-latn.js +1 -0
  139. package/public/dayjslocales/tzm.js +1 -0
  140. package/public/dayjslocales/ug-cn.js +1 -0
  141. package/public/dayjslocales/uk.js +1 -0
  142. package/public/dayjslocales/ur.js +1 -0
  143. package/public/dayjslocales/uz-latn.js +1 -0
  144. package/public/dayjslocales/uz.js +1 -0
  145. package/public/dayjslocales/vi.js +1 -0
  146. package/public/dayjslocales/x-pseudo.js +1 -0
  147. package/public/dayjslocales/yo.js +1 -0
  148. package/public/dayjslocales/zh-cn.js +1 -0
  149. package/public/dayjslocales/zh-hk.js +1 -0
  150. package/public/dayjslocales/zh-tw.js +1 -0
  151. package/public/dayjslocales/zh.js +1 -0
  152. package/public/gridedit.js +2 -2
  153. package/public/log_viewer_utils.js +156 -0
  154. package/public/saltcorn-builder.css +43 -2
  155. package/public/saltcorn-common.js +39 -29
  156. package/public/saltcorn.js +29 -8
  157. package/public/tabulator_bootstrap5.min.css +1 -0
  158. package/restart_watcher.js +1 -0
  159. package/routes/actions.js +175 -18
  160. package/routes/admin.js +83 -9
  161. package/routes/common_lists.js +344 -152
  162. package/routes/fields.js +18 -3
  163. package/routes/homepage.js +2 -1
  164. package/routes/page.js +30 -13
  165. package/routes/page_groupedit.js +104 -83
  166. package/routes/pageedit.js +23 -7
  167. package/routes/tables.js +51 -5
  168. package/routes/tag_entries.js +18 -5
  169. package/routes/tags.js +65 -12
  170. package/routes/utils.js +23 -2
  171. package/routes/view.js +12 -1
  172. package/routes/viewedit.js +46 -3
  173. package/serve.js +177 -10
  174. package/tests/admin.test.js +17 -11
  175. package/tests/api.test.js +27 -0
  176. package/tests/fields.test.js +132 -5
  177. package/tests/help.test.js +37 -0
  178. package/tests/page_group.test.js +1 -0
  179. package/tests/plugins.test.js +0 -12
  180. package/tests/table.test.js +1 -5
  181. package/tests/view.test.js +127 -15
  182. package/tests/viewedit.test.js +52 -8
  183. package/wrapper.js +9 -2
  184. 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.__("Tagname"),
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
- const headerWithCollapser = (title, cardId, showList) =>
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.__(`Tag: %s`, tag.name) }],
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) return db.connectObj.default_schema;
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
@@ -79,6 +79,7 @@ router.get(
79
79
  if ((title || "").includes("{{")) {
80
80
  title = await view.interpolate_title_string(title, query);
81
81
  }
82
+ title = { title };
82
83
  if (isModal && view.attributes?.popup_width)
83
84
  res.set(
84
85
  "SaltcornModalWidth",
@@ -86,6 +87,13 @@ router.get(
86
87
  view.attributes?.popup_width_units || "px"
87
88
  }`
88
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
+ );
89
97
  if (isModal && view.attributes?.popup_save_indicator)
90
98
  res.set("SaltcornModalSaveIndicator", `true`);
91
99
  if (isModal && view.attributes?.popup_link_out)
@@ -95,7 +103,10 @@ router.get(
95
103
  if ((description || "").includes("{{")) {
96
104
  description = await view.interpolate_title_string(description, query);
97
105
  }
98
- title = { title, description };
106
+ title.description = description;
107
+ }
108
+ if (view.attributes?.no_menu) {
109
+ title.no_menu = true;
99
110
  }
100
111
  const tock = new Date();
101
112
  const ms = tock.getTime() - tic.getTime();
@@ -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 views = await View.find({}, { orderBy, nocase: true });
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`), {
@@ -203,6 +217,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
203
217
  ),
204
218
  }),
205
219
  new Field({
220
+ // legacy
206
221
  name: "default_render_page",
207
222
  label: req.__("Show on page"),
208
223
  sublabel: req.__(
@@ -229,6 +244,14 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
229
244
  },
230
245
  showIf: { viewtemplate: hasTable },
231
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
+ }),
232
255
  new Field({
233
256
  name: "popup_title",
234
257
  label: req.__("Title"),
@@ -258,6 +281,26 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
258
281
  options: ["px", "%", "vw", "em", "rem"],
259
282
  },
260
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
+ },
261
304
  {
262
305
  name: "popup_save_indicator",
263
306
  label: req.__("Save indicator"),
@@ -782,7 +825,7 @@ router.post(
782
825
  await View.update({ configuration: newcfg }, +id);
783
826
  res.json({ success: "ok" });
784
827
  } else {
785
- res.json({ error: "no view" });
828
+ res.json({ error: req.__("Unable to save: No view") });
786
829
  }
787
830
  })
788
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
- //io.use(wrap(setTenant));
394
- io.use(wrap(getSessionStore()));
395
- io.use(wrap(passport.initialize()));
396
- io.use(wrap(passport.authenticate(["jwt", "session"])));
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
- io.on("connection", (socket) => {
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
  };