@saltcorn/server 1.1.1-beta.8 → 1.1.1-rc.2

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.
@@ -13,7 +13,17 @@ const {
13
13
  } = require("@saltcorn/markup");
14
14
  const { get_base_url } = require("./utils.js");
15
15
  const { getState } = require("@saltcorn/data/db/state");
16
- const { h4, p, div, a, i, text, span, nbsp } = require("@saltcorn/markup/tags");
16
+ const {
17
+ h4,
18
+ p,
19
+ div,
20
+ a,
21
+ i,
22
+ text,
23
+ span,
24
+ nbsp,
25
+ button,
26
+ } = require("@saltcorn/markup/tags");
17
27
 
18
28
  /**
19
29
  * @param {string} col
@@ -63,9 +73,11 @@ const tablesList = async (
63
73
  getState().getConfig("min_role_edit_tables", 1) >= req.user.role_id;
64
74
  const tagBadges = (table) => {
65
75
  const myTags = tag_entries.filter((te) => te.table_id === table.id);
66
- return myTags
67
- .map((te) => tagBadge(tagsById[te.tag_id], "tables"))
68
- .join(nbsp);
76
+ const myTagIds = new Set(myTags.map((t) => t.tag_id));
77
+ return (
78
+ myTags.map((te) => tagBadge(tagsById[te.tag_id], "tables")).join(nbsp) +
79
+ mkAddBtn(tags, "tables", table.id, req, myTagIds)
80
+ );
69
81
  };
70
82
 
71
83
  return (
@@ -278,6 +290,38 @@ const tagsDropdown = (tags, altHeader) =>
278
290
  )
279
291
  );
280
292
 
293
+ const mkAddBtn = (tags, entityType, id, req, myTagIds) =>
294
+ div(
295
+ { class: "dropdown d-inline ms-1" },
296
+ span(
297
+ {
298
+ class: "badge bg-secondary add-tag",
299
+ "data-bs-toggle": "dropdown",
300
+ "aria-haspopup": "true",
301
+ "aria-expanded": "false",
302
+ "data-boundary": "viewport",
303
+ },
304
+ i({ class: "fas fa-plus fa-sm" })
305
+ ),
306
+ div(
307
+ {
308
+ class: "dropdown-menu dropdown-menu-end",
309
+ },
310
+
311
+ tags
312
+ .filter((t) => !myTagIds.has(t.id))
313
+ .map((t) =>
314
+ post_dropdown_item(
315
+ `/tag-entries/add-tag-entity/${encodeURIComponent(
316
+ t.name
317
+ )}/${entityType}/${id}`,
318
+ t.name,
319
+ req
320
+ )
321
+ )
322
+ )
323
+ );
324
+
281
325
  const viewsList = async (
282
326
  views,
283
327
  req,
@@ -300,9 +344,12 @@ const viewsList = async (
300
344
 
301
345
  const tagBadges = (view) => {
302
346
  const myTags = tag_entries.filter((te) => te.view_id === view.id);
303
- return myTags
304
- .map((te) => tagBadge(tagsById[te.tag_id], "views"))
305
- .join(nbsp);
347
+ const myTagIds = new Set(myTags.map((t) => t.tag_id));
348
+ const addBtn = mkAddBtn(tags, "views", view.id, req, myTagIds);
349
+ return (
350
+ myTags.map((te) => tagBadge(tagsById[te.tag_id], "views")).join(nbsp) +
351
+ addBtn
352
+ );
306
353
  };
307
354
 
308
355
  return (
@@ -504,9 +551,11 @@ const getPageList = async (
504
551
 
505
552
  const tagBadges = (page) => {
506
553
  const myTags = tag_entries.filter((te) => te.page_id === page.id);
507
- return myTags
508
- .map((te) => tagBadge(tagsById[te.tag_id], "pages"))
509
- .join(nbsp);
554
+ const myTagIds = new Set(myTags.map((t) => t.tag_id));
555
+ return (
556
+ myTags.map((te) => tagBadge(tagsById[te.tag_id], "pages")).join(nbsp) +
557
+ mkAddBtn(tags, "pages", page.id, req, myTagIds)
558
+ );
510
559
  };
511
560
  return mkTable(
512
561
  [
@@ -602,7 +651,7 @@ const getPageGroupList = (rows, roles, req) => {
602
651
  );
603
652
  };
604
653
 
605
- const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
654
+ const trigger_dropdown = (trigger, req, on_done_redirect_str = "") =>
606
655
  settingsDropdown(`dropdownMenuButton${trigger.id}`, [
607
656
  a(
608
657
  {
@@ -643,8 +692,8 @@ const getTriggerList = async (
643
692
  const base_url = get_base_url(req);
644
693
  const tags = await Tag.find();
645
694
  const on_done_redirect_str = on_done_redirect
646
- ? `?on_done_redirect=${on_done_redirect}`
647
- : "";
695
+ ? `?on_done_redirect=${on_done_redirect}`
696
+ : "";
648
697
  const tag_entries = await TagEntry.find({
649
698
  not: { trigger_id: null },
650
699
  });
@@ -657,9 +706,11 @@ const getTriggerList = async (
657
706
 
658
707
  const tagBadges = (trigger) => {
659
708
  const myTags = tag_entries.filter((te) => te.trigger_id === trigger.id);
660
- return myTags
661
- .map((te) => tagBadge(tagsById[te.tag_id], "triggers"))
662
- .join(nbsp);
709
+ const myTagIds = new Set(myTags.map((t) => t.tag_id));
710
+ return (
711
+ myTags.map((te) => tagBadge(tagsById[te.tag_id], "triggers")).join(nbsp) +
712
+ mkAddBtn(tags, "triggers", trigger.id, req, myTagIds)
713
+ );
663
714
  };
664
715
  return mkTable(
665
716
  [
package/routes/fields.js CHANGED
@@ -37,8 +37,8 @@ const {
37
37
  } = require("@saltcorn/data/plugin-helper");
38
38
  const { wizardCardTitle } = require("../markup/forms.js");
39
39
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
40
- const { applyAsync } = require("@saltcorn/data/utils");
41
- const { text } = require("@saltcorn/markup/tags");
40
+ const { applyAsync, isWeb } = require("@saltcorn/data/utils");
41
+ const { text, div } = require("@saltcorn/markup/tags");
42
42
  const { mkFormContentNoLayout } = require("@saltcorn/markup/form");
43
43
 
44
44
  /**
@@ -1301,6 +1301,19 @@ router.post(
1301
1301
  // - disabled inputs do not dispactch click events
1302
1302
  const firefox = true;
1303
1303
  const fv = fieldviews[fieldview];
1304
+ field.fieldview === fieldview;
1305
+ field.fieldviewObj = fv;
1306
+ field.attributes = { ...configuration, ...field.attributes };
1307
+ if (field.type === "Key")
1308
+ await field.fill_fkey_options(
1309
+ false,
1310
+ {},
1311
+ {},
1312
+ undefined,
1313
+ undefined,
1314
+ undefined,
1315
+ req.user
1316
+ );
1304
1317
  if (!fv && field.type === "Key" && fieldview === "select")
1305
1318
  res.send(
1306
1319
  `<input ${
@@ -1422,3 +1435,101 @@ router.post(
1422
1435
  res.send(mkFormContentNoLayout(form));
1423
1436
  })
1424
1437
  );
1438
+
1439
+ router.post(
1440
+ "/edit-get-fieldview",
1441
+ error_catcher(async (req, res) => {
1442
+ const { field_name, table_name, pk, fieldview, configuration } = req.body;
1443
+ const table = Table.findOne({ name: table_name });
1444
+ const row = await table.getRow(
1445
+ { [table.pk_name]: pk },
1446
+ { forUser: req.user, forPublic: !req.user }
1447
+ );
1448
+ const field = table.getField(field_name);
1449
+ let fv;
1450
+ if (field.is_fkey) {
1451
+ await field.fill_fkey_options(
1452
+ false,
1453
+ undefined,
1454
+ undefined,
1455
+ undefined,
1456
+ undefined,
1457
+ row[field_name],
1458
+ req.user
1459
+ );
1460
+ fv = getState().keyFieldviews.select;
1461
+ } else if (fieldview === "subfield" && field.type?.name === "JSON") {
1462
+ fv = field.type.fieldviews.edit_subfield;
1463
+ } else {
1464
+ //TODO: json subfield is special
1465
+ const fieldviews = field.type.fieldviews;
1466
+ fv = Object.values(fieldviews).find((v) => v.isEdit);
1467
+ }
1468
+ res.send(
1469
+ fv.run(
1470
+ field_name,
1471
+ row[field_name],
1472
+ {
1473
+ ...field.attributes,
1474
+ ...configuration,
1475
+ },
1476
+ "",
1477
+ false,
1478
+ field
1479
+ )
1480
+ );
1481
+ })
1482
+ );
1483
+
1484
+ router.post(
1485
+ "/save-click-edit",
1486
+ error_catcher(async (req, res) => {
1487
+ const fielddata = JSON.parse(decodeURIComponent(req.body._fielddata));
1488
+ const { field_name, table_name, pk, fieldview, configuration, join_field } =
1489
+ fielddata;
1490
+ const table = Table.findOne({ name: table_name });
1491
+ const field = table.getField(field_name);
1492
+ let val = field.type?.read
1493
+ ? field.type?.read(req.body[field_name])
1494
+ : req.body[field_name];
1495
+ await table.updateRow({ [field_name]: val }, pk, req.user);
1496
+ let fv;
1497
+ if (field.is_fkey) {
1498
+ if (join_field) {
1499
+ const refTable = Table.findOne({ name: field.reftable_name });
1500
+ const refRow = await refTable.getRow({ [refTable.pk_name]: val });
1501
+ val = refRow[join_field];
1502
+ const targetField = refTable.getField(join_field);
1503
+ const fieldviews = targetField.type.fieldviews;
1504
+
1505
+ fv = fieldviews[fieldview];
1506
+ } else fv = { run: (v) => `${v}` };
1507
+ } else {
1508
+ const fieldviews = field.type.fieldviews;
1509
+
1510
+ fv = fieldviews[fieldview];
1511
+
1512
+ if (!fv) {
1513
+ const fv1 = Object.values(fieldviews).find(
1514
+ (v) => !v.isEdit && !v.isFilter
1515
+ );
1516
+ fv = fv1;
1517
+ }
1518
+ }
1519
+
1520
+ res.send(
1521
+ div(
1522
+ {
1523
+ "data-inline-edit-fielddata": req.body._fielddata,
1524
+ "data-inline-edit-ajax": "true",
1525
+ "data-inline-edit-dest-url": `/api/${table.name}/${pk}`,
1526
+ class: !isWeb(req) ? "mobile-data-inline-edit" : "",
1527
+ },
1528
+ fv.run(val, req, {
1529
+ ...field.attributes,
1530
+ ...configuration,
1531
+ })
1532
+ )
1533
+ );
1534
+ })
1535
+ );
package/routes/list.js CHANGED
@@ -402,25 +402,26 @@ router.get(
402
402
  })
403
403
  })
404
404
  window.tabulator_table = new Tabulator("#jsGrid", {
405
- ajaxURL:"/api/${encodeURIComponent(table.name)}${
406
- table.versioned ? "?versioncount=on" : ""
405
+ ajaxURL:"/api/${encodeURIComponent(
406
+ table.name
407
+ )}?tabulator_pagination_format=true${
408
+ table.versioned ? "&versioncount=on" : ""
407
409
  }",
408
410
  layout:"fitColumns",
409
411
  columns,
410
412
  height:"100%",
411
413
  pagination:true,
412
- paginationSize:20,
414
+ paginationMode:"remote",
415
+ paginationSize:10,
413
416
  clipboard:true,
414
417
  persistence:true,
415
418
  persistenceID:"table_tab_${table.name}",
416
419
  movableColumns: true,
420
+ ajaxContentType:"json",
421
+ sortMode:"remote",
417
422
  initialSort:[
418
423
  {column:"id", dir:"asc"},
419
- ],
420
- ajaxResponse:function(url, params, response){
421
-
422
- return response.success; //return the tableData property of a response json object
423
- },
424
+ ],
424
425
  });
425
426
  window.tabulator_table.on("cellEdited", function(cell){
426
427
  const row = cell.getRow().getData()
package/routes/menu.js CHANGED
@@ -9,7 +9,7 @@ const Router = require("express-promise-router");
9
9
 
10
10
  //const Field = require("@saltcorn/data/models/field");
11
11
  const Form = require("@saltcorn/data/models/form");
12
- const { isAdmin, error_catcher } = require("./utils.js");
12
+ const { isAdmin, error_catcher, isAdminOrHasConfigMinRole } = require("./utils.js");
13
13
  const { getState } = require("@saltcorn/data/db/state");
14
14
  //const File = require("@saltcorn/data/models/file");
15
15
  const User = require("@saltcorn/data/models/user");
@@ -490,7 +490,7 @@ const menuTojQME = (menu_items) =>
490
490
  */
491
491
  router.get(
492
492
  "/",
493
- isAdmin,
493
+ isAdminOrHasConfigMinRole("min_role_edit_menu"),
494
494
  error_catcher(async (req, res) => {
495
495
  const form = await menuForm(req);
496
496
  const state = getState();
@@ -566,7 +566,7 @@ const jQMEtoMenu = (menu_items) =>
566
566
  */
567
567
  router.post(
568
568
  "/",
569
- isAdmin,
569
+ isAdminOrHasConfigMinRole("min_role_edit_menu"),
570
570
  error_catcher(async (req, res) => {
571
571
  const new_menu = req.body;
572
572
  const menu_items = jQMEtoMenu(new_menu);
package/routes/search.js CHANGED
@@ -8,6 +8,8 @@ const Router = require("express-promise-router");
8
8
  const { span, h5, h4, nbsp, p, a, div } = require("@saltcorn/markup/tags");
9
9
 
10
10
  const { getState } = require("@saltcorn/data/db/state");
11
+ const db = require("@saltcorn/data/db");
12
+
11
13
  const { isAdmin, error_catcher } = require("./utils.js");
12
14
  const Form = require("@saltcorn/data/models/form");
13
15
  const Table = require("@saltcorn/data/models/table");
@@ -58,6 +60,13 @@ const searchConfigForm = (tables, views, req) => {
58
60
  sublabel: req.__("Use table description instead of name as header"),
59
61
  type: "Bool",
60
62
  });
63
+ fields.push({
64
+ name: "search_results_decoration",
65
+ label: req.__("Show results in"),
66
+ sublabel: req.__("Show results from each table in this type of element"),
67
+ input_type: "select",
68
+ options: ["Cards", "Tabs"],
69
+ });
61
70
  const blurb1 = req.__(
62
71
  `Choose views for <a href="/search">search results</a> for each table.<br/>Set to blank to omit table from global search.`
63
72
  );
@@ -95,6 +104,10 @@ router.get(
95
104
  "search_table_description",
96
105
  false
97
106
  );
107
+ form.values.search_results_decoration = getState().getConfig(
108
+ "search_results_decoration",
109
+ "Cards"
110
+ );
98
111
  send_infoarch_page({
99
112
  res,
100
113
  req,
@@ -126,13 +139,20 @@ router.post(
126
139
  const result = form.validate(req.body);
127
140
 
128
141
  if (result.success) {
142
+ const dbversion = await db.getVersion(true);
129
143
  const search_table_description =
130
144
  !!result.success.search_table_description;
131
145
  await getState().setConfig(
132
146
  "search_table_description",
133
147
  search_table_description
134
148
  );
149
+ await getState().setConfig(
150
+ "search_results_decoration",
151
+ result.success.search_results_decoration || "Cards"
152
+ );
153
+ await getState().setConfig("search_use_websearch", +dbversion >= 11.0);
135
154
  delete result.success.search_table_description;
155
+ delete result.success.search_results_decoration;
136
156
  await getState().setConfig("globalSearch", result.success);
137
157
  if (!req.xhr) res.redirect("/search/config");
138
158
  else res.json({ success: "ok" });
@@ -196,9 +216,12 @@ const runSearch = async ({ q, _page, table }, req, res) => {
196
216
  "search_table_description",
197
217
  false
198
218
  );
219
+ const search_results_decoration = getState().getConfig(
220
+ "search_results_decoration",
221
+ "Cards"
222
+ );
199
223
  const current_page = parseInt(_page) || 1;
200
224
  const offset = (current_page - 1) * page_size;
201
- let resp = [];
202
225
  let tablesWithResults = [];
203
226
  let tablesConfigured = 0;
204
227
  for (const [tableName, viewName] of Object.entries(cfg)) {
@@ -206,7 +229,9 @@ const runSearch = async ({ q, _page, table }, req, res) => {
206
229
  !viewName ||
207
230
  viewName === "" ||
208
231
  viewName === "search_table_description" ||
209
- tableName === "search_table_description"
232
+ tableName === "search_table_description" ||
233
+ viewName === "search_results_decoration" ||
234
+ tableName === "search_results_decoration"
210
235
  )
211
236
  continue;
212
237
  tablesConfigured += 1;
@@ -238,10 +263,9 @@ const runSearch = async ({ q, _page, table }, req, res) => {
238
263
  }
239
264
 
240
265
  if (vresps.length > 0) {
241
- tablesWithResults.push({ tableName, label: sectionHeader });
242
- resp.push({
243
- type: "card",
244
- title: span({ id: tableName }, sectionHeader),
266
+ tablesWithResults.push({
267
+ tableName,
268
+ label: sectionHeader,
245
269
  contents: vresps.map((vr) => vr.html).join("<hr>") + paginate,
246
270
  });
247
271
  }
@@ -251,17 +275,36 @@ const runSearch = async ({ q, _page, table }, req, res) => {
251
275
  const form = searchForm();
252
276
  form.validate({ q });
253
277
 
278
+ const mkResultDisplay = () => {
279
+ switch (search_results_decoration) {
280
+ case "Tabs":
281
+ const tabContents = {};
282
+ tablesWithResults.forEach((tblRes) => {
283
+ tabContents[tblRes.label] = tblRes.contents;
284
+ });
285
+ return [{ type: "card", tabContents }];
286
+
287
+ default:
288
+ return tablesWithResults.map((tblRes) => ({
289
+ type: "card",
290
+ title: span({ id: tblRes.tableName }, tblRes.label),
291
+ contents: tblRes.contents,
292
+ }));
293
+ }
294
+ };
295
+
254
296
  // Prepare search result visualization
255
297
  const searchResult =
256
- resp.length === 0
298
+ tablesWithResults.length === 0
257
299
  ? [{ type: "card", contents: req.__("Not found") }]
258
- : resp;
300
+ : mkResultDisplay();
259
301
  res.sendWrap(req.__("Search all tables"), {
260
302
  above: [
261
303
  {
262
304
  type: "card",
263
305
  contents: div(
264
306
  renderForm(form, false),
307
+ syntax_help_link(req),
265
308
  typeof table !== "undefined" &&
266
309
  tablesConfigured > 1 &&
267
310
  div(
@@ -296,6 +339,19 @@ const runSearch = async ({ q, _page, table }, req, res) => {
296
339
  });
297
340
  };
298
341
 
342
+ const syntax_help_link = (req) => {
343
+ const use_websearch = getState().getConfig("search_use_websearch", false);
344
+ if (use_websearch)
345
+ return a(
346
+ {
347
+ href: "javascript:void(0);",
348
+ onclick: "ajax_modal('/search/syntax-help')",
349
+ },
350
+ req.__("Search syntax")
351
+ );
352
+ else return "";
353
+ };
354
+
299
355
  /**
300
356
  * Execute search or only show search form
301
357
  * @name get
@@ -327,9 +383,33 @@ router.get(
327
383
  }
328
384
 
329
385
  const form = searchForm();
330
- form.noSubmitButton = false;
331
- form.submitLabel = req.__("Search");
332
- res.sendWrap(req.__("Search all tables"), renderForm(form, false));
386
+ res.sendWrap(req.__("Search all tables"), {
387
+ type: "card",
388
+ contents: renderForm(form, false) + syntax_help_link(req),
389
+ });
333
390
  }
334
391
  })
335
392
  );
393
+
394
+ router.get(
395
+ "/syntax-help",
396
+ error_catcher(async (req, res) => {
397
+ res.sendWrap(
398
+ req.__("Search syntax help"),
399
+ div(
400
+ p(
401
+ `Individual words matched independently. Example <code>large cat</code>`
402
+ ),
403
+ p(
404
+ `Double quotes to match phrase as a single unit. Example <code>"large cat"</code> matches "the large cat sat..." but not "the large brown cat".`
405
+ ),
406
+ p(
407
+ `"or" to match either of two phrases. Example <code>cat or mouse</code>`
408
+ ),
409
+ p(
410
+ `"-" to exclude a word or phrase. Example <code>cat -mouse</code> does not match "cat and mouse"`
411
+ )
412
+ )
413
+ );
414
+ })
415
+ );
package/routes/tables.js CHANGED
@@ -1533,6 +1533,8 @@ router.get(
1533
1533
  key: (r) =>
1534
1534
  r.type === "Unique"
1535
1535
  ? r.configuration.fields.join(", ")
1536
+ : r.type === "Index" && r.configuration?.field === "_fts"
1537
+ ? "Full text search"
1536
1538
  : r.type === "Index"
1537
1539
  ? r.configuration.field
1538
1540
  : r.type === "Formula"
@@ -1629,11 +1631,23 @@ const constraintForm = (req, table, fields, type) => {
1629
1631
  ],
1630
1632
  });
1631
1633
  case "Index":
1634
+ const fieldopts = fields.map((f) => ({ label: f.label, name: f.name }));
1635
+ const hasIncludeFts = fields.filter((f) => f.attributes?.include_fts);
1636
+ if (!db.isSQLite && !hasIncludeFts.length)
1637
+ fieldopts.push({ label: "Full-text search", name: "_fts" });
1632
1638
  return new Form({
1633
1639
  action: `/table/add-constraint/${table.id}/${type}`,
1634
- blurb: req.__(
1635
- "Choose the field to be indexed. This make searching the table faster."
1636
- ),
1640
+ blurb:
1641
+ req.__(
1642
+ "Choose the field to be indexed. This make searching the table faster."
1643
+ ) +
1644
+ " " +
1645
+ (hasIncludeFts.length
1646
+ ? req.__(
1647
+ `Full-text search index is not available as the table contains Key fields (%s) with the "Include in full-text search" option enabled. Disable this before creating a Full-text search index`,
1648
+ hasIncludeFts.map((f) => f.name).join(",")
1649
+ )
1650
+ : ""),
1637
1651
  fields: [
1638
1652
  {
1639
1653
  type: "String",
@@ -1641,7 +1655,7 @@ const constraintForm = (req, table, fields, type) => {
1641
1655
  label: "Field",
1642
1656
  required: true,
1643
1657
  attributes: {
1644
- options: fields.map((f) => ({ label: f.label, name: f.name })),
1658
+ options: fieldopts,
1645
1659
  },
1646
1660
  },
1647
1661
  ],
@@ -12,7 +12,13 @@ const Tag = require("@saltcorn/data/models/tag");
12
12
  const TagEntry = require("@saltcorn/data/models/tag_entry");
13
13
  const Router = require("express-promise-router");
14
14
 
15
- const { isAdmin, error_catcher, csrfField } = require("./utils");
15
+ const {
16
+ isAdmin,
17
+ error_catcher,
18
+ csrfField,
19
+ isAdminOrHasConfigMinRole,
20
+ checkEditPermission,
21
+ } = require("./utils");
16
22
 
17
23
  const Table = require("@saltcorn/data/models/table");
18
24
  const View = require("@saltcorn/data/models/view");
@@ -170,6 +176,42 @@ router.post(
170
176
  })
171
177
  );
172
178
 
179
+ router.post(
180
+ "/add-tag-entity/:tagname/:entitytype/:entityid",
181
+ isAdminOrHasConfigMinRole([
182
+ "min_role_edit_tables",
183
+ "min_role_edit_views",
184
+ "min_role_edit_pages",
185
+ "min_role_edit_triggers",
186
+ ]),
187
+ error_catcher(async (req, res) => {
188
+ const { tagname, entitytype, entityid } = req.params;
189
+ const tag = await Tag.findOne({ name: tagname });
190
+
191
+ const fieldName = idField(entitytype);
192
+ const auth = checkEditPermission(entitytype, req.user);
193
+ if (!auth) req.flash("error", "Not authorized");
194
+ else await tag.addEntry({ [fieldName]: +entityid });
195
+ switch (entitytype) {
196
+ case "views":
197
+ res.redirect(`/viewedit`);
198
+ break;
199
+ case "pages":
200
+ res.redirect(`/pageedit`);
201
+ break;
202
+ case "tables":
203
+ res.redirect(`/table`);
204
+ break;
205
+ case "triggers":
206
+ res.redirect(`/actions`);
207
+ break;
208
+
209
+ default:
210
+ break;
211
+ }
212
+ })
213
+ );
214
+
173
215
  // add one object to multiple tags
174
216
  router.post(
175
217
  "/add/multiple_tags/:entry_type/:object_id",
package/routes/tags.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { a, text, i, div } = require("@saltcorn/markup/tags");
2
2
 
3
3
  const Tag = require("@saltcorn/data/models/tag");
4
+ const TagEntry = require("@saltcorn/data/models/tag_entry");
4
5
  const Router = require("express-promise-router");
5
6
  const Form = require("@saltcorn/data/models/form");
6
7
  const User = require("@saltcorn/data/models/user");
package/routes/utils.js CHANGED
@@ -611,6 +611,20 @@ const getRandomPage = (pageGroup, req) => {
611
611
  return Page.findOne({ id: sessionMember.page_id });
612
612
  };
613
613
 
614
+ const checkEditPermission = (type, user) => {
615
+ if (user.role_id === 1) return true;
616
+ switch (type) {
617
+ case "views":
618
+ return getState().getConfig("min_role_edit_views", 1) >= user.role_id;
619
+ case "pages":
620
+ return getState().getConfig("min_role_edit_pages", 1) >= user.role_id;
621
+ case "triggers":
622
+ return getState().getConfig("min_role_edit_triggers", 1) >= user.role_id;
623
+ default:
624
+ return false;
625
+ }
626
+ };
627
+
614
628
  module.exports = {
615
629
  sqlsanitize,
616
630
  csrfField,
@@ -634,4 +648,5 @@ module.exports = {
634
648
  getEligiblePage,
635
649
  getRandomPage,
636
650
  tenant_letsencrypt_name,
651
+ checkEditPermission,
637
652
  };