@saltcorn/server 0.7.4 → 0.8.0-beta.0

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.
@@ -1,9 +1,6 @@
1
1
  const {
2
- a,
3
2
  div,
4
- text,
5
3
  button,
6
- i,
7
4
  form,
8
5
  select,
9
6
  option,
@@ -81,10 +78,10 @@ const formOptions = async (type, tag_id) => {
81
78
  ),
82
79
  };
83
80
  }
84
- case "trigger": {
81
+ case "triggers": {
85
82
  const ids = await tag.getTriggerIds();
86
83
  return {
87
- trigger: (await Trigger.find()).filter(
84
+ triggers: (Trigger.find()).filter(
88
85
  (value) => ids.indexOf(value.id) === -1
89
86
  ),
90
87
  };
@@ -101,7 +98,7 @@ router.get(
101
98
  above: [
102
99
  {
103
100
  type: "breadcrumbs",
104
- crumbs: [{ text: `Tag entry` }],
101
+ crumbs: [{ text: req.__(`Tag entry`) }],
105
102
  },
106
103
  {
107
104
  type: "card",
@@ -120,15 +117,19 @@ router.get(
120
117
 
121
118
  const idField = (entryType) => {
122
119
  switch (entryType) {
123
- case "tables": {
120
+ case "tables":
121
+ case "table": {
124
122
  return "table_id";
125
123
  }
126
- case "views": {
124
+ case "views":
125
+ case "view": {
127
126
  return "view_id";
128
127
  }
129
- case "pages": {
128
+ case "pages":
129
+ case "page": {
130
130
  return "page_id";
131
131
  }
132
+ case "triggers":
132
133
  case "trigger": {
133
134
  return "trigger_id";
134
135
  }
@@ -136,6 +137,7 @@ const idField = (entryType) => {
136
137
  return null;
137
138
  };
138
139
 
140
+ // add multiple objects to one tag
139
141
  router.post(
140
142
  "/add/:entry_type/:tag_id",
141
143
  isAdmin,
@@ -155,6 +157,24 @@ router.post(
155
157
  })
156
158
  );
157
159
 
160
+ // add one object to multiple tags
161
+ router.post(
162
+ "/add/multiple_tags/:entry_type/:object_id",
163
+ isAdmin,
164
+ error_catcher(async (req, res) => {
165
+ let { entry_type, object_id } = req.params;
166
+ let { tag_ids } = req.body;
167
+ object_id = parseInt(object_id);
168
+ tag_ids = tag_ids.map((id) => parseInt(id));
169
+ const tags = (await Tag.find()).filter((tag) => tag_ids.includes(tag.id));
170
+ const fieldName = idField(entry_type);
171
+ for (const tag of tags) {
172
+ await tag.addEntry({ [fieldName]: object_id });
173
+ }
174
+ res.json({ tags });
175
+ })
176
+ );
177
+
158
178
  router.post(
159
179
  "/remove/:entry_type/:entry_id/:tag_id",
160
180
  isAdmin,
@@ -168,6 +188,7 @@ router.post(
168
188
  } else {
169
189
  await TagEntry.update(entry.id, { [fieldName]: null });
170
190
  }
171
- res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
191
+ if (!req.xhr) res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
192
+ else res.json({ okay: true });
172
193
  })
173
194
  );
package/routes/tags.js CHANGED
@@ -128,18 +128,18 @@ router.get(
128
128
  const views = await tag.getViews();
129
129
  await setTableRefs(views);
130
130
  const pages = await tag.getPages();
131
- const trigger = await tag.getTrigger();
131
+ const triggers = await tag.getTriggers();
132
132
  const roles = await User.get_roles();
133
133
 
134
134
  const tablesDomId = "tablesListId";
135
135
  const viewsDomId = "viewsListId";
136
136
  const pagesDomId = "pagesDomId";
137
- const triggerDomId = "triggerDomId";
137
+ const triggersDomId = "triggerDomId";
138
138
  res.sendWrap(req.__("%s Tag", tag.name), {
139
139
  above: [
140
140
  {
141
141
  type: "breadcrumbs",
142
- crumbs: [{ text: `Tag: ${tag.name}` }],
142
+ crumbs: [{ text: req.__(`Tag: %s`, tag.name) }],
143
143
  },
144
144
  {
145
145
  type: "card",
@@ -211,19 +211,19 @@ router.get(
211
211
  type: "card",
212
212
  bodyId: "collapseTriggerCard",
213
213
  title: headerWithCollapser(
214
- req.__("Trigger"),
215
- triggerDomId,
216
- isShowList(show_list, "trigger")
214
+ req.__("Triggers"),
215
+ triggersDomId,
216
+ isShowList(show_list, "triggers")
217
217
  ),
218
218
  contents: [
219
- getTriggerList(trigger, req, {
219
+ getTriggerList(triggers, req, {
220
220
  tagId: tag.id,
221
- domId: triggerDomId,
222
- showList: isShowList(show_list, "trigger"),
221
+ domId: triggersDomId,
222
+ showList: isShowList(show_list, "triggers"),
223
223
  }),
224
224
  a(
225
225
  {
226
- href: `/tag-entries/add/trigger/${tag.id}`,
226
+ href: `/tag-entries/add/triggers/${tag.id}`,
227
227
  class: "btn btn-primary",
228
228
  },
229
229
  req.__("Add triggers")
package/routes/tenant.js CHANGED
@@ -8,13 +8,14 @@
8
8
  const Router = require("express-promise-router");
9
9
  const Form = require("@saltcorn/data/models/form");
10
10
  const { getState, add_tenant } = require("@saltcorn/data/db/state");
11
- const { create_tenant } = require("@saltcorn/admin-models/models/tenant");
12
11
  const {
12
+ create_tenant,
13
13
  getAllTenants,
14
14
  domain_sanitize,
15
15
  deleteTenant,
16
16
  switchToTenant,
17
17
  insertTenant,
18
+ Tenant,
18
19
  } = require("@saltcorn/admin-models/models/tenant");
19
20
  const {
20
21
  renderForm,
@@ -24,7 +25,6 @@ const {
24
25
  } = require("@saltcorn/markup");
25
26
  const {
26
27
  div,
27
- nbsp,
28
28
  p,
29
29
  a,
30
30
  h4,
@@ -87,6 +87,11 @@ const tenant_form = (req) =>
87
87
  input_type: "text",
88
88
  postText: text(req.hostname),
89
89
  },
90
+ {
91
+ name: "description",
92
+ label: req.__("Description"),
93
+ input_type: "text",
94
+ },
90
95
  ],
91
96
  });
92
97
 
@@ -148,37 +153,49 @@ router.get(
148
153
  "You are trying to create a tenant while connecting via an IP address rather than a domain. This will probably not work."
149
154
  )
150
155
  );
151
- let create_tenant_warning = "";
152
- // todo add custom create tenant warning message
153
- if (getState().getConfig("create_tenant_warning"))
154
- create_tenant_warning = div(
155
- {
156
- class: "alert alert-warning alert-dismissible fade show mt-5",
157
- role: "alert",
158
- },
159
- h4(req.__("Warning")),
160
- p(
161
- req.__(
162
- "Hosting on this site is provided for free and with no guarantee of availability or security of your application. "
163
- ) +
164
- " " +
165
- req.__(
166
- "This facility is intended solely for you to evaluate the suitability of Saltcorn. "
167
- ) +
168
- " " +
169
- req.__(
170
- "If you would like to store private information that needs to be secure, please use self-hosted Saltcorn. "
171
- ) +
172
- " " +
173
- req.__(
174
- 'See <a href="https://github.com/saltcorn/saltcorn">GitHub repository</a> for instructions<p>'
175
- )
176
- )
177
- );
156
+ let create_tenant_warning_text = "";
157
+ if (getState().getConfig("create_tenant_warning")) {
158
+ create_tenant_warning_text = getState().getConfig("create_tenant_warning_text");
159
+ if (create_tenant_warning_text && create_tenant_warning_text.length > 0)
160
+ create_tenant_warning_text = div(
161
+ {
162
+ class: "alert alert-warning alert-dismissible fade show mt-5",
163
+ role: "alert",
164
+ },
165
+ h4(req.__("Warning")),
166
+ p( create_tenant_warning_text
167
+ )
168
+ );
169
+ else
170
+ create_tenant_warning_text = div(
171
+ {
172
+ class: "alert alert-warning alert-dismissible fade show mt-5",
173
+ role: "alert",
174
+ },
175
+ h4(req.__("Warning")),
176
+ p(
177
+ req.__(
178
+ "Hosting on this site is provided for free and with no guarantee of availability or security of your application. "
179
+ ) +
180
+ " " +
181
+ req.__(
182
+ "This facility is intended solely for you to evaluate the suitability of Saltcorn. "
183
+ ) +
184
+ " " +
185
+ req.__(
186
+ "If you would like to store private information that needs to be secure, please use self-hosted Saltcorn. "
187
+ ) +
188
+ " " +
189
+ req.__(
190
+ 'See <a href="https://github.com/saltcorn/saltcorn">GitHub repository</a> for instructions<p>'
191
+ )
192
+ )
193
+ );
194
+ }
178
195
 
179
196
  res.sendWrap(
180
197
  req.__("Create application"),
181
- create_tenant_warning +
198
+ create_tenant_warning_text +
182
199
  renderForm(tenant_form(req), req.csrfToken()) +
183
200
  p(
184
201
  { class: "mt-2" },
@@ -246,6 +263,8 @@ router.post(
246
263
  else {
247
264
  // normalize domain name
248
265
  const subdomain = domain_sanitize(valres.success.subdomain);
266
+ // get description
267
+ const description = valres.success.description;
249
268
  // get list of tenants
250
269
  const allTens = await getAllTenants();
251
270
  if (allTens.includes(subdomain) || !subdomain) {
@@ -258,9 +277,15 @@ router.post(
258
277
  renderForm(form, req.csrfToken())
259
278
  );
260
279
  } else {
280
+ // tenant url
261
281
  const newurl = getNewURL(req, subdomain);
282
+ // tenant template
262
283
  const tenant_template = getState().getConfig("tenant_template");
263
- await switchToTenant(await insertTenant(subdomain), newurl);
284
+ // tenant creator
285
+ const user_email = req.user && req.user.email;
286
+ // switch to tenant
287
+ await switchToTenant(await insertTenant(subdomain, user_email, description, tenant_template), newurl);
288
+ // add tenant to global state
264
289
  add_tenant(subdomain);
265
290
  await create_tenant({
266
291
  t: subdomain,
@@ -348,6 +373,15 @@ router.get(
348
373
  {
349
374
  label: req.__("Description"),
350
375
  key: (r) => text(r.description),
376
+ //blurb: req.__("Specify some description for tenant if need"),
377
+ },
378
+ {
379
+ label: req.__("Creator email"),
380
+ key: (r) => text(r.email),
381
+ },
382
+ {
383
+ label: req.__("Created"),
384
+ key: (r) => text(r.created),
351
385
  },
352
386
  {
353
387
  label: req.__("Information"),
@@ -387,6 +421,7 @@ const tenant_settings_form = (req) =>
387
421
  field_names: [
388
422
  "role_to_create_tenant",
389
423
  "create_tenant_warning",
424
+ "create_tenant_warning_text",
390
425
  "tenant_template",
391
426
  ],
392
427
  action: "/tenant/settings",
@@ -468,8 +503,19 @@ router.post(
468
503
  const get_tenant_info = async (subdomain) => {
469
504
  const saneDomain = domain_sanitize(subdomain);
470
505
 
506
+ let info = {};
507
+
508
+ // get tenant row
509
+ const ten = await Tenant.findOne({ subdomain: saneDomain });
510
+ if (ten) {
511
+ info.description = ten.description;
512
+ info.created = ten.created;
513
+ }
514
+
515
+
516
+ // get data from tenant schema
471
517
  return await db.runWithTenant(saneDomain, async () => {
472
- let info = {};
518
+
473
519
  // TBD fix the first user issue because not always firt user by id is creator of tenant
474
520
  const firstUser = await User.find({}, { orderBy: "id", limit: 1 });
475
521
  if (firstUser && firstUser.length > 0) {
@@ -497,7 +543,11 @@ const get_tenant_info = async (subdomain) => {
497
543
  info.nconfigs = await db.count("_sc_config");
498
544
  // plugins count
499
545
  info.nplugins = await db.count("_sc_plugins");
500
- // TBD decide Do we need count tenants, table constraints, migrations
546
+ // migration count
547
+ info.nmigrations = await db.count("_sc_migrations");
548
+ // library count
549
+ info.nlibrary = await db.count("_sc_library");
550
+ // TBD decide Do we need count tenants, table constraints
501
551
  // base url
502
552
  info.base_url = await getConfig("base_url");
503
553
  return info;
@@ -525,6 +575,7 @@ router.get(
525
575
  return;
526
576
  }
527
577
  const { subdomain } = req.params;
578
+ // get tenant info
528
579
  const info = await get_tenant_info(subdomain);
529
580
  // get list of files
530
581
  let files;
@@ -545,7 +596,7 @@ router.get(
545
596
  contents: [
546
597
  table(
547
598
  tr(
548
- th(req.__("E-mail")),
599
+ th(req.__("First user E-mail")),
549
600
  td(
550
601
  a(
551
602
  { href: "mailto:" + info.first_user_email },
@@ -616,8 +667,13 @@ router.get(
616
667
  label: req.__("Base URL"),
617
668
  type: "String",
618
669
  },
670
+ {
671
+ name: "description",
672
+ label: req.__("Description"),
673
+ type: "String",
674
+ },
619
675
  ],
620
- values: { base_url: info.base_url },
676
+ values: { base_url: info.base_url, description: info.description },
621
677
  }),
622
678
  req.csrfToken()
623
679
  ),
@@ -673,6 +729,12 @@ router.post(
673
729
  const { base_url } = req.body;
674
730
  const saneDomain = domain_sanitize(subdomain);
675
731
 
732
+ // save description
733
+ const { description } = req.body;
734
+ await Tenant.update( saneDomain, {description: description});
735
+
736
+
737
+
676
738
  await db.runWithTenant(saneDomain, async () => {
677
739
  await getState().setConfig("base_url", base_url);
678
740
  });
@@ -701,7 +763,7 @@ router.post(
701
763
  return;
702
764
  }
703
765
  const { sub } = req.params;
704
-
766
+ // todo warning before deletion
705
767
  await deleteTenant(sub);
706
768
  res.redirect(`/tenant/list`);
707
769
  })
package/routes/utils.js CHANGED
@@ -20,6 +20,8 @@ const { validateHeaderName, validateHeaderValue } = require("http");
20
20
  const Crash = require("@saltcorn/data/models/crash");
21
21
 
22
22
  /**
23
+ * Checks that user logged or not.
24
+ * If not shows than shows flash and redirects to login
23
25
  * @param {object} req
24
26
  * @param {object} res
25
27
  * @param {function} next
@@ -35,6 +37,8 @@ function loggedIn(req, res, next) {
35
37
  }
36
38
 
37
39
  /**
40
+ * Checks that user has admin role or not.
41
+ * If user hasn't admin role shows flash and redirects user to login or totp
38
42
  * @param {object} req
39
43
  * @param {object} res
40
44
  * @param {function} next
@@ -60,6 +64,7 @@ function isAdmin(req, res, next) {
60
64
  }
61
65
 
62
66
  /**
67
+ * Sets language for HTTP Request / HTTP Responce
63
68
  * @param {object} req
64
69
  * @param {object} res
65
70
  * @param {string} state
@@ -73,6 +78,7 @@ const setLanguage = (req, res, state) => {
73
78
  };
74
79
 
75
80
  /**
81
+ * Sets Custom HTTP headers using data from "custom_http_headers" config variable
76
82
  * @param {object} res
77
83
  * @param {string} state
78
84
  * @returns {void}
@@ -96,6 +102,7 @@ const set_custom_http_headers = (res, state) => {
96
102
  };
97
103
 
98
104
  /**
105
+ * Tries to recognize tenant from HTTP Request
99
106
  * @param {object} req
100
107
  * @returns {string}
101
108
  */
@@ -179,6 +186,7 @@ const setTenant = (req, res, next) => {
179
186
  };
180
187
 
181
188
  /**
189
+ * Injects hidden input "_csrf" for CSRF token
182
190
  * @param {object} req
183
191
  * @returns {input}
184
192
  */
@@ -190,6 +198,7 @@ const csrfField = (req) =>
190
198
  });
191
199
 
192
200
  /**
201
+ * Errors catcher
193
202
  * @param {function} fn
194
203
  * @returns {function}
195
204
  */
@@ -198,6 +207,7 @@ const error_catcher = (fn) => (request, response, next) => {
198
207
  };
199
208
 
200
209
  /**
210
+ * Scans for page title from contents
201
211
  * @param {string|object} contents
202
212
  * @param {string} viewname
203
213
  * @returns {string}
@@ -218,11 +228,13 @@ const scan_for_page_title = (contents, viewname) => {
218
228
  };
219
229
 
220
230
  /**
231
+ * Gets gir revision
221
232
  * @returns {string}
222
233
  */
223
234
  const getGitRevision = () => db.connectObj.git_commit;
224
235
 
225
236
  /**
237
+ * Gets session store
226
238
  * @returns {session|cookieSession}
227
239
  */
228
240
  const getSessionStore = () => {
package/routes/view.js CHANGED
@@ -8,7 +8,6 @@ const Router = require("express-promise-router");
8
8
 
9
9
  const View = require("@saltcorn/data/models/view");
10
10
  const Table = require("@saltcorn/data/models/table");
11
- const Page = require("@saltcorn/data/models/page");
12
11
 
13
12
  const { div, text, i, a } = require("@saltcorn/markup/tags");
14
13
  const { renderForm, link } = require("@saltcorn/markup");
@@ -16,7 +16,7 @@ const {
16
16
  post_dropdown_item,
17
17
  renderBuilder,
18
18
  settingsDropdown,
19
- alert
19
+ alert,
20
20
  } = require("@saltcorn/markup");
21
21
  const {
22
22
  //span,
@@ -27,7 +27,9 @@ const {
27
27
  a,
28
28
  div,
29
29
  //button,
30
+ script,
30
31
  text,
32
+ domReady,
31
33
  } = require("@saltcorn/markup/tags");
32
34
 
33
35
  const { getState } = require("@saltcorn/data/db/state");
@@ -40,7 +42,6 @@ const View = require("@saltcorn/data/models/view");
40
42
  const Workflow = require("@saltcorn/data/models/workflow");
41
43
  const User = require("@saltcorn/data/models/user");
42
44
  const Page = require("@saltcorn/data/models/page");
43
- const Tag = require("@saltcorn/data/models/tag");
44
45
  const db = require("@saltcorn/data/db");
45
46
 
46
47
  const { add_to_menu } = require("@saltcorn/admin-models/models/pack");
@@ -77,20 +78,26 @@ router.get(
77
78
 
78
79
  const viewMarkup = await viewsList(views, req);
79
80
  const tables = await Table.find();
80
- const viewAccessWarning = view => {
81
- const table = tables.find(t => t.name === view.table)
82
- if (!table) return false
83
- if (table.ownership_field_id || table.ownership_formula) return false
81
+ const viewAccessWarning = (view) => {
82
+ const table = tables.find((t) => t.name === view.table);
83
+ if (!table) return false;
84
+ if (table.ownership_field_id || table.ownership_formula) return false;
84
85
 
85
- return table.min_role_read < view.min_role
86
- }
87
- const hasAccessWarning = views.filter(viewAccessWarning)
88
- const accessWarning = hasAccessWarning.length > 0
89
- ? alert("danger", `<p>You have views with a role to access lower than the table role to read,
86
+ return table.min_role_read < view.min_role;
87
+ };
88
+ const hasAccessWarning = views.filter(viewAccessWarning);
89
+ const accessWarning =
90
+ hasAccessWarning.length > 0
91
+ ? alert(
92
+ "danger",
93
+ `<p>You have views with a role to access lower than the table role to read,
90
94
  with no table ownership. In the next version of Saltcorn, this may cause a
91
95
  denial of access. Users will need to have table read access to any data displayed.</p>
92
- Views potentially affected: ${hasAccessWarning.map(v => v.name).join(", ")}`)
93
- : ''
96
+ Views potentially affected: ${hasAccessWarning
97
+ .map((v) => v.name)
98
+ .join(", ")}`
99
+ )
100
+ : "";
94
101
  res.sendWrap(req.__(`Views`), {
95
102
  above: [
96
103
  {
@@ -225,15 +232,15 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
225
232
  }),
226
233
  ...(isEdit
227
234
  ? [
228
- new Field({
229
- name: "viewtemplate",
230
- input_type: "hidden",
231
- }),
232
- new Field({
233
- name: "table_name",
234
- input_type: "hidden",
235
- }),
236
- ]
235
+ new Field({
236
+ name: "viewtemplate",
237
+ input_type: "hidden",
238
+ }),
239
+ new Field({
240
+ name: "table_name",
241
+ input_type: "hidden",
242
+ }),
243
+ ]
237
244
  : []),
238
245
  ],
239
246
  values,
@@ -434,7 +441,7 @@ router.post(
434
441
  * @returns {void}
435
442
  */
436
443
  const respondWorkflow = (view, wf, wfres, req, res) => {
437
- const wrap = (contents, noCard) => ({
444
+ const wrap = (contents, noCard, previewURL) => ({
438
445
  above: [
439
446
  {
440
447
  type: "breadcrumbs",
@@ -450,6 +457,12 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
450
457
  title: wfres.title,
451
458
  contents,
452
459
  },
460
+ ...previewURL ? [{
461
+ type: "card",
462
+ title: req.__("Preview"),
463
+ contents: div({ id: "viewcfg-preview", "data-preview-url": previewURL },
464
+ script(domReady(`updateViewPreview()`))),
465
+ }] : []
453
466
  ],
454
467
  });
455
468
  if (wfres.flash) req.flash(wfres.flash[0], wfres.flash[1]);
@@ -472,7 +485,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
472
485
  },
473
486
  ],
474
487
  },
475
- wrap(renderForm(wfres.renderForm, req.csrfToken()))
488
+ wrap(renderForm(wfres.renderForm, req.csrfToken()), false, wfres.previewURL)
476
489
  );
477
490
  else if (wfres.renderBuilder) {
478
491
  wfres.renderBuilder.options.view_id = view.id;
@@ -686,14 +699,14 @@ router.post(
686
699
  const view = await View.findOne({ id });
687
700
  const roles = await User.get_roles();
688
701
  const roleRow = roles.find((r) => r.id === +role);
689
- if (roleRow && view)
690
- req.flash(
691
- "success",
692
- req.__(`Minimum role for %s updated to %s`, view.name, roleRow.role)
693
- );
694
- else req.flash("success", req.__(`Minimum role updated`));
695
-
696
- res.redirect("/viewedit");
702
+ const message =
703
+ roleRow && view
704
+ ? req.__(`Minimum role for %s updated to %s`, view.name, roleRow.role)
705
+ : req.__(`Minimum role updated`);
706
+ if (!req.xhr) {
707
+ req.flash("success", message);
708
+ res.redirect("/viewedit");
709
+ } else res.json({ okay: true, responseText: message });
697
710
  })
698
711
  );
699
712
 
package/serve.js CHANGED
@@ -99,11 +99,16 @@ const workerDispatchMsg = ({ tenant, ...msg }) => {
99
99
  db.runWithTenant(tenant, () => workerDispatchMsg(msg));
100
100
  return;
101
101
  }
102
+
102
103
  if (msg.refresh_plugin_cfg) {
103
104
  Plugin.findOne({ name: msg.refresh_plugin_cfg }).then((plugin) => {
104
105
  if (plugin) loadPlugin(plugin);
105
106
  });
106
107
  }
108
+ if (!getState()) {
109
+ console.error("no State for tenant", tenant)
110
+ return
111
+ }
107
112
  if (msg.refresh) getState()[`refresh_${msg.refresh}`](true);
108
113
  if (msg.createTenant) {
109
114
  const tenant_template = getState().getConfig("tenant_template");
@@ -68,9 +68,11 @@ describe("admin page", () => {
68
68
  .expect(toInclude("Site identity settings"));
69
69
  });
70
70
  adminPageContains([
71
+ ["/admin", "Site identity"],
71
72
  ["/admin/backup", "Download a backup"],
72
73
  ["/admin/email", "Email settings"],
73
74
  ["/admin/system", "Restart server"],
75
+ ["/admin/dev", "Development"],
74
76
  ]);
75
77
  adminPageContains([
76
78
  ["/useradmin", "Create user"],
@@ -18,6 +18,7 @@ const { getState } = require("@saltcorn/data/db/state");
18
18
  const { get_reset_link, generate_email } = require("../auth/resetpw");
19
19
  const i18n = require("i18n");
20
20
  const path = require("path");
21
+ const fs = require("fs")
21
22
 
22
23
  afterAll(db.close);
23
24
  beforeAll(async () => {
@@ -602,3 +603,22 @@ describe("signup with custom login form", () => {
602
603
  expect(userrow.height).toBe(15);
603
604
  });
604
605
  });
606
+
607
+ describe("Locale files", () => {
608
+ it("should be valid JSON", async () => {
609
+
610
+ const localeFiles =
611
+ await fs.promises.readdir(path.join(__dirname, "..", "/locales"));
612
+ expect(localeFiles.length).toBeGreaterThan(3)
613
+ expect(localeFiles).toContain("en.json")
614
+ for (const fnm of localeFiles) {
615
+ const conts = await fs.promises.readFile(
616
+ path.join(__dirname, "..", "/locales", fnm)
617
+ )
618
+ expect(conts.length).toBeGreaterThan(1)
619
+
620
+ const j = JSON.parse(conts)
621
+ expect(Object.keys(j).length).toBeGreaterThan(1)
622
+ }
623
+ })
624
+ })