@saltcorn/server 0.8.3-alpha.0 → 0.8.3-alpha.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.
package/auth/admin.js CHANGED
@@ -354,6 +354,7 @@ const permissions_settings_form = async (req) =>
354
354
  field_names: [
355
355
  "min_role_upload",
356
356
  "min_role_apikeygen",
357
+ "min_role_search",
357
358
  //hidden "exttables_min_role_read",
358
359
  ],
359
360
  action: "/useradmin/permissions",
package/auth/routes.js CHANGED
@@ -1129,7 +1129,6 @@ router.post(
1129
1129
  error_catcher(async (req, res, next) => {
1130
1130
  const { method } = req.params;
1131
1131
  const auth = getState().auth_methods[method];
1132
- console.log(method, auth);
1133
1132
  if (auth) {
1134
1133
  passport.authenticate(method, auth.parameters)(
1135
1134
  req,
package/auth/testhelp.js CHANGED
@@ -65,6 +65,29 @@ const toSucceed =
65
65
  }
66
66
  };
67
67
 
68
+ /**
69
+ *
70
+ * @param {number} expCode
71
+ * @returns {void}
72
+ * @throws {Error}
73
+ */
74
+ const toSucceedWithImage =
75
+ ({ expCode = 200, lengthIs }) =>
76
+ (res) => {
77
+ if (res.statusCode !== expCode) {
78
+ console.log(res.text);
79
+ throw new Error(`Expected status ${expCode}, received ${res.statusCode}`);
80
+ }
81
+ if (res.type.split("/")[0] !== "image") {
82
+ throw new Error(`Expected response type image/*, received ${res.type}`);
83
+ }
84
+ if (lengthIs && !lengthIs(res.body.length)) {
85
+ throw new Error(
86
+ `Image response not accepted. Length not satisfied. Received ${res.body.length} bytes`
87
+ );
88
+ }
89
+ };
90
+
68
91
  /**
69
92
  *
70
93
  * @param {number} txt
@@ -209,4 +232,5 @@ module.exports = {
209
232
  succeedJsonWith,
210
233
  notAuthorized,
211
234
  respondJsonWith,
235
+ toSucceedWithImage,
212
236
  };
package/load_plugins.js CHANGED
@@ -56,13 +56,17 @@ const defaultManager = new PluginManager({
56
56
  * @param force - force flag
57
57
  */
58
58
  const loadPlugin = async (plugin, force) => {
59
- // load pluging
59
+ // load plugin
60
60
  const res = await requirePlugin(plugin, force);
61
+ const configuration =
62
+ typeof plugin.configuration === "string"
63
+ ? JSON.parse(plugin.configuration)
64
+ : plugin.configuration;
61
65
  // register plugin
62
66
  getState().registerPlugin(
63
67
  res.plugin_module.plugin_name || plugin.name,
64
68
  res.plugin_module,
65
- plugin.configuration,
69
+ configuration,
66
70
  res.location,
67
71
  res.name
68
72
  );
package/locales/da.json CHANGED
@@ -558,5 +558,126 @@
558
558
  "List options": "List options",
559
559
  "Modules": "Modules",
560
560
  "File not found": "File not found",
561
- "Welcome, %s!": "Welcome, %s!"
561
+ "Welcome, %s!": "Welcome, %s!",
562
+ "CSV upload": "CSV upload",
563
+ "Upload file(s)": "Upload file(s)",
564
+ "API token": "API token",
565
+ "No API token issued": "No API token issued",
566
+ "Generate": "Generate",
567
+ "Two-factor authentication": "Two-factor authentication",
568
+ "Two-factor authentication is disabled": "Two-factor authentication is disabled",
569
+ "Enable TWA": "Enable TWA",
570
+ "Pattern": "Pattern",
571
+ "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.": "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.",
572
+ "Views potentially affected": "Views potentially affected",
573
+ "History": "History",
574
+ "External": "External",
575
+ "Discover tables that are already in the Database, but not known to Saltcorn": "Discover tables that are already in the Database, but not known to Saltcorn",
576
+ "Forget table": "Forget table",
577
+ "Ownership field": "Ownership field",
578
+ "The user referred to in this field will be the owner of the row": "The user referred to in this field will be the owner of the row",
579
+ "None": "None",
580
+ "Ownership formula": "Ownership formula",
581
+ "User is treated as owner if true. In scope: ": "User is treated as owner if true. In scope: ",
582
+ "User group": "User group",
583
+ "Add relations to this table in dropdown options for ownership field": "Add relations to this table in dropdown options for ownership field",
584
+ "User must have this role or higher to read rows from the table, unless they are the owner": "User must have this role or higher to read rows from the table, unless they are the owner",
585
+ "User must have this role or higher to edit or create new rows in the table, unless they are the owner": "User must have this role or higher to edit or create new rows in the table, unless they are the owner",
586
+ "Edit properties": "Edit properties",
587
+ "Preset %s": "Preset %s",
588
+ "%s configuration": "%s configuration",
589
+ "Module Store endpoint": "Module Store endpoint",
590
+ "The endpoint of plugins store.": "The endpoint of plugins store.",
591
+ "Packs Store endpoint": "Packs Store endpoint",
592
+ "The endpoint of packs store.": "The endpoint of packs store.",
593
+ "Mobile app": "Mobile app",
594
+ "Backup now": "Backup now",
595
+ "Frequency": "Frequency",
596
+ "Destination": "Destination",
597
+ "Directory": "Directory",
598
+ "Expiration in days": "Expiration in days",
599
+ "Delete old backup files in this directory after the set number of days": "Delete old backup files in this directory after the set number of days",
600
+ "Snapshot now": "Snapshot now",
601
+ "Periodic snapshots enabled": "Periodic snapshots enabled",
602
+ "Snapshot will be made every hour if there are changes": "Snapshot will be made every hour if there are changes",
603
+ "Manual backup": "Manual backup",
604
+ "Automated backup": "Automated backup",
605
+ "Restore/download automated backups »": "Restore/download automated backups »",
606
+ "Snapshots": "Snapshots",
607
+ "Snapshots store your application structure and definition, without the table data. Individual views and pages can be restored from snapshots from the <a href='/viewedit'>view</a> or <a href='/pageedit'>pages</a> overviews (\"Restore\" from individual page or view dropdowns).": "Snapshots store your application structure and definition, without the table data. Individual views and pages can be restored from snapshots from the <a href='/viewedit'>view</a> or <a href='/pageedit'>pages</a> overviews (\"Restore\" from individual page or view dropdowns).",
608
+ "List/download snapshots &raquo;": "List/download snapshots &raquo;",
609
+ "Configuration check": "Configuration check",
610
+ "Check for updates": "Check for updates",
611
+ "Database type": "Database type",
612
+ "Database host": "Database host",
613
+ "Database port": "Database port",
614
+ "Database name": "Database name",
615
+ "Database user": "Database user",
616
+ "Database schema": "Database schema",
617
+ "Build mobile app": "Build mobile app",
618
+ "Entry point": "Entry point",
619
+ "Platform": "Platform",
620
+ "docker": "docker",
621
+ "android": "android",
622
+ "iOS": "iOS",
623
+ "App file": "App file",
624
+ "Server URL": "Server URL",
625
+ "Log client errors": "Log client errors",
626
+ "Record all client errors in the crash log": "Record all client errors in the crash log",
627
+ "System logging verbosity": "System logging verbosity",
628
+ "NPM packages in code": "NPM packages in code",
629
+ "Comma-separated list of packages which will be available in JavaScript actions": "Comma-separated list of packages which will be available in JavaScript actions",
630
+ "Development settings": "Development settings",
631
+ "Module store": "Module store",
632
+ "Upgrading modules...": "Upgrading modules...",
633
+ "Upgrade installed modules": "Upgrade installed modules",
634
+ "Add another module": "Add another module",
635
+ "Module": "Module",
636
+ "Become user": "Become user",
637
+ "Send password reset email": "Send password reset email",
638
+ "Login and Signup": "Login and Signup",
639
+ "Table access": "Table access",
640
+ "HTTP": "HTTP",
641
+ "Permissions": "Permissions",
642
+ "2FA policy": "2FA policy",
643
+ "Cookie duration (hours)": "Cookie duration (hours)",
644
+ "Set to 0 for expiration at the end of browser session": "Set to 0 for expiration at the end of browser session",
645
+ "Cookie duration (hours) when remember ticked": "Cookie duration (hours) when remember ticked",
646
+ "Public cache TTL (minutes)": "Public cache TTL (minutes)",
647
+ "Cache-control max-age for public views and pages. 0 to disable": "Cache-control max-age for public views and pages. 0 to disable",
648
+ "HTTP settings": "HTTP settings",
649
+ "Role to generate API keys": "Role to generate API keys",
650
+ "User should have this role or higher to generate API keys in their user settings": "User should have this role or higher to generate API keys in their user settings",
651
+ "Role for search": "Role for search",
652
+ "Min role to access search page": "Min role to access search page",
653
+ "Permissions settings": "Permissions settings",
654
+ "Update": "Update",
655
+ "Add": "Add",
656
+ "Recalculate dynamic": "Recalculate dynamic",
657
+ "Order field": "Order field",
658
+ "Section field": "Section field",
659
+ "Optional. String type with options, each of which will become a menu section": "Optional. String type with options, each of which will become a menu section",
660
+ "Label formula": "Label formula",
661
+ "URL formula": "URL formula",
662
+ "Include formula": "Include formula",
663
+ "If specified, only include in menu rows that evaluate to true": "If specified, only include in menu rows that evaluate to true",
664
+ "Location": "Location",
665
+ "Not all themes support all locations": "Not all themes support all locations",
666
+ "Library": "Library",
667
+ "Tags": "Tags",
668
+ "Diagram": "Diagram",
669
+ "Library: component assemblies that can be used in the builder": "Library: component assemblies that can be used in the builder",
670
+ "Creator email": "Creator email",
671
+ "Created": "Created",
672
+ "Create tenant warning text": "Create tenant warning text",
673
+ "Provide your own create warning text if need": "Provide your own create warning text if need",
674
+ "New tenant template": "New tenant template",
675
+ "Copy site structure for new tenants from this tenant": "Copy site structure for new tenants from this tenant",
676
+ "Tagname": "Tagname",
677
+ "Create tag": "Create tag",
678
+ "Application diagram": "Application diagram",
679
+ "Trigger": "Trigger",
680
+ "All entities": "All entities",
681
+ "no tags": "no tags",
682
+ "Add tag": "Add tag"
562
683
  }
package/locales/en.json CHANGED
@@ -1085,5 +1085,13 @@
1085
1085
  "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.": "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.",
1086
1086
  "Views potentially affected": "Views potentially affected",
1087
1087
  "Empty view": "Empty view",
1088
- "A view that will be shown only if there are no tables rows to show": "A view that will be shown only if there are no tables rows to show"
1088
+ "A view that will be shown only if there are no tables rows to show": "A view that will be shown only if there are no tables rows to show",
1089
+ "Calculated field will be stored in Database": "Calculated field will be stored in Database",
1090
+ "Set Email": "Set Email",
1091
+ "Please enter your email address": "Please enter your email address",
1092
+ "Role for search": "Role for search",
1093
+ "Min role to access search page": "Min role to access search page",
1094
+ "Update": "Update",
1095
+ "Add": "Add",
1096
+ "Recalculate dynamic": "Recalculate dynamic"
1089
1097
  }
package/locales/ru.json CHANGED
@@ -731,7 +731,7 @@
731
731
  "Forget table": "Забыть таблицу",
732
732
  "Ownership formula": "Owner (формула)",
733
733
  "User is treated as owner if true. In scope: ": "Ownership formula. Пользователь воспринимается как владелец, если значение формулы true. В рамках: ",
734
- "This is a translation of a different field in a different language": "Это трансляция перевода полей на разные языки",
734
+ "This is a translation of a different field in a different language": "Это поле является переводом другого поля на другой язык",
735
735
  "Language locale of translation": "Языковая локализация (locale) для перевода",
736
736
  "Slug": "Slug",
737
737
  "Field that can be used for a prettier URL structure": "Поле, которе используется для настройки структуры URL",
@@ -933,21 +933,45 @@
933
933
  "%s view - %s on %s": "%s view - %s on %s",
934
934
  "Password Repeat": "Повторите пароль",
935
935
  "Remember me": "Запомнить меня",
936
- "Specifies a filter for what file types the user can pick from the file input dialog box. Example is `text/csv,audio/*,video/*,image/*`": "Specifies a filter for what file types the user can pick from the file input dialog box. Example is `text/csv,audio/*,video/*,image/*`",
936
+ "Specifies a filter for what file types the user can pick from the file input dialog box. Example is `text/csv,audio/*,video/*,image/*`": "Определяет через запятую список MIME типов файлов, которые допустимо загружать через диалог загрузки файлов. Пример `text/csv,audio/*,video/*,image/*`",
937
937
  "Home Page by Role": "Домашняя страница в соответствие с ролью",
938
938
  "Files accept filter": "Фильтр типов файлов при загрузке",
939
939
  "Specify how to create a new row": "Укажите как создавать новую строку",
940
940
  "Cascade delete to file": "Каскадное удаление к файлу",
941
- "Deleting a row will also delete the file referenced by this field": "Deleting a row will also delete the file referenced by this field",
942
- "Specifies a filter for what file types the user can pick from the file input dialog box. Example is `.doc,audio/*,video/*,image/*`": "Specifies a filter for what file types the user can pick from the file input dialog box. Example is `.doc,audio/*,video/*,image/*`",
943
- "Specifies a default filter for what file types the user can pick from the file input dialog box. Example is `.doc, text/csv,audio/*,video/*,image/*`": "Specifies a default filter for what file types the user can pick from the file input dialog box. Example is `.doc, text/csv,audio/*,video/*,image/*`",
941
+ "Deleting a row will also delete the file referenced by this field": "При удалении записи также удаляется файл, на который ссылается поле этой записи",
942
+ "Specifies a filter for what file types the user can pick from the file input dialog box. Example is `.doc,audio/*,video/*,image/*`": "Определяет через запятую список MIME типов файлов, которые допустимо загружать через диалог загрузки файлов. Пример `.doc,audio/*,video/*,image/*`",
943
+ "Specifies a default filter for what file types the user can pick from the file input dialog box. Example is `.doc, text/csv,audio/*,video/*,image/*`": "Определяет через запятую список MIME типов файлов, которые допустимо загружать через диалог загрузки файлов. Пример `.doc, text/csv,audio/*,video/*,image/*`",
944
944
  "No file found": "Файл не найден",
945
945
  "Default File accept filter": "Фильтр типов файлов для загрузки",
946
946
  "File upload debug": "Флаг отладки загрузки файлов",
947
- "Turn on to debug file upload in express-fileupload.": "Turn on to debug file upload in express-fileupload.",
947
+ "Turn on to debug file upload in express-fileupload.": "Включить отладочные сообщения при загрузке файлов.",
948
948
  "File upload size limit in bytes": "Ограничение на размер файла при загрузке (в байтах)",
949
- "Defines in bytes limit for upload files in express-fileupload.": "Defines in bytes limit for upload files in express-fileupload.",
949
+ "Defines in bytes limit for upload files in express-fileupload.": "Максимальный допустимый размер загружаемого файла (указывается в байтах).",
950
950
  "File upload timeout": "Таймаут загрузки файлов",
951
- "Defines how long to wait for data before aborting for express-fileupload. Set to 0 if you want to turn off timeout checks. ": "Defines how long to wait for data before aborting for express-fileupload. Set to 0 if you want to turn off timeout checks. ",
952
- "Files settings": "Настройки Файлов"
951
+ "Defines how long to wait for data before aborting for express-fileupload. Set to 0 if you want to turn off timeout checks. ": "определяет максимально допустимый интервал времени ожидания загрузки файла. 0 отключает проверку. ",
952
+ "Files settings": "Настройки Файлов",
953
+ "Log client errors": "Логировать ошибки на клиенте",
954
+ "Record all client errors in the crash log": "Записывать все ошибки в приложении-клиенте в журнал сбоев (crash log)",
955
+ "NPM packages in code": "Пакеты NPM для JS кода",
956
+ "Comma-separated list of packages which will be available in JavaScript actions": "Список пакетов через запятую, которые будут использованы в действиях Javascript",
957
+ "Owned": "Принадлежит",
958
+ "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.": "Настроены представления с уровнем доступа меньшим, чем уровень чтения данных из таблиц. При этом не используется механизм владения таблицей. Это может вызвать конфликт доступа. Пользователи должны иметь доступ ко всем отображаемым данным.",
959
+ "Views potentially affected": "Потенциально затронутые представления",
960
+ "Become user": "Стать пользователем",
961
+ "Public cache TTL (minutes)": "Публичный кеш TTL (минуты)",
962
+ "Cache-control max-age for public views and pages. 0 to disable": "Максимальный возраст кеша для публичных представлений и страниц. 0 отключает проверку",
963
+ "Update": "Обновить",
964
+ "Add": "Добавить",
965
+ "Recalculate dynamic": "Пересчитать динамические элементы",
966
+ "Upload size limit (Kb)": "Лимит размера загружаемых файлов (Kb)",
967
+ "Maximum upload file size in kilobytes": "Максимальный допустимый размер загружаемого файла (указывается в килобайтах)",
968
+ "Defines how long to wait for data before aborting file upload. Set to 0 if you want to turn off timeout checks. ": "Определяет максимальный допустимый интервал времени ожидания загрузки данных файла. 0 отключает проверку. ",
969
+ "User group": "Группа пользователей",
970
+ "Add relations to this table in dropdown options for ownership field": "Добавить связь с этой таблицей в выбор опций для поля Владельца ",
971
+ "Calculated field will be stored in Database": "Вычисляемое будет сохранено в базе данных",
972
+ "Save indicator": "Индикатор сохранения",
973
+ "%s configuration": "%s конфигурация",
974
+ "Show only matches in table:": "Показывать только совпадения в таблице:",
975
+ "Role for search": "Роль для поиска",
976
+ "Min role to access search page": "Минимальная роль для доступа к странице поиска"
953
977
  }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.3-alpha.0",
3
+ "version": "0.8.3-alpha.2",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@saltcorn/base-plugin": "0.8.3-alpha.0",
10
- "@saltcorn/builder": "0.8.3-alpha.0",
11
- "@saltcorn/data": "0.8.3-alpha.0",
12
- "@saltcorn/admin-models": "0.8.3-alpha.0",
13
- "@saltcorn/filemanager": "0.8.3-alpha.0",
14
- "@saltcorn/markup": "0.8.3-alpha.0",
15
- "@saltcorn/sbadmin2": "0.8.3-alpha.0",
9
+ "@saltcorn/base-plugin": "0.8.3-alpha.2",
10
+ "@saltcorn/builder": "0.8.3-alpha.2",
11
+ "@saltcorn/data": "0.8.3-alpha.2",
12
+ "@saltcorn/admin-models": "0.8.3-alpha.2",
13
+ "@saltcorn/filemanager": "0.8.3-alpha.2",
14
+ "@saltcorn/markup": "0.8.3-alpha.2",
15
+ "@saltcorn/sbadmin2": "0.8.3-alpha.2",
16
16
  "@socket.io/cluster-adapter": "^0.1.0",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "aws-sdk": "^2.1037.0",
@@ -35,10 +35,10 @@
35
35
  "i18n": "^0.14.0",
36
36
  "jsonwebtoken": "^9.0.0",
37
37
  "live-plugin-manager": "^0.16.0",
38
- "moment": "^2.27.0",
39
- "multer": "^1.4.3",
38
+ "moment": "^2.29.4",
39
+ "multer": "1.4.5-lts.1",
40
40
  "multer-s3": "^2.10.0",
41
- "node-fetch": "2.6.2",
41
+ "node-fetch": "2.6.9",
42
42
  "node-watch": "^0.7.2",
43
43
  "notp": "2.0.3",
44
44
  "passport": "^0.4.1",
@@ -49,7 +49,7 @@
49
49
  "pg": "^8.2.1",
50
50
  "pluralize": "^8.0.0",
51
51
  "qrcode": "1.5.0",
52
- "resize-with-sharp-or-jimp": "0.1.5",
52
+ "resize-with-sharp-or-jimp": "0.1.6",
53
53
  "socket.io": "4.2.0",
54
54
  "thirty-two": "1.0.2",
55
55
  "tmp-promise": "^3.0.2",
@@ -64,7 +64,7 @@
64
64
  "devDependencies": {
65
65
  "jest": "26.6.3",
66
66
  "jest-environment-jsdom": "^26.6.2",
67
- "supertest": "^4.0.2"
67
+ "supertest": "^6.3.3"
68
68
  },
69
69
  "scripts": {
70
70
  "dev": "nodemon index.js",
package/routes/api.js CHANGED
@@ -147,7 +147,7 @@ router.post(
147
147
  (await view.authorise_get({ req, ...view })) // TODO set query to state
148
148
  ) {
149
149
  const queries = view.queries(false, req);
150
- if (queries[queryName]) {
150
+ if (Object.prototype.hasOwnProperty.call(queries, queryName)) {
151
151
  const { args } = req.body;
152
152
  const resp = await queries[queryName](...args, true);
153
153
  res.json({ success: resp, alerts: getFlashes(req) });
package/routes/edit.js CHANGED
@@ -32,13 +32,18 @@ router.post(
32
32
  const { redirect } = req.query;
33
33
  // todo check that works after where change
34
34
  const table = await Table.findOne({ name: tableName });
35
- const role = req.user && req.user.id ? req.user.role_id : 10;
36
- if (role <= table.min_role_write) await table.toggleBool(+id, field_name);
37
- else
38
- req.flash(
39
- "error",
40
- req.__("Not allowed to write to table %s", table.name)
35
+
36
+ const row = await table.getRow(
37
+ { [table.pk_name]: id },
38
+ { forUser: req.user, forPublic: !req.user }
39
+ );
40
+ if (row)
41
+ await table.updateRow(
42
+ { [field_name]: !row[field_name] },
43
+ id,
44
+ req.user || { role_id: 10 }
41
45
  );
46
+
42
47
  if (req.xhr) res.send("OK");
43
48
  else if (req.get("referer")) res.redirect(req.get("referer"));
44
49
  else res.redirect(redirect || `/list/${table.name}`);
package/routes/fields.js CHANGED
@@ -137,6 +137,7 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
137
137
  new Field({
138
138
  label: req.__("Stored"),
139
139
  name: "stored",
140
+ sublabel: req.__("Calculated field will be stored in Database"),
140
141
  type: "Bool",
141
142
  disabled: !!id,
142
143
  showIf: { calculated: true },
@@ -924,16 +925,25 @@ router.post(
924
925
  res.send("");
925
926
  return;
926
927
  }
928
+ const firefox = /firefox/i.test(req.headers["user-agent"]);
927
929
  const fv = fieldviews[fieldview];
928
930
  if (!fv && field.type === "Key" && fieldview === "select")
929
- res.send(`<input readonly class="form-control form-select"></input>`);
931
+ res.send(
932
+ `<input ${
933
+ firefox ? "readonly" : "disabled"
934
+ } class="form-control form-select"></input>`
935
+ );
930
936
  else if (!fv) res.send("");
931
937
  else if (fv.isEdit || fv.isFilter)
932
938
  res.send(
933
939
  fv.run(
934
940
  field.name,
935
941
  undefined,
936
- { readonly: true, ...configuration, ...(field.attributes || {}) },
942
+ {
943
+ ...(firefox ? { readonly: true } : { disabled: true }),
944
+ ...configuration,
945
+ ...(field.attributes || {}),
946
+ },
937
947
  "",
938
948
  false,
939
949
  field
package/routes/menu.js CHANGED
@@ -83,10 +83,10 @@ const menuForm = async (req) => {
83
83
  labelCols: 3,
84
84
  noSubmitButton: true,
85
85
  additionalButtons: [
86
- { label: "Update", id: "btnUpdate", class: "btn btn-primary" },
87
- { label: "Add", id: "btnAdd", class: "btn btn-primary" },
86
+ { label: req.__("Update"), id: "btnUpdate", class: "btn btn-primary" },
87
+ { label: req.__("Add"), id: "btnAdd", class: "btn btn-primary" },
88
88
  {
89
- label: "Recalculate dynamic",
89
+ label: req.__("Recalculate dynamic"),
90
90
  id: "btnRecalc",
91
91
  class: "btn btn-primary",
92
92
  },
package/routes/search.js CHANGED
@@ -28,16 +28,17 @@ const router = new Router();
28
28
  module.exports = router;
29
29
 
30
30
  /**
31
+ * Search Configuration form
31
32
  * @param {object[]} tables
32
33
  * @param {object[]} views
33
34
  * @param {object} req
34
35
  * @returns {Forms}
35
36
  */
36
37
  const searchConfigForm = (tables, views, req) => {
37
- var fields = [];
38
- var tbls_noviews = [];
38
+ let fields = [];
39
+ let tbls_noviews = [];
39
40
  for (const t of tables) {
40
- var ok_views = views.filter(
41
+ const ok_views = views.filter(
41
42
  (v) =>
42
43
  v.table_id === t.id && v.viewtemplateObj && v.viewtemplateObj.runMany
43
44
  );
@@ -70,6 +71,7 @@ const searchConfigForm = (tables, views, req) => {
70
71
  };
71
72
 
72
73
  /**
74
+ * Show config on GET
73
75
  * @name get/config
74
76
  * @function
75
77
  * @memberof module:routes/search~searchRouter
@@ -79,7 +81,7 @@ router.get(
79
81
  "/config",
80
82
  isAdmin,
81
83
  error_catcher(async (req, res) => {
82
- var views = await View.find({}, { orderBy: "name" });
84
+ const views = await View.find({}, { orderBy: "name" });
83
85
  const tables = await Table.find();
84
86
  const form = searchConfigForm(tables, views, req);
85
87
  form.values = getState().getConfig("globalSearch");
@@ -98,6 +100,7 @@ router.get(
98
100
  );
99
101
 
100
102
  /**
103
+ * Execute config update
101
104
  * @name post/config
102
105
  * @function
103
106
  * @memberof module:routes/search~searchRouter
@@ -107,7 +110,7 @@ router.post(
107
110
  "/config",
108
111
  isAdmin,
109
112
  error_catcher(async (req, res) => {
110
- var views = await View.find({}, { orderBy: "name" });
113
+ const views = await View.find({}, { orderBy: "name" });
111
114
  const tables = await Table.find();
112
115
  const form = searchConfigForm(tables, views, req);
113
116
  const result = form.validate(req.body);
@@ -132,6 +135,7 @@ router.post(
132
135
  );
133
136
 
134
137
  /**
138
+ * Search form
135
139
  * @returns {Form}
136
140
  */
137
141
  const searchForm = () =>
@@ -150,6 +154,7 @@ const searchForm = () =>
150
154
  });
151
155
 
152
156
  /**
157
+ * Run search
153
158
  * @param {object} opts
154
159
  * @param {*} opts.q
155
160
  * @param {*} opts._page
@@ -161,7 +166,9 @@ const searchForm = () =>
161
166
  */
162
167
  const runSearch = async ({ q, _page, table }, req, res) => {
163
168
  const role = (req.user || {}).role_id || 10;
169
+ // globalSearch contains list of pairs: table, view
164
170
  const cfg = getState().getConfig("globalSearch");
171
+ const page_size = getState().getConfig("search_page_size");
165
172
 
166
173
  if (!cfg) {
167
174
  req.flash("warning", req.__("Search not configured"));
@@ -169,7 +176,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
169
176
  return;
170
177
  }
171
178
  const current_page = parseInt(_page) || 1;
172
- const offset = (current_page - 1) * 20;
179
+ const offset = (current_page - 1) * page_size;
173
180
  let resp = [];
174
181
  let tablesWithResults = [];
175
182
  let tablesConfigured = 0;
@@ -182,18 +189,19 @@ const runSearch = async ({ q, _page, table }, req, res) => {
182
189
  throw new InvalidConfiguration(
183
190
  `View ${viewName} selected as search results for ${tableName}: view not found`
184
191
  );
192
+ // search table using view
185
193
  const vresps = await view.runMany(
186
194
  { _fts: q },
187
- { res, req, limit: 20, offset }
195
+ { res, req, limit: page_size, offset }
188
196
  );
189
197
  let paginate = "";
190
- if (vresps.length === 20 || current_page > 1) {
198
+ if (vresps.length === page_size || current_page > 1) {
191
199
  paginate = pagination({
192
200
  current_page,
193
- pages: current_page + (vresps.length === 20 ? 1 : 0),
194
- trailing_ellipsis: vresps.length === 20,
201
+ pages: current_page + (vresps.length === page_size ? 1 : 0),
202
+ trailing_ellipsis: vresps.length === page_size,
195
203
  get_page_link: (n) =>
196
- `javascript:gopage(${n}, 20, undefined, {table:'${tableName}'})`,
204
+ `javascript:gopage(${n}, ${page_size}, undefined, {table:'${tableName}'})`,
197
205
  });
198
206
  }
199
207
 
@@ -207,9 +215,11 @@ const runSearch = async ({ q, _page, table }, req, res) => {
207
215
  }
208
216
  }
209
217
 
218
+ // Prepare search form
210
219
  const form = searchForm();
211
220
  form.validate({ q });
212
221
 
222
+ // Prepare search result visualization
213
223
  const searchResult =
214
224
  resp.length === 0
215
225
  ? [{ type: "card", contents: req.__("Not found") }]
@@ -250,6 +260,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
250
260
  };
251
261
 
252
262
  /**
263
+ * Execute search or only show search form
253
264
  * @name get
254
265
  * @function
255
266
  * @memberof module:routes/search~searchRouter
@@ -258,6 +269,14 @@ const runSearch = async ({ q, _page, table }, req, res) => {
258
269
  router.get(
259
270
  "/",
260
271
  error_catcher(async (req, res) => {
272
+
273
+ const min_role = getState().getConfig("min_role_search");
274
+ const role = (req.user || {}).role_id || 10;
275
+ if(role>min_role){
276
+ res.redirect("/"); // silent redirect to home page
277
+ return;
278
+ }
279
+
261
280
  if (req.query && req.query.q) {
262
281
  await runSearch(req.query, req, res);
263
282
  } else {
package/routes/tables.js CHANGED
@@ -430,6 +430,7 @@ router.get(
430
430
  label: `<b>${t.name}</b>\n${t.fields
431
431
  .map((f) => `${f.name} : ${f.pretty_type}`)
432
432
  .join("\n")}`,
433
+ title: t.description? t.description : t.name,
433
434
  })),
434
435
  edges,
435
436
  };
@@ -439,7 +440,7 @@ router.get(
439
440
  headers: [
440
441
  {
441
442
  script:
442
- "https://unpkg.com/vis-network@9.0.2/standalone/umd/vis-network.min.js",
443
+ "https://unpkg.com/vis-network@9.1.2/standalone/umd/vis-network.min.js",
443
444
  },
444
445
  ],
445
446
  },
Binary file
@@ -9,9 +9,11 @@ const {
9
9
  toSucceed,
10
10
  toNotInclude,
11
11
  resetToFixtures,
12
+ toSucceedWithImage,
12
13
  } = require("../auth/testhelp");
13
14
  const db = require("@saltcorn/data/db");
14
15
  const fs = require("fs").promises;
16
+ const path = require("path");
15
17
  const File = require("@saltcorn/data/models/file");
16
18
  const Field = require("@saltcorn/data/models/field");
17
19
  const Table = require("@saltcorn/data/models/table");
@@ -20,14 +22,31 @@ const { table } = require("console");
20
22
 
21
23
  beforeAll(async () => {
22
24
  await resetToFixtures();
23
- const mv = async (fnm) => {
24
- await fs.writeFile(fnm, "nevergonnagiveyouup");
25
- };
25
+ await File.ensure_file_store();
26
26
  await File.from_req_files(
27
- { mimetype: "image/png", name: "rick.png", mv, size: 245752 },
27
+ {
28
+ mimetype: "image/png",
29
+ name: "rick.png",
30
+ mv: async (fnm) => {
31
+ await fs.writeFile(fnm, "nevergonnagiveyouup");
32
+ },
33
+ size: 245752,
34
+ },
28
35
  1,
29
36
  4
30
37
  );
38
+ await File.from_req_files(
39
+ {
40
+ mimetype: "image/png",
41
+ name: "large-image.png",
42
+ mv: async (fnm) => {
43
+ await fs.copyFile(path.join(__dirname, "assets/large-image.png"), fnm);
44
+ },
45
+ size: 219422,
46
+ },
47
+ 1,
48
+ 10
49
+ );
31
50
  });
32
51
  afterAll(db.close);
33
52
 
@@ -70,6 +89,28 @@ describe("files admin", () => {
70
89
  const app = await getApp({ disableCsrf: true });
71
90
  await request(app).get("/files/serve/rick.png").expect(404);
72
91
  });
92
+ it("serve public file", async () => {
93
+ const app = await getApp({ disableCsrf: true });
94
+ await request(app)
95
+ .get("/files/serve/large-image.png")
96
+ .expect(toSucceedWithImage({ lengthIs: (bs) => bs === 219422 }));
97
+ });
98
+ it("serve resized file", async () => {
99
+ const app = await getApp({ disableCsrf: true });
100
+ await request(app)
101
+ .get("/files/resize/200/100/large-image.png")
102
+ .expect(
103
+ toSucceedWithImage({ lengthIs: (bs) => bs < 100000 && bs > 2000 })
104
+ );
105
+ });
106
+ it("serve resized file without height", async () => {
107
+ const app = await getApp({ disableCsrf: true });
108
+ await request(app)
109
+ .get("/files/resize/200/0/large-image.png")
110
+ .expect(
111
+ toSucceedWithImage({ lengthIs: (bs) => bs < 100000 && bs > 2000 })
112
+ );
113
+ });
73
114
  it("not download file to public", async () => {
74
115
  const app = await getApp({ disableCsrf: true });
75
116
  await request(app).get("/files/download/rick.png").expect(404);