@saltcorn/server 0.8.7-beta.3 → 0.8.7-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auth/admin.js CHANGED
@@ -55,7 +55,7 @@ module.exports = router;
55
55
  */
56
56
  const getUserFields = async (req) => {
57
57
  const userTable = Table.findOne({ name: "users" });
58
- const userFields = (await userTable.getFields()).filter(
58
+ const userFields = (userTable.getFields()).filter(
59
59
  (f) => !f.calculated && f.name !== "id"
60
60
  );
61
61
  //console.log("userFields:",userFields);
@@ -265,7 +265,7 @@ router.get(
265
265
  class: "form-control",
266
266
  type: "search",
267
267
  "data-filter-table": "table.user-admin",
268
- placeholder: "🔍 Search",
268
+ placeholder: `🔍 ${req.__("Search")}`,
269
269
  })
270
270
  )
271
271
  ),
@@ -280,7 +280,7 @@ router.get(
280
280
  label: "",
281
281
  key: (r) =>
282
282
  r.disabled
283
- ? span({ class: "badge bg-danger" }, "Disabled")
283
+ ? span({ class: "badge bg-danger" }, req.__("Disabled"))
284
284
  : "",
285
285
  },
286
286
  {
package/locales/en.json CHANGED
@@ -1175,5 +1175,21 @@
1175
1175
  "App version": "App version",
1176
1176
  "Forgot password?": "Forgot password?",
1177
1177
  "Details": "Details",
1178
- "URL is a formula?": "URL is a formula?"
1178
+ "URL is a formula?": "URL is a formula?",
1179
+ "Receive notifications by:": "Receive notifications by:",
1180
+ "Backup settings": "Backup settings",
1181
+ "Logo image": "Logo image",
1182
+ "Custom code": "Custom code",
1183
+ "Extension store": "Extension store",
1184
+ "Backup file prefix": "Backup file prefix",
1185
+ "Directory for backup files": "Directory for backup files",
1186
+ "Backup File Prefix": "Backup File Prefix",
1187
+ "Search for...": "Search for...",
1188
+ "Every 5 minutes": "Every 5 minutes",
1189
+ "Not scheduled but can be run as an action from a button click": "Not scheduled but can be run as an action from a button click",
1190
+ "Fixed and blocked fields": "Fixed and blocked fields",
1191
+ "Do not allow the following fields to have a value set from the query string or state": "Do not allow the following fields to have a value set from the query string or state",
1192
+ "Action configuration saved": "Action configuration saved",
1193
+ "Prevent any deletion of parent rows": "Prevent any deletion of parent rows",
1194
+ "If the parent row is deleted, set key fields on child rows to null": "If the parent row is deleted, set key fields on child rows to null"
1179
1195
  }
package/locales/ru.json CHANGED
@@ -164,8 +164,8 @@
164
164
  "Installed": "Установлены",
165
165
  "Refresh": "Обновить",
166
166
  "Upgrade installed plugins": "Обновить установленные плагины",
167
- "Add another plugin": "Добавить еще один плагин",
168
- "Add another pack": "Добавить еще один пакет",
167
+ "Add another plugin": "Установить плагин",
168
+ "Add another pack": "Установить пакет",
169
169
  "Create pack": "Создать пакет",
170
170
  "Pack": "Пакет",
171
171
  "Install": "Установить",
@@ -261,8 +261,8 @@
261
261
  "New field:": "Новое поле:",
262
262
  "Field attributes": "Атрибуты поля",
263
263
  "Field %s created": "Поле %s создано",
264
- "Summary field": "Поле итога",
265
- "A default value is required when adding required fields to nonempty tables": "Для обязательных полей необходимо определить значение по-умолчанию",
264
+ "Summary field": "Поле сводки",
265
+ "A default value is required when adding required fields to nonempty tables": "Значение по умолчанию требуется при добавлении обязательных полей в непустные таблицы",
266
266
  "Create user": "Создать пользователя",
267
267
  "Please create your first user account, which will have administrative privileges. You can add other users and give them administrative privileges later.": "Пожалуйства создайте первого пользователя, который будет обладать правами администратора. Позже вы можете создать остальных пользователей и наделить их правами администратора при необходимости.",
268
268
  "Create first user": "Создать первого пользователя",
@@ -807,9 +807,9 @@
807
807
  "Automated backup": "Автоматическое резервное копирование",
808
808
  "Snapshots": "Снимки (снепшоты)",
809
809
  "Mobile app": "Мобильное приложение",
810
- "Module store": "Магазин модулей",
810
+ "Module store": "Магазин расширений",
811
811
  "Upgrade installed modules": "Обновить установленные модули",
812
- "Add another module": "Добавить модуль",
812
+ "Add another module": "Установить модуль",
813
813
  "Module": "Модуль",
814
814
  "Module installation and control": "Установка и управление модулями",
815
815
  "System logging verbosity": "Уровень логирования системы",
@@ -1004,10 +1004,51 @@
1004
1004
  "Connected views": "Связанные представления",
1005
1005
  "Embedded in": "Встроенно в",
1006
1006
  "Linked from": "Ссылается из",
1007
- "First user E-mail": "First user E-mail",
1007
+ "First user E-mail": "E-mail первого пользователя",
1008
1008
  "Table constraints": "Констрейнты таблиц",
1009
1009
  "Sessions": "Сессии",
1010
1010
  "Event logs": "Лог событий",
1011
1011
  "Migrations": "Миграции",
1012
- "Tag Entries": "Экземпляры тегов"
1012
+ "Tag Entries": "Экземпляры тегов",
1013
+ "Locale identifier short code, e.g. en, zh, fr, ar etc. ": "Короткий код локали, например: en, zh, fr, ar и т.д. ",
1014
+ " with password %s": " с паролем %s",
1015
+ "Import table %s": "Импорт таблицы %s",
1016
+ "Import CSV": "Импорт CSV",
1017
+ "Open": "Открыть",
1018
+ "App name": "Название приложения",
1019
+ "App version": "Версия приложения",
1020
+ "App icon": "Иконка приложения",
1021
+ "Splash Page": "Заставка",
1022
+ "Allow offline mode": "Разрешить режим оффлайн",
1023
+ "URL is a formula?": "Является ли URL формулой?",
1024
+ "In scope:": "В области (In scope):",
1025
+ "Destination page": "Целевая страница",
1026
+ "Include in full-text search": "Включить в полнотекстовый поиск",
1027
+ "The field that will be shown to the user when choosing a value": "Данное поле будет показано пользователю при выборе значения из связанной таблицы",
1028
+ "On delete": "При удалении",
1029
+ "If the parent row is deleted, do this to the child rows.": "В случае удаления родительской записи, выполнить указанное действие для дочерних записей.",
1030
+ "Set a default value for missing data": "Установить значение для недостающих данных",
1031
+ "Prevent any deletion of parent rows": "Запретить удаление родительских строк",
1032
+ "If the parent row is deleted, automatically delete the child rows.": "При удалении родительской записи автоматически удалять дочерние записи.",
1033
+ "If the parent row is deleted, set key fields on child rows to null": "При удалении родительской записи установить ключевые поля в null для дочерних записей",
1034
+ "Every 5 minutes": "Каждые 5 минут",
1035
+ "Not scheduled but can be run as an action from a button click": "Не запланировано, но может быть запущено по кнопке",
1036
+ "Action configuration saved": "Конфигурация действия сохранена",
1037
+ "Action information saved": "Информация о действии сохранена",
1038
+ "JavaScript code:": "JavaScript code:",
1039
+ "code here": "ваш программный код",
1040
+ "No notifications": "Нет уведомлений",
1041
+ "Receive notifications by:": "Receive notifications by:",
1042
+ "Backup file prefix": "Префикс имени файлов backup",
1043
+ "Directory for backup files": "Папка для файлов backup",
1044
+ "Backup File Prefix": "Префикс имени файлов backup",
1045
+ "Module %s installed": "Модуль %s установлен",
1046
+ "View patterns": "Паттерны представления",
1047
+ "%s module information": "Информация о модуле %s",
1048
+ "Search for...": "Поиск...",
1049
+ "Modules up-to-date. Please restart server": "Модули актуальны. Пожалуйста рестартуйте сервер",
1050
+ "Logo image": "Логотип сайта",
1051
+ "Custom code": "Пользовательский код",
1052
+ "Extension store": "Магазин расширений",
1053
+ "Progressive Web Application": "Прогрессивное веб-приложение (PWA)"
1013
1054
  }
package/markup/admin.js CHANGED
@@ -402,7 +402,7 @@ const config_fields_form = async ({
402
402
  if (typeof name0 === "object" && name0.section_header) {
403
403
  fields.push({
404
404
  input_type: "section_header",
405
- label: name0.section_header,
405
+ label: req.__(name0.section_header),
406
406
  });
407
407
  continue;
408
408
  }
package/markup/forms.js CHANGED
@@ -63,10 +63,16 @@ const fileUploadForm = (req, folder) => {
63
63
  name: "file",
64
64
  class: "form-control ms-1 w-unset d-inline",
65
65
  type: "file",
66
- onchange: "form.submit()",
66
+ onchange: "handle_upload_file_change(form)",
67
67
  multiple: true,
68
68
  }),
69
- folder && input({ type: "hidden", name: "folder", value: folder })
69
+ folder &&
70
+ input({
71
+ id: "uploadFolderInpId",
72
+ type: "hidden",
73
+ name: "folder",
74
+ value: folder,
75
+ })
70
76
  );
71
77
  return frm;
72
78
  };
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.7-beta.3",
3
+ "version": "0.8.7-beta.4",
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.7-beta.3",
10
- "@saltcorn/builder": "0.8.7-beta.3",
11
- "@saltcorn/data": "0.8.7-beta.3",
12
- "@saltcorn/admin-models": "0.8.7-beta.3",
13
- "@saltcorn/filemanager": "0.8.7-beta.3",
14
- "@saltcorn/markup": "0.8.7-beta.3",
15
- "@saltcorn/sbadmin2": "0.8.7-beta.3",
9
+ "@saltcorn/base-plugin": "0.8.7-beta.4",
10
+ "@saltcorn/builder": "0.8.7-beta.4",
11
+ "@saltcorn/data": "0.8.7-beta.4",
12
+ "@saltcorn/admin-models": "0.8.7-beta.4",
13
+ "@saltcorn/filemanager": "0.8.7-beta.4",
14
+ "@saltcorn/markup": "0.8.7-beta.4",
15
+ "@saltcorn/sbadmin2": "0.8.7-beta.4",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -143,6 +143,13 @@ function apply_showif() {
143
143
  } value="${value}">${label}</option>`;
144
144
  e.append($(html));
145
145
  });
146
+ //TODO: also sort inserted HTML options
147
+ dataOptions.sort((a, b) =>
148
+ (a.text?.toLowerCase?.() || a.text) >
149
+ (b.text?.toLowerCase?.() || b.text)
150
+ ? 1
151
+ : -1
152
+ );
146
153
  element.dispatchEvent(new Event("RefreshSelectOptions"));
147
154
  if (e.hasClass("selectized") && $().selectize) {
148
155
  e.selectize()[0].selectize.clearOptions(true);
@@ -841,6 +848,10 @@ function notifyAlert(note, spin) {
841
848
  </div>`);
842
849
  }
843
850
 
851
+ function emptyAlerts() {
852
+ $("#alerts-area").html("");
853
+ }
854
+
844
855
  function press_store_button(clicked) {
845
856
  const width = $(clicked).width();
846
857
  $(clicked).html('<i class="fas fa-spinner fa-spin"></i>').width(width);
@@ -51,13 +51,13 @@ function removeQueryStringParameter(uri1, key) {
51
51
  uri = uris[0];
52
52
  }
53
53
 
54
- var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
55
- var separator = uri.indexOf("?") !== -1 ? "&" : "?";
54
+ var re = new RegExp("([?&])" + key + "=.*?(&|$)", "gi");
56
55
  if (uri.match(re)) {
57
56
  uri = uri.replace(re, "$1" + "$2");
58
57
  }
59
58
  if (uri[uri.length - 1] === "?" || uri[uri.length - 1] === "&")
60
59
  uri = uri.substring(0, uri.length - 1);
60
+ if (uri.match(re)) return removeQueryStringParameter(uri + hash, key);
61
61
  return uri + hash;
62
62
  }
63
63
 
@@ -561,6 +561,22 @@ function create_new_folder(folder) {
561
561
  });
562
562
  }
563
563
 
564
+ function handle_upload_file_change(form) {
565
+ const url = new URL(window.location);
566
+ const dir = url.searchParams.get("dir");
567
+ if (dir !== null) $("#uploadFolderInpId").val(dir);
568
+ const jqForm = $(form);
569
+ const sortBy = url.searchParams.get("sortBy");
570
+ if (sortBy) {
571
+ jqForm.append(`<input type="hidden" name="sortBy" value="${sortBy}" />`);
572
+ }
573
+ const sortDesc = url.searchParams.get("sortDesc");
574
+ if (sortDesc === "on") {
575
+ jqForm.append('<input type="hidden" name="sortDesc" value="on" />');
576
+ }
577
+ form.submit();
578
+ }
579
+
564
580
  async function fill_formula_btn_click(btn, k) {
565
581
  const formula = decodeURIComponent($(btn).attr("data-formula"));
566
582
  const free_vars = JSON.parse(
package/routes/actions.js CHANGED
@@ -173,9 +173,9 @@ const triggerForm = async (req, trigger) => {
173
173
  sublabel: req.__("Event type which runs the trigger"),
174
174
  attributes: {
175
175
  explainers: {
176
- Often: "Every 5 minutes",
176
+ Often: req.__("Every 5 minutes"),
177
177
  Never:
178
- "Not scheduled but can be run as an action from a button click",
178
+ req.__("Not scheduled but can be run as an action from a button click"),
179
179
  },
180
180
  },
181
181
  },
@@ -358,7 +358,7 @@ router.post(
358
358
  });
359
359
  } else {
360
360
  await Trigger.update(trigger.id, form.values); //{configuration: form.values});
361
- req.flash("success", "Action information saved");
361
+ req.flash("success", req.__("Action information saved"));
362
362
  res.redirect(`/actions/`);
363
363
  }
364
364
  })
@@ -443,13 +443,13 @@ router.get(
443
443
  )
444
444
  )
445
445
  ),
446
- h6({ class: "mt-1" }, "JavaScript code:"),
446
+ h6({ class: "mt-1" }, req.__("JavaScript code:")),
447
447
  div(
448
448
  { class: "mt-1" },
449
449
 
450
450
  pre(
451
451
  { class: "js-code-display" },
452
- code({ id: "blockly_js_output" }, "code here")
452
+ code({ id: "blockly_js_output" }, req.__("code here"))
453
453
  )
454
454
  ),
455
455
  ],
@@ -538,7 +538,7 @@ router.post(
538
538
  res.json({ success: "ok" });
539
539
  return;
540
540
  }
541
- req.flash("success", "Action configuration saved");
541
+ req.flash("success", req.__("Action configuration saved"));
542
542
  res.redirect(
543
543
  req.query.on_done_redirect &&
544
544
  is_relative_url(req.query.on_done_redirect)
package/routes/admin.js CHANGED
@@ -249,6 +249,11 @@ router.get(
249
249
  "/backup",
250
250
  isAdmin,
251
251
  error_catcher(async (req, res) => {
252
+ //
253
+ const aBackupFilePrefixForm = backupFilePrefixForm(req);
254
+ aBackupFilePrefixForm.values.backup_file_prefix =
255
+ getState().getConfig("backup_file_prefix");
256
+ //
252
257
  const backupForm = autoBackupForm(req);
253
258
  backupForm.values.auto_backup_frequency = getState().getConfig(
254
259
  "auto_backup_frequency"
@@ -262,11 +267,13 @@ router.get(
262
267
  backupForm.values.auto_backup_expire_days = getState().getConfig(
263
268
  "auto_backup_expire_days"
264
269
  );
270
+ //
265
271
  const aSnapshotForm = snapshotForm(req);
266
272
  aSnapshotForm.values.snapshots_enabled =
267
273
  getState().getConfig("snapshots_enabled");
274
+ //
268
275
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
269
-
276
+ //
270
277
  send_admin_page({
271
278
  res,
272
279
  req,
@@ -367,6 +374,12 @@ router.get(
367
374
  )
368
375
  ),
369
376
  },
377
+ {
378
+ type: "card",
379
+ title: req.__("Backup settings"),
380
+ titleAjaxIndicator: true,
381
+ contents: div(renderForm(aBackupFilePrefixForm, req.csrfToken())),
382
+ },
370
383
  ],
371
384
  },
372
385
  });
@@ -388,10 +401,17 @@ router.get(
388
401
  return;
389
402
  }
390
403
  const auto_backup_directory = getState().getConfig("auto_backup_directory");
391
- const fileNms = await fs.promises.readdir(auto_backup_directory);
404
+
405
+ const backup_file_prefix = getState().getConfig("backup_file_prefix");
406
+
407
+ const fileNms = auto_backup_directory
408
+ ? await fs.promises.readdir(auto_backup_directory)
409
+ : [];
410
+
392
411
  const backupFiles = fileNms.filter(
393
- (fnm) => fnm.startsWith("sc-backup") && fnm.endsWith(".zip")
412
+ (fnm) => fnm.startsWith(backup_file_prefix) && fnm.endsWith(".zip")
394
413
  );
414
+
395
415
  send_admin_page({
396
416
  res,
397
417
  req,
@@ -402,20 +422,22 @@ router.get(
402
422
  type: "card",
403
423
  title: req.__("Download automated backup"),
404
424
  contents: div(
405
- ul(
406
- backupFiles.map((fnm) =>
407
- li(
408
- a(
409
- {
410
- href: `/admin/auto-backup-download/${encodeURIComponent(
425
+ backupFiles.length > 0
426
+ ? ul(
427
+ backupFiles.map((fnm) =>
428
+ li(
429
+ a(
430
+ {
431
+ href: `/admin/auto-backup-download/${encodeURIComponent(
432
+ fnm
433
+ )}`,
434
+ },
411
435
  fnm
412
- )}`,
413
- },
414
- fnm
436
+ )
437
+ )
415
438
  )
416
439
  )
417
- )
418
- )
440
+ : p(req.__("No files"))
419
441
  ),
420
442
  },
421
443
  {
@@ -465,23 +487,25 @@ router.get(
465
487
  type: "card",
466
488
  title: req.__("Download snapshots"),
467
489
  contents: div(
468
- ul(
469
- snaps.map((snap) =>
470
- li(
471
- a(
472
- {
473
- href: `/admin/snapshot-download/${encodeURIComponent(
474
- snap.id
475
- )}`,
476
- target: "_blank",
477
- },
478
- `${localeDateTime(snap.created)} (${moment(
479
- snap.created
480
- ).fromNow()})`
490
+ snaps.length > 0
491
+ ? ul(
492
+ snaps.map((snap) =>
493
+ li(
494
+ a(
495
+ {
496
+ href: `/admin/snapshot-download/${encodeURIComponent(
497
+ snap.id
498
+ )}`,
499
+ target: "_blank",
500
+ },
501
+ `${localeDateTime(snap.created)} (${moment(
502
+ snap.created
503
+ ).fromNow()})`
504
+ )
505
+ )
481
506
  )
482
507
  )
483
- )
484
- )
508
+ : p(req.__("No files"))
485
509
  ),
486
510
  },
487
511
  ],
@@ -588,9 +612,10 @@ router.get(
588
612
  error_catcher(async (req, res) => {
589
613
  const { filename } = req.params;
590
614
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
615
+ const backup_file_prefix = getState().getConfig("backup_file_prefix");
591
616
  if (
592
617
  !isRoot ||
593
- !(filename.startsWith("sc-backup") && filename.endsWith(".zip"))
618
+ !(filename.startsWith(backup_file_prefix) && filename.endsWith(".zip"))
594
619
  ) {
595
620
  res.redirect("/admin/backup");
596
621
  return;
@@ -600,6 +625,27 @@ router.get(
600
625
  })
601
626
  );
602
627
 
628
+ /**
629
+ * Set Backup File Prefix Form
630
+ * @param req
631
+ * @returns {Form}
632
+ */
633
+ const backupFilePrefixForm = (req) =>
634
+ new Form({
635
+ action: "/admin/set-backup-prefix",
636
+ onChange: `saveAndContinue(this);`,
637
+ noSubmitButton: true,
638
+ fields: [
639
+ {
640
+ type: "String",
641
+ label: req.__("Backup file prefix"),
642
+ name: "backup_file_prefix",
643
+ sublabel: req.__("Backup file prefix"),
644
+ default: "sc-backup-",
645
+ },
646
+ ],
647
+ });
648
+
603
649
  /**
604
650
  * Auto backup Form
605
651
  * @param {object} req
@@ -638,9 +684,10 @@ const autoBackupForm = (req) =>
638
684
  type: "String",
639
685
  label: req.__("Directory"),
640
686
  name: "auto_backup_directory",
687
+ sublabel: req.__("Directory for backup files"),
641
688
  showIf: {
642
689
  auto_backup_frequency: ["Daily", "Weekly"],
643
- auto_backup_destination: "Local directory",
690
+ //auto_backup_destination: "Local directory",
644
691
  },
645
692
  },
646
693
  {
@@ -658,6 +705,11 @@ const autoBackupForm = (req) =>
658
705
  ],
659
706
  });
660
707
 
708
+ /**
709
+ * Snapshot Form
710
+ * @param req
711
+ * @returns {Form}
712
+ */
661
713
  const snapshotForm = (req) =>
662
714
  new Form({
663
715
  action: "/admin/set-snapshot",
@@ -682,6 +734,9 @@ const snapshotForm = (req) =>
682
734
  },
683
735
  ],
684
736
  });
737
+ /**
738
+ * Do Set snapshot
739
+ */
685
740
  router.post(
686
741
  "/set-snapshot",
687
742
  isAdmin,
@@ -697,6 +752,38 @@ router.post(
697
752
  } else res.json({ success: "ok" });
698
753
  })
699
754
  );
755
+ /**
756
+ * Do Set Backup Prefix
757
+ */
758
+ router.post(
759
+ "/set-backup-prefix",
760
+ isAdmin,
761
+ error_catcher(async (req, res) => {
762
+ const form = await backupFilePrefixForm(req);
763
+ form.validate(req.body);
764
+ if (form.hasErrors) {
765
+ send_admin_page({
766
+ res,
767
+ req,
768
+ active_sub: "Backup",
769
+ contents: {
770
+ type: "card",
771
+ title: req.__("Backup settings"),
772
+ contents: [renderForm(form, req.csrfToken())],
773
+ },
774
+ });
775
+ } else {
776
+ await save_config_from_form(form);
777
+ if (!req.xhr) {
778
+ req.flash("success", req.__("Backup settings updated"));
779
+ res.redirect("/admin/backup");
780
+ } else res.json({ success: "ok" });
781
+ }
782
+ })
783
+ );
784
+ /**
785
+ * Do Set auto backup
786
+ */
700
787
  router.post(
701
788
  "/set-auto-backup",
702
789
  isAdmin,
@@ -723,6 +810,9 @@ router.post(
723
810
  }
724
811
  })
725
812
  );
813
+ /**
814
+ * Do Auto backup now
815
+ */
726
816
  router.post(
727
817
  "/auto-backup-now",
728
818
  isAdmin,
@@ -737,7 +827,9 @@ router.post(
737
827
  res.json({ reload_page: true });
738
828
  })
739
829
  );
740
-
830
+ /**
831
+ * Do Snapshot now
832
+ */
741
833
  router.post(
742
834
  "/snapshot-now",
743
835
  isAdmin,
@@ -755,6 +847,7 @@ router.post(
755
847
  );
756
848
 
757
849
  /**
850
+ * Show System page
758
851
  * @name get/system
759
852
  * @function
760
853
  * @memberof module:routes/admin~routes/adminRouter
@@ -948,6 +1041,7 @@ router.get(
948
1041
  );
949
1042
 
950
1043
  /**
1044
+ * Do Restart
951
1045
  * @name post/restart
952
1046
  * @function
953
1047
  * @memberof module:routes/admin~routes/adminRouter
@@ -972,6 +1066,7 @@ router.post(
972
1066
  );
973
1067
 
974
1068
  /**
1069
+ * Do Upgrade
975
1070
  * @name post/upgrade
976
1071
  * @function
977
1072
  * @memberof module:routes/admin~routes/adminRouter
@@ -1013,7 +1108,7 @@ router.post(
1013
1108
  })
1014
1109
  );
1015
1110
  /**
1016
- * /check-for-updates
1111
+ * Do Check for Update
1017
1112
  */
1018
1113
  router.post(
1019
1114
  "/check-for-upgrade",
@@ -1025,6 +1120,7 @@ router.post(
1025
1120
  })
1026
1121
  );
1027
1122
  /**
1123
+ * Do Manual Backup
1028
1124
  * @name post/backup
1029
1125
  * @function
1030
1126
  * @memberof module:routes/admin~routes/adminRouter
@@ -1045,6 +1141,7 @@ router.post(
1045
1141
  );
1046
1142
 
1047
1143
  /**
1144
+ * Do Restore from Backup
1048
1145
  * @name post/restore
1049
1146
  * @function
1050
1147
  * @memberof module:routes/admin~routes/adminRouter
@@ -1143,6 +1240,7 @@ const clearAllForm = (req) =>
1143
1240
  });
1144
1241
 
1145
1242
  /**
1243
+ * Do Enable letsencrypt
1146
1244
  * @name post/enable-letsencrypt
1147
1245
  * @function
1148
1246
  * @memberof module:routes/admin~routes/adminRouter
@@ -1222,6 +1320,7 @@ router.post(
1222
1320
  );
1223
1321
 
1224
1322
  /**
1323
+ * Do Clear All
1225
1324
  * @name get/clear-all
1226
1325
  * @function
1227
1326
  * @memberof module:routes/admin~routes/adminRouter
@@ -1250,7 +1349,7 @@ router.get(
1250
1349
  })
1251
1350
  );
1252
1351
  /**
1253
- * /configuration-check
1352
+ * Do Configuration Check
1254
1353
  */
1255
1354
  router.get(
1256
1355
  "/configuration-check",
@@ -1337,7 +1436,6 @@ router.get(
1337
1436
  });
1338
1437
  })
1339
1438
  );
1340
-
1341
1439
  const buildDialogScript = () => {
1342
1440
  return `<script>
1343
1441
  function swapEntryInputs(activeTab, activeInput, disabledTab, disabledInput) {
@@ -1760,7 +1858,9 @@ router.get(
1760
1858
  });
1761
1859
  })
1762
1860
  );
1763
-
1861
+ /**
1862
+ * Do Build Mobile App
1863
+ */
1764
1864
  router.post(
1765
1865
  "/build-mobile-app",
1766
1866
  isAdmin,
@@ -1788,7 +1888,7 @@ router.post(
1788
1888
  error: req.__("Only the android build supports docker."),
1789
1889
  });
1790
1890
  }
1791
- if (!serverURL || serverURL.length == 0) {
1891
+ if (!serverURL || serverURL.length === 0) {
1792
1892
  serverURL = getState().getConfig("base_url") || "";
1793
1893
  }
1794
1894
  if (!serverURL.startsWith("http")) {
@@ -1888,8 +1988,7 @@ router.post(
1888
1988
  );
1889
1989
 
1890
1990
  /**
1891
- * Clear all
1892
- * @name post/clear-all
1991
+ * Do Clear All
1893
1992
  * @function
1894
1993
  * @memberof module:routes/admin~routes/adminRouter
1895
1994
  */
@@ -2012,7 +2111,9 @@ router.post(
2012
2111
  }
2013
2112
  })
2014
2113
  );
2015
-
2114
+ /**
2115
+ * Dev / Admin
2116
+ */
2016
2117
  admin_config_route({
2017
2118
  router,
2018
2119
  path: "/dev",
@@ -2051,7 +2152,9 @@ admin_config_route({
2051
2152
  });
2052
2153
  },
2053
2154
  });
2054
-
2155
+ /**
2156
+ * Notifications
2157
+ */
2055
2158
  admin_config_route({
2056
2159
  router,
2057
2160
  path: "/notifications",
package/routes/fields.js CHANGED
@@ -454,11 +454,11 @@ const fieldFlow = (req) =>
454
454
  required: true,
455
455
  attributes: {
456
456
  explainers: {
457
- Fail: "Prevent any deletion of parent rows",
457
+ Fail: req.__("Prevent any deletion of parent rows"),
458
458
  Cascade:
459
- "If the parent row is deleted, automatically delete the child rows.",
459
+ req.__("If the parent row is deleted, automatically delete the child rows."),
460
460
  "Set null":
461
- "If the parent row is deleted, set key fields on child rows to null",
461
+ req.__("If the parent row is deleted, set key fields on child rows to null"),
462
462
  },
463
463
  },
464
464
  sublabel: req.__(
package/routes/files.js CHANGED
@@ -69,9 +69,12 @@ router.get(
69
69
  isAdmin,
70
70
  error_catcher(async (req, res) => {
71
71
  // todo limit select from file by 10 or 20
72
- const { dir } = req.query;
72
+ const { dir, search } = req.query;
73
73
  const safeDir = File.normalise(dir || "/");
74
- const rows = await File.find({ folder: dir }, { orderBy: "filename" });
74
+ const rows = await File.find(
75
+ { folder: dir, search },
76
+ { orderBy: "filename" }
77
+ );
75
78
  const roles = await User.get_roles();
76
79
  if (safeDir && safeDir !== "/" && safeDir !== ".") {
77
80
  let dirname = path.dirname(safeDir);
@@ -383,7 +386,7 @@ router.post(
383
386
  "/upload",
384
387
  setTenant,
385
388
  error_catcher(async (req, res) => {
386
- let { folder } = req.body;
389
+ let { folder, sortBy, sortDesc } = req.body;
387
390
  let jsonResp = {};
388
391
  const min_role_upload = getState().getConfig("min_role_upload", 1);
389
392
  const role = req.user && req.user.id ? req.user.role_id : 100;
@@ -404,16 +407,11 @@ router.post(
404
407
  );
405
408
  const many = Array.isArray(f);
406
409
  file_for_redirect = many ? f[0] : f;
407
- if (!req.xhr)
408
- req.flash(
409
- "success",
410
- req.__(
411
- `File %s uploaded`,
412
- many
413
- ? f.map((fl) => text(fl.filename)).join(", ")
414
- : text(f.filename)
415
- )
416
- );
410
+ const successMsg = req.__(
411
+ `File %s uploaded`,
412
+ many ? f.map((fl) => text(fl.filename)).join(", ") : text(f.filename)
413
+ );
414
+ if (!req.xhr) req.flash("success", successMsg);
417
415
  else
418
416
  jsonResp = {
419
417
  success: {
@@ -422,16 +420,18 @@ router.post(
422
420
  url: many
423
421
  ? f.map((fl) => `/files/serve/${fl.path_to_serve}`)
424
422
  : `/files/serve/${f.path_to_serve}`,
423
+ msg: successMsg,
425
424
  },
426
425
  };
427
426
  }
428
- if (!req.xhr)
429
- res.redirect(
430
- !file_for_redirect
431
- ? "/files"
432
- : `/files?dir=${encodeURIComponent(file_for_redirect.current_folder)}`
433
- );
434
- else res.json(jsonResp);
427
+ if (!req.xhr) {
428
+ const sp = new URLSearchParams();
429
+ if (file_for_redirect) sp.append("dir", file_for_redirect.current_folder);
430
+ if (sortBy) sp.append("sortBy", sortBy);
431
+ if (sortDesc) sp.append("sortDesc", sortDesc);
432
+ const query = sp.toString();
433
+ res.redirect(`/files${query ? `?${query}` : ""}`);
434
+ } else res.json(jsonResp);
435
435
  })
436
436
  );
437
437
 
@@ -449,7 +449,7 @@ router.post(
449
449
  const { redirect } = req.query;
450
450
  const f = await File.findOne(serve_path);
451
451
  if (!f) {
452
- req.flash("error", "File not found");
452
+ req.flash("error", req.__("File not found"));
453
453
  res.redirect("/files");
454
454
  return;
455
455
  }
@@ -70,7 +70,7 @@ router.get(
70
70
  {
71
71
  type: "card",
72
72
  contents: [
73
- "Receive notifications by:",
73
+ req.__("Receive notifications by:"),
74
74
  renderForm(notificationSettingsForm(), req.csrfToken()),
75
75
  ],
76
76
  },
package/routes/plugins.js CHANGED
@@ -526,7 +526,8 @@ const plugin_store_html = (items, req) => {
526
526
  contents: div(
527
527
  { class: "d-flex justify-content-between" },
528
528
  storeNavPills(req),
529
- div(search_bar("q", req.query.q || "", { stateField: "q" })),
529
+ div(search_bar("q", req.query.q || "",
530
+ { placeHolder: req.__("Search for..."), stateField: "q" })),
530
531
  div(store_actions_dropdown(req))
531
532
  ),
532
533
  },
@@ -825,7 +826,7 @@ router.get(
825
826
  pkgjson = require(path.join(mod.location, "package.json"));
826
827
 
827
828
  if (!plugin_db) {
828
- req.flash("warning", "Module not found");
829
+ req.flash("warning", req.__("Module not found"));
829
830
  res.redirect("/plugins");
830
831
  return;
831
832
  }
@@ -1035,7 +1036,7 @@ router.post(
1035
1036
 
1036
1037
  const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
1037
1038
  if (!plugin) {
1038
- req.flash("warning", "Module not found");
1039
+ req.flash("warning", req.__("Module not found"));
1039
1040
  res.redirect("/plugins");
1040
1041
  return;
1041
1042
  }
package/routes/tables.js CHANGED
@@ -1135,7 +1135,7 @@ router.get(
1135
1135
  error_catcher(async (req, res) => {
1136
1136
  const { name } = req.params;
1137
1137
  const table = Table.findOne({ name });
1138
- const rows = await table.getRows({}, { orderBy: "id", orderDesc: true });
1138
+ const rows = await table.getRows({}, { orderBy: "id" });
1139
1139
  res.setHeader("Content-Type", "text/csv");
1140
1140
  res.setHeader("Content-Disposition", `attachment; filename="${name}.csv"`);
1141
1141
  res.setHeader("Cache-Control", "no-cache");
@@ -34,6 +34,9 @@ test("updateQueryStringParameter", () => {
34
34
  expect(removeQueryStringParameter("/foo?name=Bar&age=45", "age")).toBe(
35
35
  "/foo?name=Bar"
36
36
  );
37
+ expect(
38
+ removeQueryStringParameter("/foo?name=Baz&name=Foo&age=45", "name")
39
+ ).not.toContain("name");
37
40
  expect(
38
41
  updateQueryStringParameter("/foo", "publisher.publisher->name", "AK")
39
42
  ).toBe("/foo?publisher.publisher->name=AK");
@@ -7,9 +7,9 @@ const {
7
7
  itShouldRedirectUnauthToLogin,
8
8
  toInclude,
9
9
  toSucceed,
10
- toNotInclude,
11
10
  resetToFixtures,
12
11
  toSucceedWithImage,
12
+ respondJsonWith,
13
13
  } = require("../auth/testhelp");
14
14
  const db = require("@saltcorn/data/db");
15
15
  const fs = require("fs").promises;
@@ -18,7 +18,17 @@ const File = require("@saltcorn/data/models/file");
18
18
  const Field = require("@saltcorn/data/models/field");
19
19
  const Table = require("@saltcorn/data/models/table");
20
20
  const View = require("@saltcorn/data/models/view");
21
- const { table } = require("console");
21
+ const { existsSync } = require("fs");
22
+
23
+ const createTestFile = async (folder, name, mimetype, content) => {
24
+ if (
25
+ !existsSync(
26
+ path.join(db.connectObj.file_store, db.getTenantSchema(), folder, name)
27
+ )
28
+ ) {
29
+ await File.from_contents(name, mimetype, content, 1, 100, folder);
30
+ }
31
+ };
22
32
 
23
33
  beforeAll(async () => {
24
34
  await resetToFixtures();
@@ -47,6 +57,33 @@ beforeAll(async () => {
47
57
  1,
48
58
  100
49
59
  );
60
+
61
+ await File.new_folder(path.join("_sc_test_subfolder_one", "subsubfolder"));
62
+ await createTestFile(
63
+ "_sc_test_subfolder_one",
64
+ "foo_image.png",
65
+ "image/png",
66
+ "imagecontent"
67
+ );
68
+ await createTestFile(
69
+ "_sc_test_subfolder_one",
70
+ "bar_image.png",
71
+ "image/png",
72
+ "imagecontent"
73
+ );
74
+ await createTestFile(
75
+ path.join("_sc_test_subfolder_one", "subsubfolder"),
76
+ "bar_image.png",
77
+ "image/png",
78
+ "imagecontent"
79
+ );
80
+ await File.new_folder("_sc_test_subfolder_two");
81
+ await createTestFile(
82
+ "_sc_test_subfolder_two",
83
+ "foo_image.png",
84
+ "image/png",
85
+ "imagecontent"
86
+ );
50
87
  });
51
88
  afterAll(db.close);
52
89
 
@@ -146,6 +183,90 @@ describe("files admin", () => {
146
183
 
147
184
  .expect(toRedirect("/files?dir=."));
148
185
  });
186
+ it("search files by name", async () => {
187
+ const app = await getApp({ disableCsrf: true });
188
+ 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
+ );
196
+ const searchTestHelper = async (dir, search, expected) => {
197
+ await request(app)
198
+ .get("/files")
199
+ .query({ dir, search })
200
+ .set("X-Requested-With", "XMLHttpRequest")
201
+ .set("Cookie", loginCookie)
202
+ .expect(
203
+ respondJsonWith(200, (data) => checkFiles(data.files, expected))
204
+ );
205
+ };
206
+
207
+ await searchTestHelper("/", "foo", [
208
+ {
209
+ filename: "foo_image.png",
210
+ location: path.join("_sc_test_subfolder_one", "foo_image.png"),
211
+ },
212
+ {
213
+ filename: "foo_image.png",
214
+ location: path.join("_sc_test_subfolder_two", "foo_image.png"),
215
+ },
216
+ ]);
217
+ await searchTestHelper("_sc_test_subfolder_two", "foo", [
218
+ {
219
+ filename: "foo_image.png",
220
+ location: path.join("_sc_test_subfolder_two", "foo_image.png"),
221
+ },
222
+ {
223
+ filename: "..",
224
+ location: "",
225
+ },
226
+ ]);
227
+ await searchTestHelper("/", "bar", [
228
+ {
229
+ filename: "bar_image.png",
230
+ location: path.join("_sc_test_subfolder_one", "bar_image.png"),
231
+ },
232
+ {
233
+ filename: "bar_image.png",
234
+ location: path.join(
235
+ "_sc_test_subfolder_one",
236
+ "subsubfolder",
237
+ "bar_image.png"
238
+ ),
239
+ },
240
+ ]);
241
+ await searchTestHelper(
242
+ path.join("_sc_test_subfolder_one", "subsubfolder"),
243
+ "foo",
244
+ [
245
+ {
246
+ filename: "..",
247
+ location: "_sc_test_subfolder_one",
248
+ },
249
+ ]
250
+ );
251
+ await searchTestHelper(
252
+ path.join("_sc_test_subfolder_one", "subsubfolder"),
253
+ "bar",
254
+ [
255
+ {
256
+ filename: "..",
257
+ location: "_sc_test_subfolder_one",
258
+ },
259
+ {
260
+ filename: "bar_image.png",
261
+ location: path.join(
262
+ "_sc_test_subfolder_one",
263
+ "subsubfolder",
264
+ "bar_image.png"
265
+ ),
266
+ },
267
+ ]
268
+ );
269
+ });
149
270
  });
150
271
  describe("files edit", () => {
151
272
  it("creates table and view", async () => {