@saltcorn/server 1.0.0-beta.0 → 1.0.0-beta.10

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/routes/files.js CHANGED
@@ -20,7 +20,15 @@ const {
20
20
  setTenant,
21
21
  is_relative_url,
22
22
  } = require("./utils.js");
23
- const { h1, div, text } = require("@saltcorn/markup/tags");
23
+ const {
24
+ h1,
25
+ div,
26
+ text,
27
+ script,
28
+ style,
29
+ link,
30
+ domReady,
31
+ } = require("@saltcorn/markup/tags");
24
32
  const { editRoleForm, fileUploadForm } = require("../markup/forms.js");
25
33
  const { strictParseInt } = require("@saltcorn/data/plugin-helper");
26
34
  const {
@@ -43,20 +51,98 @@ const { extract } = require("@saltcorn/admin-models/models/backup");
43
51
  const router = new Router();
44
52
  module.exports = router;
45
53
 
46
- /**
47
- * Edit file Role form
48
- * @param {*} file
49
- * @param {*} roles
50
- * @param {*} req
51
- * @returns {Form}
52
- */
53
- const editFileRoleForm = (file, roles, req) =>
54
- editRoleForm({
55
- url: `/files/setrole/${file.path_to_serve}`,
56
- current_role: file.min_role_read,
57
- roles,
58
- req,
54
+ const send_files_picker = async (folder, noSubdirs, inputId, req, res) => {
55
+ res.set("SaltcornModalWidth", "1200px");
56
+ res.sendWrap(req.__("Please select a file"), {
57
+ above: [
58
+ script({
59
+ src: `/static_assets/${db.connectObj.version_tag}/bundle.js`,
60
+ defer: true,
61
+ }),
62
+ script(
63
+ domReady(
64
+ `$("head").append('${link({
65
+ rel: "stylesheet",
66
+ href: `/static_assets/${db.connectObj.version_tag}/bundle.css`,
67
+ })}')`
68
+ )
69
+ ),
70
+ div({
71
+ id: "saltcorn-file-manager",
72
+ full_manager: "false",
73
+ folder: folder,
74
+ input_id: inputId,
75
+ ...(noSubdirs ? { no_subdirs: "true" } : {}),
76
+ }),
77
+ ],
59
78
  });
79
+ };
80
+
81
+ router.get(
82
+ "/picker",
83
+ error_catcher(async (req, res) => {
84
+ const { folder, input_id, no_subdirs } = req.query;
85
+ send_files_picker(folder, no_subdirs, input_id, req, res);
86
+ })
87
+ );
88
+
89
+ router.get(
90
+ "/visible_entries",
91
+ error_catcher(async (req, res) => {
92
+ const role = req.user?.role_id ? req.user.role_id : 100;
93
+ const userId = req.user?.id;
94
+ const { dir, no_subdirs } = req.query;
95
+ const noSubdirs = no_subdirs === "true";
96
+ const safeDir = File.normalise(dir || "/");
97
+ const absFolder = path.join(
98
+ db.connectObj.file_store,
99
+ db.getTenantSchema(),
100
+ safeDir
101
+ );
102
+ const dirOnDisk = await File.from_file_on_disk(
103
+ path.basename(absFolder),
104
+ path.dirname(absFolder)
105
+ );
106
+ if (dirOnDisk.min_role_read < role) {
107
+ getState().log(5, `Directory denied. path=${dir} role=${role}`);
108
+ res.json({ files: [], roles: [], directories: [] });
109
+ return;
110
+ }
111
+ const rows = (
112
+ await File.find({ folder: dir }, { orderBy: "filename" })
113
+ ).filter((f) => {
114
+ if (noSubdirs && f.isDirectory) return false;
115
+ else return role <= f.min_role_read || (userId && userId === f.user_id);
116
+ });
117
+ const roles = await User.get_roles();
118
+ if (!no_subdirs && safeDir && safeDir !== "/" && safeDir !== ".") {
119
+ let dirname = path.dirname(safeDir);
120
+ if (dirname === ".") dirname = "/";
121
+ rows.unshift(
122
+ new File({
123
+ filename: "..",
124
+ location: dirname,
125
+ isDirectory: true,
126
+ mime_super: "",
127
+ mime_sub: "",
128
+ })
129
+ );
130
+ }
131
+
132
+ for (const file of rows) {
133
+ file.location = file.path_to_serve;
134
+ }
135
+ const directories = !noSubdirs
136
+ ? (await File.allDirectories(true)).filter(
137
+ (dir) => role <= dir.min_role_read
138
+ )
139
+ : [];
140
+ for (const dir of directories) {
141
+ dir.location = dir.path_to_serve;
142
+ }
143
+ res.json({ files: rows, roles, directories });
144
+ })
145
+ );
60
146
 
61
147
  /**
62
148
  * @name get
@@ -116,7 +202,7 @@ router.get(
116
202
  contents: {
117
203
  type: "card",
118
204
  contents: [
119
- div({ id: "saltcorn-file-manager" }),
205
+ div({ full_manager: "true", id: "saltcorn-file-manager" }),
120
206
  fileUploadForm(req, safeDir),
121
207
  ],
122
208
  },
@@ -14,6 +14,7 @@ const Form = require("@saltcorn/data/models/form");
14
14
  const File = require("@saltcorn/data/models/file");
15
15
  const User = require("@saltcorn/data/models/user");
16
16
  const { renderForm, post_btn } = require("@saltcorn/markup");
17
+ const db = require("@saltcorn/data/db");
17
18
 
18
19
  const router = new Router();
19
20
  module.exports = router;
@@ -39,9 +40,18 @@ router.get(
39
40
  orderDesc: true,
40
41
  limit: 20,
41
42
  });
42
- await Notification.mark_as_read({
43
- id: { in: nots.filter((n) => !n.read).map((n) => n.id) },
44
- });
43
+ const unreads = nots.filter((n) => !n.read);
44
+ if (unreads.length > 0)
45
+ await Notification.mark_as_read(
46
+ !db.isSQLite
47
+ ? {
48
+ id: { in: unreads.map((n) => n.id) },
49
+ }
50
+ : {
51
+ or: unreads.map((n) => ({ id: n.id })),
52
+ }
53
+ );
54
+
45
55
  const form = notificationSettingsForm();
46
56
  const user = await User.findOne({ id: req.user?.id });
47
57
  form.values = { notify_email: user?._attributes?.notify_email };
package/routes/plugins.js CHANGED
@@ -59,6 +59,10 @@ const { sleep, removeNonWordChars } = require("@saltcorn/data/utils");
59
59
  const { loadAllPlugins } = require("../load_plugins");
60
60
  const npmFetch = require("npm-registry-fetch");
61
61
  const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
62
+ const {
63
+ supportedVersion,
64
+ isVersionSupported,
65
+ } = require("@saltcorn/plugins-loader/stable_versioning");
62
66
 
63
67
  /**
64
68
  * @type {object}
@@ -177,6 +181,8 @@ const get_store_items = async () => {
177
181
  has_auth: plugin.has_auth,
178
182
  unsafe: plugin.unsafe,
179
183
  source: plugin.source,
184
+ ready_for_mobile:
185
+ plugin.ready_for_mobile && plugin.ready_for_mobile(plugin.name),
180
186
  }))
181
187
  .filter((p) => !p.unsafe || isRoot || tenants_unsafe_plugins);
182
188
  const local_logins = installed_plugins
@@ -190,6 +196,8 @@ const get_store_items = async () => {
190
196
  github: plugin.source === "github",
191
197
  git: plugin.source === "git",
192
198
  local: plugin.source === "local",
199
+ ready_for_mobile:
200
+ plugin.ready_for_mobile && plugin.ready_for_mobile(plugin.name),
193
201
  }));
194
202
 
195
203
  const pack_items = packs_available.map((pack) => ({
@@ -274,7 +282,8 @@ const store_item_html = (req) => (item) => ({
274
282
  item.github && badge("GitHub"),
275
283
  item.git && badge("Git"),
276
284
  item.local && badge(req.__("Local")),
277
- item.installed && badge(req.__("Installed"))
285
+ item.installed && badge(req.__("Installed")),
286
+ item.ready_for_mobile && badge(req.__("Mobile"))
278
287
  ),
279
288
  div(item.description || ""),
280
289
  item.documentation_link
@@ -584,7 +593,9 @@ router.get(
584
593
  error_catcher(async (req, res) => {
585
594
  const { name } = req.params;
586
595
  const withoutOrg = name.replace(/^@saltcorn\//, "");
587
- const plugin = await Plugin.store_by_name(decodeURIComponent(withoutOrg));
596
+ let plugin = await Plugin.store_by_name(decodeURIComponent(withoutOrg));
597
+ if (!plugin)
598
+ plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
588
599
  if (!plugin) {
589
600
  getState().log(
590
601
  2,
@@ -609,6 +620,7 @@ router.get(
609
620
  if (mod) selected = mod.version;
610
621
  }
611
622
  if (!selected) selected = versions[versions.length - 1];
623
+ const packageJson = require("../package.json");
612
624
  return res.send(
613
625
  form(
614
626
  {
@@ -631,14 +643,18 @@ router.get(
631
643
  class: "form-control form-select",
632
644
  name: "version",
633
645
  },
634
- versions.map((version) =>
635
- option({
636
- id: `${version}_opt`,
637
- value: version,
638
- label: version,
639
- selected: version === selected,
640
- })
641
- )
646
+ versions
647
+ .filter((v) =>
648
+ isVersionSupported(v, pkgInfo.versions, packageJson.version)
649
+ )
650
+ .map((version) =>
651
+ option({
652
+ id: `${version}_opt`,
653
+ value: version,
654
+ label: version,
655
+ selected: version === selected,
656
+ })
657
+ )
642
658
  )
643
659
  ),
644
660
  div(
@@ -1354,7 +1370,17 @@ router.get(
1354
1370
  const { name } = req.params;
1355
1371
 
1356
1372
  const plugin = await Plugin.findOne({ name });
1357
- await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
1373
+ const pkgInfo = await npmFetch.json(
1374
+ `https://registry.npmjs.org/${plugin.location}`
1375
+ );
1376
+ await plugin.upgrade_version(
1377
+ (p, f) => load_plugins.loadPlugin(p, f),
1378
+ supportedVersion(
1379
+ "latest",
1380
+ pkgInfo.versions,
1381
+ require("../package.json").version
1382
+ )
1383
+ );
1358
1384
  req.flash("success", req.__(`Module up-to-date`));
1359
1385
 
1360
1386
  res.redirect(`/plugins/info/${encodeURIComponent(plugin.name)}`);
@@ -1385,11 +1411,12 @@ router.post(
1385
1411
  res.redirect(`/plugins`);
1386
1412
  } else {
1387
1413
  try {
1388
- await load_plugins.loadAndSaveNewPlugin(
1414
+ const msgs = await load_plugins.loadAndSaveNewPlugin(
1389
1415
  plugin,
1390
1416
  schema === db.connectObj.default_schema || plugin.source === "github"
1391
1417
  );
1392
1418
  req.flash("success", req.__(`Module %s installed`, plugin.name));
1419
+ for (const msg of msgs) req.flash("warning", msg);
1393
1420
  res.redirect(`/plugins`);
1394
1421
  } catch (e) {
1395
1422
  req.flash("error", `${e.message}`);
@@ -1488,12 +1515,23 @@ router.post(
1488
1515
  res.redirect(`/plugins`);
1489
1516
  return;
1490
1517
  }
1491
- const msgs = await load_plugins.loadAndSaveNewPlugin(
1492
- plugin,
1493
- forceReInstall,
1494
- undefined,
1495
- req.__
1496
- );
1518
+
1519
+ let msgs = null;
1520
+ try {
1521
+ msgs = await load_plugins.loadAndSaveNewPlugin(
1522
+ plugin,
1523
+ forceReInstall,
1524
+ undefined,
1525
+ req.__
1526
+ );
1527
+ } catch (e) {
1528
+ req.flash(
1529
+ "error",
1530
+ e.message || req.__("Error installing module %s", plugin.name)
1531
+ );
1532
+ res.redirect(`/plugins`);
1533
+ return;
1534
+ }
1497
1535
  const plugin_module = getState().plugins[name];
1498
1536
  await sleep(1000); // Allow other workers to load this plugin
1499
1537
  await getState().refresh_views();
package/routes/search.js CHANGED
@@ -202,7 +202,12 @@ const runSearch = async ({ q, _page, table }, req, res) => {
202
202
  let tablesWithResults = [];
203
203
  let tablesConfigured = 0;
204
204
  for (const [tableName, viewName] of Object.entries(cfg)) {
205
- if (!viewName || viewName === "" || viewName === "search_table_description")
205
+ if (
206
+ !viewName ||
207
+ viewName === "" ||
208
+ viewName === "search_table_description" ||
209
+ tableName === "search_table_description"
210
+ )
206
211
  continue;
207
212
  tablesConfigured += 1;
208
213
  if (table && tableName !== table) continue;
package/routes/tables.js CHANGED
@@ -708,6 +708,7 @@ const attribBadges = (f) => {
708
708
  ].includes(k)
709
709
  )
710
710
  return;
711
+ if(Array.isArray(v) && !v.length) return;
711
712
  if (v || v === 0) s += badge("secondary", k);
712
713
  });
713
714
  }
@@ -1911,7 +1912,7 @@ router.post(
1911
1912
  const table = Table.findOne({ name });
1912
1913
 
1913
1914
  try {
1914
- await table.deleteRows({});
1915
+ await table.deleteRows({}, req.user);
1915
1916
  req.flash("success", req.__("Deleted all rows"));
1916
1917
  } catch (e) {
1917
1918
  req.flash("error", e.message);
@@ -186,7 +186,11 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
186
186
  sublabel: req.__("Display data from this table"),
187
187
  options: tableOptions,
188
188
  disabled: isEdit,
189
- showIf: { viewtemplate: hasTable },
189
+ showIf: isEdit
190
+ ? hasTable.includes(values.viewtemplate)
191
+ ? undefined
192
+ : { nosuchvar: true }
193
+ : { viewtemplate: hasTable },
190
194
  }),
191
195
  new Field({
192
196
  name: "min_role",
@@ -242,7 +246,11 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
242
246
  mapObjectValues(slugOptions, (lvs) => lvs.map((lv) => lv.label)),
243
247
  ],
244
248
  },
245
- showIf: { viewtemplate: hasTable },
249
+ showIf: isEdit
250
+ ? hasTable.includes(values.viewtemplate)
251
+ ? undefined
252
+ : { nosuchvar: true }
253
+ : { viewtemplate: hasTable },
246
254
  }),
247
255
  new Field({
248
256
  name: "no_menu",
package/serve.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @category server
5
5
  * @module serve
6
6
  */
7
- const runScheduler = require("@saltcorn/data/models/scheduler");
7
+ const { runScheduler } = require("@saltcorn/data/models/scheduler");
8
8
  const User = require("@saltcorn/data/models/user");
9
9
  const Plugin = require("@saltcorn/data/models/plugin");
10
10
  const db = require("@saltcorn/data/db");
@@ -346,7 +346,7 @@ describe("JSDOM-E2E edit test", () => {
346
346
  showIfFormula: 'publisher?.name == "AK Press"',
347
347
  });
348
348
  const dom = await load_url_dom("/view/AuthorEditForTest");
349
- await sleep(1000);
349
+ await sleep(2000);
350
350
  const pubwarn = dom.window.document.querySelector("div.pubwarn");
351
351
  //console.log(dom.serialize());
352
352
  expect(pubwarn.style.display).toBe("none");
@@ -362,7 +362,7 @@ describe("JSDOM-E2E edit test", () => {
362
362
  select.value = "1";
363
363
  select.dispatchEvent(newEvent(dom, "change"));
364
364
 
365
- await sleep(1000);
365
+ await sleep(2000);
366
366
  expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
367
367
  "",
368
368
  "Leo Tolstoy",
@@ -381,7 +381,7 @@ describe("JSDOM-E2E edit test", () => {
381
381
  const input = dom.window.document.querySelector("input[name=pages]");
382
382
  input.value = "13";
383
383
  input.dispatchEvent(newEvent(dom, "change"));
384
- await sleep(1000);
384
+ await sleep(2000);
385
385
  const cf = dom.window.document.querySelector(
386
386
  `div[data-source-url="/field/show-calculated/books/pagesp1/show?input_type=text"]`
387
387
  );
@@ -394,7 +394,7 @@ describe("JSDOM-E2E edit test", () => {
394
394
  showIfFormula: "publisher == 1",
395
395
  });
396
396
  const dom = await load_url_dom("/view/AuthorEditForTest1");
397
- await sleep(1000);
397
+ await sleep(2000);
398
398
  const pubwarn = dom.window.document.querySelector("div.pubwarn");
399
399
 
400
400
  expect(pubwarn.style.display).toBe("none");
@@ -410,7 +410,7 @@ describe("JSDOM-E2E edit test", () => {
410
410
  select.value = "1";
411
411
  select.dispatchEvent(newEvent(dom, "change"));
412
412
 
413
- await sleep(1000);
413
+ await sleep(2000);
414
414
  expect([...select_seq.options].map((o) => o.text)).toStrictEqual([
415
415
  "",
416
416
  "Leo Tolstoy",
@@ -30,6 +30,14 @@ const createTestFile = async (folder, name, mimetype, content) => {
30
30
  }
31
31
  };
32
32
 
33
+ const checkFiles = (files, expecteds) =>
34
+ files.length >= expecteds.length &&
35
+ expecteds.every(({ filename, location }) =>
36
+ files.find(
37
+ (file) => file.filename === filename && file.location === location
38
+ )
39
+ );
40
+
33
41
  beforeAll(async () => {
34
42
  await resetToFixtures();
35
43
  await File.ensure_file_store();
@@ -186,13 +194,7 @@ describe("files admin", () => {
186
194
  it("search files by name", async () => {
187
195
  const app = await getApp({ disableCsrf: true });
188
196
  const loginCookie = await getAdminLoginCookie();
189
- const checkFiles = (files, expecteds) =>
190
- files.length >= expecteds.length &&
191
- expecteds.every(({ filename, location }) =>
192
- files.find(
193
- (file) => file.filename === filename && file.location === location
194
- )
195
- );
197
+
196
198
  const searchTestHelper = async (dir, search, expected) => {
197
199
  await request(app)
198
200
  .get("/files")
@@ -345,3 +347,81 @@ describe("files edit", () => {
345
347
  expect(!!file).toBe(true);
346
348
  });
347
349
  });
350
+
351
+ describe("visible_entries test", () => {
352
+ const setRole = async (role, entry) => {
353
+ const app = await getApp({ disableCsrf: true });
354
+ const adminCookie = await getAdminLoginCookie();
355
+ await request(app)
356
+ .post(`/files/setrole/${entry}`)
357
+ .set("Cookie", adminCookie)
358
+ .send(`role=${role}`)
359
+ .expect(toRedirect("/files?dir=_sc_test_subfolder_one"));
360
+ };
361
+
362
+ it("shows allowed files", async () => {
363
+ await setRole(100, path.join("_sc_test_subfolder_one", "foo_image.png"));
364
+
365
+ const app = await getApp({ disableCsrf: true });
366
+ const staffCookie = await getStaffLoginCookie();
367
+ await request(app)
368
+ .get("/files/visible_entries?dir=_sc_test_subfolder_one")
369
+ .set("Cookie", staffCookie)
370
+ .expect(
371
+ respondJsonWith(200, (data) =>
372
+ checkFiles(data.files, [
373
+ {
374
+ filename: "foo_image.png",
375
+ location: path.join("_sc_test_subfolder_one", "foo_image.png"),
376
+ },
377
+ {
378
+ filename: "bar_image.png",
379
+ location: path.join("_sc_test_subfolder_one", "bar_image.png"),
380
+ },
381
+ ])
382
+ )
383
+ );
384
+ });
385
+
386
+ it("shows no disallowed files", async () => {
387
+ await setRole(1, path.join("_sc_test_subfolder_one", "foo_image.png"));
388
+ const app = await getApp({ disableCsrf: true });
389
+ const staffCookie = await getStaffLoginCookie();
390
+ const resp = await request(app)
391
+ .get("/files/visible_entries?dir=_sc_test_subfolder_one")
392
+ .set("Cookie", staffCookie);
393
+ expect(resp.statusCode).toBe(200);
394
+ const files = resp.body.files;
395
+ expect(
396
+ files.find((file) => file.filename === "foo_image.png")
397
+ ).toBeUndefined();
398
+ });
399
+
400
+ it("shows allowed directories", async () => {
401
+ const dir = path.join("_sc_test_subfolder_one", "subsubfolder");
402
+ await setRole(80, dir);
403
+ const app = await getApp({ disableCsrf: true });
404
+ const staffCookie = await getStaffLoginCookie();
405
+ const resp = await request(app)
406
+ .get(`/files/visible_entries?dir=${dir}`)
407
+ .set("Cookie", staffCookie);
408
+ expect(resp.statusCode).toBe(200);
409
+ expect(
410
+ resp.body.directories.find((file) => file.filename === "subsubfolder")
411
+ ).toBeDefined();
412
+ });
413
+
414
+ it("shows no disallowed directories", async () => {
415
+ const dir = path.join("_sc_test_subfolder_one", "subsubfolder");
416
+ await setRole(1, dir);
417
+ const app = await getApp({ disableCsrf: true });
418
+ const staffCookie = await getStaffLoginCookie();
419
+ const resp = await request(app)
420
+ .get(`/files/visible_entries?dir=${dir}`)
421
+ .set("Cookie", staffCookie);
422
+ expect(resp.statusCode).toBe(200);
423
+ const body = resp.body;
424
+ expect(body.files.length).toBe(0);
425
+ expect(body.directories.length).toBe(0);
426
+ });
427
+ });