pgkiln 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/db/migrations/140_item_region_types.sql +27 -0
  2. package/db/migrations/141_workspace_resources.sql +115 -0
  3. package/docs/apex-feature-parity.md +91 -12
  4. package/docs/guide/01-installation.md +1 -0
  5. package/docs/guide/03-builder.md +47 -4
  6. package/docs/guide/04-pages-and-regions.md +41 -1
  7. package/docs/guide/05-items.md +43 -3
  8. package/docs/guide/11-from-apex.md +3 -0
  9. package/docs/guide/12-development.md +25 -3
  10. package/docs/guide/16-files.md +29 -0
  11. package/docs/guide/19-rest-data-sources.md +41 -3
  12. package/examples/hr/hr_75_item_region_types.sql +81 -0
  13. package/examples/hr/hr_76_remote_servers.sql +48 -0
  14. package/package.json +1 -1
  15. package/public/app.css +48 -0
  16. package/public/app.js +377 -1
  17. package/src/ai/service.ts +10 -1
  18. package/src/app.ts +2 -0
  19. package/src/builder/advisor.ts +18 -2
  20. package/src/builder/ai.ts +29 -3
  21. package/src/builder/arrange.ts +7 -0
  22. package/src/builder/components.ts +5 -5
  23. package/src/builder/designer.ts +9 -3
  24. package/src/builder/home.ts +3 -0
  25. package/src/builder/i18n/de.ts +6 -0
  26. package/src/builder/i18n/en.ts +6 -0
  27. package/src/builder/i18n/es.ts +6 -0
  28. package/src/builder/i18n/fr.ts +6 -0
  29. package/src/builder/i18n/it.ts +6 -0
  30. package/src/builder/i18n/ja.ts +6 -0
  31. package/src/builder/i18n/ko.ts +6 -0
  32. package/src/builder/i18n/nl.ts +6 -0
  33. package/src/builder/i18n/pt.ts +6 -0
  34. package/src/builder/i18n/zh-cn.ts +6 -0
  35. package/src/builder/i18n/zh-tw.ts +6 -0
  36. package/src/builder/network-access.ts +33 -0
  37. package/src/builder/region-help.ts +8 -2
  38. package/src/builder/rest-catalogs.ts +22 -14
  39. package/src/builder/routes.ts +5 -1
  40. package/src/builder/static-files.ts +14 -4
  41. package/src/builder/websources.ts +12 -6
  42. package/src/builder/workflows.ts +2 -1
  43. package/src/builder/workspace-resources.ts +528 -0
  44. package/src/i18n/ar.ts +13 -0
  45. package/src/i18n/cs.ts +13 -0
  46. package/src/i18n/da.ts +13 -0
  47. package/src/i18n/de.ts +13 -0
  48. package/src/i18n/el.ts +13 -0
  49. package/src/i18n/es.ts +13 -0
  50. package/src/i18n/fi.ts +13 -0
  51. package/src/i18n/fr.ts +13 -0
  52. package/src/i18n/he.ts +13 -0
  53. package/src/i18n/it.ts +13 -0
  54. package/src/i18n/ja.ts +13 -0
  55. package/src/i18n/ko.ts +13 -0
  56. package/src/i18n/nb.ts +13 -0
  57. package/src/i18n/pl.ts +13 -0
  58. package/src/i18n/pt.ts +13 -0
  59. package/src/i18n/ru.ts +13 -0
  60. package/src/i18n/sv.ts +13 -0
  61. package/src/i18n/tr.ts +13 -0
  62. package/src/i18n/uk.ts +13 -0
  63. package/src/i18n/zh.ts +13 -0
  64. package/src/i18n.ts +26 -0
  65. package/src/metadata.ts +22 -5
  66. package/src/remote-servers.ts +133 -0
  67. package/src/restcatalog.ts +21 -10
  68. package/src/restsync.ts +1 -1
  69. package/src/runtime/context.ts +2 -0
  70. package/src/runtime/items.ts +32 -3
  71. package/src/runtime/page-regions.ts +50 -0
  72. package/src/runtime/region-cache.ts +1 -1
  73. package/src/runtime/regions.ts +11 -0
  74. package/src/runtime/render.ts +3 -17
  75. package/src/runtime/report.ts +55 -3
  76. package/src/runtime/static-files.ts +46 -17
  77. package/src/runtime/template-components.ts +2 -1
  78. package/src/websources.ts +43 -18
  79. package/src/workspace-export.ts +80 -1
package/src/i18n/el.ts CHANGED
@@ -424,6 +424,19 @@ export const el: Record<MessageKey, string> = {
424
424
  'lov.loading': "Φόρτωση περισσότερων…",
425
425
  'da.wait': "Παρακαλώ περιμένετε…",
426
426
  'lov.no_rows': "Δεν βρέθηκε τίποτα.",
427
+ 'shuttle.available': "Διαθέσιμα",
428
+ 'shuttle.chosen': "Επιλεγμένα",
429
+ 'shuttle.move': "Μετακίνηση",
430
+ 'shuttle.move_all': "Μετακίνηση όλων",
431
+ 'shuttle.remove': "Αφαίρεση",
432
+ 'shuttle.remove_all': "Αφαίρεση όλων",
433
+ 'shuttle.up': "Μετακίνηση πάνω",
434
+ 'shuttle.down': "Μετακίνηση κάτω",
435
+ 'shuttle.top': "Μετακίνηση στην αρχή",
436
+ 'shuttle.bottom': "Μετακίνηση στο τέλος",
437
+ 'shuttle.filter': "Φιλτράρισμα",
438
+ 'region.breadcrumb': "Διαδρομή πλοήγησης",
439
+ 'region.no_help': "Δεν υπάρχει βοήθεια για αυτή τη σελίδα.",
427
440
  'search.did_you_mean': "Μήπως εννοούσατε:",
428
441
  'error.lov_value': "{label}: επιλέξτε τιμή από τη λίστα.",
429
442
  'item.yes': "Ναι",
package/src/i18n/es.ts CHANGED
@@ -421,6 +421,19 @@ export const es: Record<MessageKey, string> = {
421
421
  'lov.loading': 'Cargando más…',
422
422
  'da.wait': 'Espere, por favor…',
423
423
  'lov.no_rows': 'No se ha encontrado nada.',
424
+ 'shuttle.available': 'Disponibles',
425
+ 'shuttle.chosen': 'Seleccionados',
426
+ 'shuttle.move': 'Mover',
427
+ 'shuttle.move_all': 'Mover todos',
428
+ 'shuttle.remove': 'Quitar',
429
+ 'shuttle.remove_all': 'Quitar todos',
430
+ 'shuttle.up': 'Subir',
431
+ 'shuttle.down': 'Bajar',
432
+ 'shuttle.top': 'Mover al principio',
433
+ 'shuttle.bottom': 'Mover al final',
434
+ 'shuttle.filter': 'Filtrar',
435
+ 'region.breadcrumb': 'Ruta de navegación',
436
+ 'region.no_help': 'No hay ayuda disponible para esta página.',
424
437
  'search.did_you_mean': "Quizás quiso decir:",
425
438
  'error.lov_value': '{label}: elija un valor de la lista.',
426
439
  'item.yes': 'Sí',
package/src/i18n/fi.ts CHANGED
@@ -424,6 +424,19 @@ export const fi: Record<MessageKey, string> = {
424
424
  'lov.loading': "Ladataan lisää…",
425
425
  'da.wait': "Odota hetki…",
426
426
  'lov.no_rows': "Mitään ei löytynyt.",
427
+ 'shuttle.available': "Käytettävissä",
428
+ 'shuttle.chosen': "Valitut",
429
+ 'shuttle.move': "Siirrä",
430
+ 'shuttle.move_all': "Siirrä kaikki",
431
+ 'shuttle.remove': "Poista",
432
+ 'shuttle.remove_all': "Poista kaikki",
433
+ 'shuttle.up': "Siirrä ylös",
434
+ 'shuttle.down': "Siirrä alas",
435
+ 'shuttle.top': "Siirrä alkuun",
436
+ 'shuttle.bottom': "Siirrä loppuun",
437
+ 'shuttle.filter': "Suodata",
438
+ 'region.breadcrumb': "Murupolku",
439
+ 'region.no_help': "Tälle sivulle ei ole ohjetta.",
427
440
  'search.did_you_mean': "Tarkoititko:",
428
441
  'error.lov_value': "{label}: valitse arvo luettelosta.",
429
442
  'item.yes': "Kyllä",
package/src/i18n/fr.ts CHANGED
@@ -421,6 +421,19 @@ export const fr: Record<MessageKey, string> = {
421
421
  'lov.loading': 'Chargement de la suite…',
422
422
  'da.wait': 'Veuillez patienter…',
423
423
  'lov.no_rows': 'Aucun résultat.',
424
+ 'shuttle.available': 'Disponibles',
425
+ 'shuttle.chosen': 'Sélectionnés',
426
+ 'shuttle.move': 'Déplacer',
427
+ 'shuttle.move_all': 'Tout déplacer',
428
+ 'shuttle.remove': 'Retirer',
429
+ 'shuttle.remove_all': 'Tout retirer',
430
+ 'shuttle.up': 'Monter',
431
+ 'shuttle.down': 'Descendre',
432
+ 'shuttle.top': 'Placer en premier',
433
+ 'shuttle.bottom': 'Placer en dernier',
434
+ 'shuttle.filter': 'Filtrer',
435
+ 'region.breadcrumb': 'Fil d\'Ariane',
436
+ 'region.no_help': 'Aucune aide n\'est disponible pour cette page.',
424
437
  'search.did_you_mean': "Vouliez-vous dire :",
425
438
  'error.lov_value': '{label} : choisissez une valeur dans la liste.',
426
439
  'item.yes': 'Oui',
package/src/i18n/he.ts CHANGED
@@ -424,6 +424,19 @@ export const he: Record<MessageKey, string> = {
424
424
  'lov.loading': "טוען עוד…",
425
425
  'da.wait': "נא להמתין…",
426
426
  'lov.no_rows': "לא נמצא דבר.",
427
+ 'shuttle.available': "זמינים",
428
+ 'shuttle.chosen': "נבחרים",
429
+ 'shuttle.move': "העבר",
430
+ 'shuttle.move_all': "העבר הכול",
431
+ 'shuttle.remove': "הסר",
432
+ 'shuttle.remove_all': "הסר הכול",
433
+ 'shuttle.up': "הזז למעלה",
434
+ 'shuttle.down': "הזז למטה",
435
+ 'shuttle.top': "הזז להתחלה",
436
+ 'shuttle.bottom': "הזז לסוף",
437
+ 'shuttle.filter': "סינון",
438
+ 'region.breadcrumb': "נתיב ניווט",
439
+ 'region.no_help': "אין עזרה זמינה לדף זה.",
427
440
  'search.did_you_mean': "האם התכוונת ל:",
428
441
  'error.lov_value': "{label}: בחרו ערך מהרשימה.",
429
442
  'item.yes': "כן",
package/src/i18n/it.ts CHANGED
@@ -424,6 +424,19 @@ export const it: Record<MessageKey, string> = {
424
424
  'lov.loading': "Caricamento di altri…",
425
425
  'da.wait': "Attendere prego…",
426
426
  'lov.no_rows': "Nessun risultato.",
427
+ 'shuttle.available': "Disponibili",
428
+ 'shuttle.chosen': "Selezionati",
429
+ 'shuttle.move': "Sposta",
430
+ 'shuttle.move_all': "Sposta tutti",
431
+ 'shuttle.remove': "Rimuovi",
432
+ 'shuttle.remove_all': "Rimuovi tutti",
433
+ 'shuttle.up': "Sposta su",
434
+ 'shuttle.down': "Sposta giù",
435
+ 'shuttle.top': "Sposta all'inizio",
436
+ 'shuttle.bottom': "Sposta alla fine",
437
+ 'shuttle.filter': "Filtra",
438
+ 'region.breadcrumb': "Percorso di navigazione",
439
+ 'region.no_help': "Nessuna guida disponibile per questa pagina.",
427
440
  'search.did_you_mean': "Forse cercavi:",
428
441
  'error.lov_value': "{label}: scegli un valore dall'elenco.",
429
442
  'item.yes': "Sì",
package/src/i18n/ja.ts CHANGED
@@ -424,6 +424,19 @@ export const ja: Record<MessageKey, string> = {
424
424
  'lov.loading': "さらに読み込み中…",
425
425
  'da.wait': "お待ちください…",
426
426
  'lov.no_rows': "見つかりません。",
427
+ 'shuttle.available': "使用可能",
428
+ 'shuttle.chosen': "選択済み",
429
+ 'shuttle.move': "移動",
430
+ 'shuttle.move_all': "すべて移動",
431
+ 'shuttle.remove': "削除",
432
+ 'shuttle.remove_all': "すべて削除",
433
+ 'shuttle.up': "上へ移動",
434
+ 'shuttle.down': "下へ移動",
435
+ 'shuttle.top': "先頭へ移動",
436
+ 'shuttle.bottom': "末尾へ移動",
437
+ 'shuttle.filter': "フィルタ",
438
+ 'region.breadcrumb': "パンくずリスト",
439
+ 'region.no_help': "このページのヘルプはありません。",
427
440
  'search.did_you_mean': "もしかして:",
428
441
  'error.lov_value': "{label}: 一覧から値を選んでください。",
429
442
  'item.yes': "はい",
package/src/i18n/ko.ts CHANGED
@@ -424,6 +424,19 @@ export const ko: Record<MessageKey, string> = {
424
424
  'lov.loading': "더 불러오는 중…",
425
425
  'da.wait': "잠시 기다려 주세요…",
426
426
  'lov.no_rows': "찾은 항목이 없습니다.",
427
+ 'shuttle.available': "사용 가능",
428
+ 'shuttle.chosen': "선택됨",
429
+ 'shuttle.move': "이동",
430
+ 'shuttle.move_all': "모두 이동",
431
+ 'shuttle.remove': "제거",
432
+ 'shuttle.remove_all': "모두 제거",
433
+ 'shuttle.up': "위로 이동",
434
+ 'shuttle.down': "아래로 이동",
435
+ 'shuttle.top': "맨 위로 이동",
436
+ 'shuttle.bottom': "맨 아래로 이동",
437
+ 'shuttle.filter': "필터",
438
+ 'region.breadcrumb': "이동 경로",
439
+ 'region.no_help': "이 페이지에 대한 도움말이 없습니다.",
427
440
  'search.did_you_mean': "이것을 찾으셨나요:",
428
441
  'error.lov_value': "{label}: 목록에서 값을 선택하세요.",
429
442
  'item.yes': "예",
package/src/i18n/nb.ts CHANGED
@@ -424,6 +424,19 @@ export const nb: Record<MessageKey, string> = {
424
424
  'lov.loading': "Laster inn flere…",
425
425
  'da.wait': "Vent litt…",
426
426
  'lov.no_rows': "Fant ingenting.",
427
+ 'shuttle.available': "Tilgjengelige",
428
+ 'shuttle.chosen': "Valgte",
429
+ 'shuttle.move': "Flytt",
430
+ 'shuttle.move_all': "Flytt alle",
431
+ 'shuttle.remove': "Fjern",
432
+ 'shuttle.remove_all': "Fjern alle",
433
+ 'shuttle.up': "Flytt opp",
434
+ 'shuttle.down': "Flytt ned",
435
+ 'shuttle.top': "Flytt øverst",
436
+ 'shuttle.bottom': "Flytt nederst",
437
+ 'shuttle.filter': "Filtrer",
438
+ 'region.breadcrumb': "Brødsmulesti",
439
+ 'region.no_help': "Det finnes ingen hjelp for denne siden.",
427
440
  'search.did_you_mean': "Mente du:",
428
441
  'error.lov_value': "{label}: velg en verdi fra listen.",
429
442
  'item.yes': "Ja",
package/src/i18n/pl.ts CHANGED
@@ -424,6 +424,19 @@ export const pl: Record<MessageKey, string> = {
424
424
  'lov.loading': "Wczytywanie kolejnych…",
425
425
  'da.wait': "Proszę czekać…",
426
426
  'lov.no_rows': "Nic nie znaleziono.",
427
+ 'shuttle.available': "Dostępne",
428
+ 'shuttle.chosen': "Wybrane",
429
+ 'shuttle.move': "Przenieś",
430
+ 'shuttle.move_all': "Przenieś wszystkie",
431
+ 'shuttle.remove': "Usuń",
432
+ 'shuttle.remove_all': "Usuń wszystkie",
433
+ 'shuttle.up': "W górę",
434
+ 'shuttle.down': "W dół",
435
+ 'shuttle.top': "Na początek",
436
+ 'shuttle.bottom': "Na koniec",
437
+ 'shuttle.filter': "Filtruj",
438
+ 'region.breadcrumb': "Ścieżka nawigacji",
439
+ 'region.no_help': "Brak pomocy dla tej strony.",
427
440
  'search.did_you_mean': "Czy chodziło o:",
428
441
  'error.lov_value': "{label}: wybierz wartość z listy.",
429
442
  'item.yes': "Tak",
package/src/i18n/pt.ts CHANGED
@@ -424,6 +424,19 @@ export const pt: Record<MessageKey, string> = {
424
424
  'lov.loading': "A carregar mais…",
425
425
  'da.wait': "Aguarde, por favor…",
426
426
  'lov.no_rows': "Nada encontrado.",
427
+ 'shuttle.available': "Disponíveis",
428
+ 'shuttle.chosen': "Selecionados",
429
+ 'shuttle.move': "Mover",
430
+ 'shuttle.move_all': "Mover todos",
431
+ 'shuttle.remove': "Remover",
432
+ 'shuttle.remove_all': "Remover todos",
433
+ 'shuttle.up': "Mover para cima",
434
+ 'shuttle.down': "Mover para baixo",
435
+ 'shuttle.top': "Mover para o início",
436
+ 'shuttle.bottom': "Mover para o fim",
437
+ 'shuttle.filter': "Filtrar",
438
+ 'region.breadcrumb': "Trilha de navegação",
439
+ 'region.no_help': "Não há ajuda disponível para esta página.",
427
440
  'search.did_you_mean': "Queria dizer:",
428
441
  'error.lov_value': "{label}: escolha um valor da lista.",
429
442
  'item.yes': "Sim",
package/src/i18n/ru.ts CHANGED
@@ -424,6 +424,19 @@ export const ru: Record<MessageKey, string> = {
424
424
  'lov.loading': "Загрузка…",
425
425
  'da.wait': "Подождите…",
426
426
  'lov.no_rows': "Ничего не найдено.",
427
+ 'shuttle.available': "Доступные",
428
+ 'shuttle.chosen': "Выбранные",
429
+ 'shuttle.move': "Переместить",
430
+ 'shuttle.move_all': "Переместить все",
431
+ 'shuttle.remove': "Убрать",
432
+ 'shuttle.remove_all': "Убрать все",
433
+ 'shuttle.up': "Вверх",
434
+ 'shuttle.down': "Вниз",
435
+ 'shuttle.top': "В начало",
436
+ 'shuttle.bottom': "В конец",
437
+ 'shuttle.filter': "Фильтр",
438
+ 'region.breadcrumb': "Навигационная цепочка",
439
+ 'region.no_help': "Для этой страницы нет справки.",
427
440
  'search.did_you_mean': "Возможно, вы имели в виду:",
428
441
  'error.lov_value': "{label}: выберите значение из списка.",
429
442
  'item.yes': "Да",
package/src/i18n/sv.ts CHANGED
@@ -424,6 +424,19 @@ export const sv: Record<MessageKey, string> = {
424
424
  'lov.loading': "Läser in fler…",
425
425
  'da.wait': "Vänta…",
426
426
  'lov.no_rows': "Inget hittades.",
427
+ 'shuttle.available': "Tillgängliga",
428
+ 'shuttle.chosen': "Valda",
429
+ 'shuttle.move': "Flytta",
430
+ 'shuttle.move_all': "Flytta alla",
431
+ 'shuttle.remove': "Ta bort",
432
+ 'shuttle.remove_all': "Ta bort alla",
433
+ 'shuttle.up': "Flytta upp",
434
+ 'shuttle.down': "Flytta ned",
435
+ 'shuttle.top': "Flytta först",
436
+ 'shuttle.bottom': "Flytta sist",
437
+ 'shuttle.filter': "Filtrera",
438
+ 'region.breadcrumb': "Brödsmulor",
439
+ 'region.no_help': "Det finns ingen hjälp för den här sidan.",
427
440
  'search.did_you_mean': "Menade du:",
428
441
  'error.lov_value': "{label}: välj ett värde i listan.",
429
442
  'item.yes': "Ja",
package/src/i18n/tr.ts CHANGED
@@ -424,6 +424,19 @@ export const tr: Record<MessageKey, string> = {
424
424
  'lov.loading': "Daha fazlası yükleniyor…",
425
425
  'da.wait': "Lütfen bekleyin…",
426
426
  'lov.no_rows': "Hiçbir şey bulunamadı.",
427
+ 'shuttle.available': "Kullanılabilir",
428
+ 'shuttle.chosen': "Seçilenler",
429
+ 'shuttle.move': "Taşı",
430
+ 'shuttle.move_all': "Tümünü taşı",
431
+ 'shuttle.remove': "Kaldır",
432
+ 'shuttle.remove_all': "Tümünü kaldır",
433
+ 'shuttle.up': "Yukarı taşı",
434
+ 'shuttle.down': "Aşağı taşı",
435
+ 'shuttle.top': "En üste taşı",
436
+ 'shuttle.bottom': "En alta taşı",
437
+ 'shuttle.filter': "Filtrele",
438
+ 'region.breadcrumb': "İçerik haritası",
439
+ 'region.no_help': "Bu sayfa için yardım yok.",
427
440
  'search.did_you_mean': "Bunu mu demek istediniz:",
428
441
  'error.lov_value': "{label}: listeden bir değer seçin.",
429
442
  'item.yes': "Evet",
package/src/i18n/uk.ts CHANGED
@@ -424,6 +424,19 @@ export const uk: Record<MessageKey, string> = {
424
424
  'lov.loading': "Завантаження…",
425
425
  'da.wait': "Зачекайте…",
426
426
  'lov.no_rows': "Нічого не знайдено.",
427
+ 'shuttle.available': "Доступні",
428
+ 'shuttle.chosen': "Вибрані",
429
+ 'shuttle.move': "Перемістити",
430
+ 'shuttle.move_all': "Перемістити всі",
431
+ 'shuttle.remove': "Прибрати",
432
+ 'shuttle.remove_all': "Прибрати всі",
433
+ 'shuttle.up': "Вгору",
434
+ 'shuttle.down': "Вниз",
435
+ 'shuttle.top': "На початок",
436
+ 'shuttle.bottom': "У кінець",
437
+ 'shuttle.filter': "Фільтр",
438
+ 'region.breadcrumb': "Навігаційний ланцюжок",
439
+ 'region.no_help': "Для цієї сторінки немає довідки.",
427
440
  'search.did_you_mean': "Можливо, ви мали на увазі:",
428
441
  'error.lov_value': "{label}: виберіть значення зі списку.",
429
442
  'item.yes': "Так",
package/src/i18n/zh.ts CHANGED
@@ -424,6 +424,19 @@ export const zh: Record<MessageKey, string> = {
424
424
  'lov.loading': "正在加载更多…",
425
425
  'da.wait': "请稍候…",
426
426
  'lov.no_rows': "未找到任何内容。",
427
+ 'shuttle.available': "可选",
428
+ 'shuttle.chosen': "已选",
429
+ 'shuttle.move': "移入",
430
+ 'shuttle.move_all': "全部移入",
431
+ 'shuttle.remove': "移除",
432
+ 'shuttle.remove_all': "全部移除",
433
+ 'shuttle.up': "上移",
434
+ 'shuttle.down': "下移",
435
+ 'shuttle.top': "移到顶部",
436
+ 'shuttle.bottom': "移到底部",
437
+ 'shuttle.filter': "筛选",
438
+ 'region.breadcrumb': "面包屑导航",
439
+ 'region.no_help': "此页面没有帮助信息。",
427
440
  'search.did_you_mean': "您是不是要找:",
428
441
  'error.lov_value': "{label}:请从列表中选择一个值。",
429
442
  'item.yes': "是",
package/src/i18n.ts CHANGED
@@ -451,6 +451,19 @@ const en = {
451
451
  'lov.loading': 'Loading more…',
452
452
  'da.wait': 'Please wait…',
453
453
  'lov.no_rows': 'Nothing found.',
454
+ 'shuttle.available': 'Available',
455
+ 'shuttle.chosen': 'Selected',
456
+ 'shuttle.move': 'Move',
457
+ 'shuttle.move_all': 'Move all',
458
+ 'shuttle.remove': 'Remove',
459
+ 'shuttle.remove_all': 'Remove all',
460
+ 'shuttle.up': 'Move up',
461
+ 'shuttle.down': 'Move down',
462
+ 'shuttle.top': 'Move to top',
463
+ 'shuttle.bottom': 'Move to bottom',
464
+ 'shuttle.filter': 'Filter',
465
+ 'region.breadcrumb': 'Breadcrumb',
466
+ 'region.no_help': 'No help is available for this page.',
454
467
  'search.did_you_mean': 'Did you mean:',
455
468
  'error.lov_value': '{label}: choose a value from the list.',
456
469
  'item.yes': 'Yes',
@@ -1319,6 +1332,19 @@ const nl: Record<MessageKey, string> = {
1319
1332
  'lov.loading': 'Meer laden…',
1320
1333
  'da.wait': 'Even geduld…',
1321
1334
  'lov.no_rows': 'Niets gevonden.',
1335
+ 'shuttle.available': 'Beschikbaar',
1336
+ 'shuttle.chosen': 'Gekozen',
1337
+ 'shuttle.move': 'Verplaatsen',
1338
+ 'shuttle.move_all': 'Alles verplaatsen',
1339
+ 'shuttle.remove': 'Verwijderen',
1340
+ 'shuttle.remove_all': 'Alles verwijderen',
1341
+ 'shuttle.up': 'Omhoog',
1342
+ 'shuttle.down': 'Omlaag',
1343
+ 'shuttle.top': 'Bovenaan',
1344
+ 'shuttle.bottom': 'Onderaan',
1345
+ 'shuttle.filter': 'Filteren',
1346
+ 'region.breadcrumb': 'Kruimelpad',
1347
+ 'region.no_help': 'Er is geen help voor deze pagina.',
1322
1348
  'search.did_you_mean': 'Bedoelde u:',
1323
1349
  'error.lov_value': '{label}: kies een waarde uit de lijst.',
1324
1350
  'item.yes': 'Ja',
package/src/metadata.ts CHANGED
@@ -37,8 +37,13 @@ export interface PageSummary {
37
37
  export interface StaticInclude {
38
38
  name: string;
39
39
  v: number;
40
+ /** (141) a static workspace file (the name without #WORKSPACE_FILES#) */
41
+ ws?: boolean;
40
42
  }
41
43
 
44
+ /** (141) The prefix of a static workspace file in a list of files to load. */
45
+ export const WORKSPACE_FILES = '#WORKSPACE_FILES#';
46
+
42
47
  export interface AppProcess {
43
48
  id: number;
44
49
  name: string;
@@ -159,7 +164,7 @@ export interface Region {
159
164
  title: string | null;
160
165
  /** (0.31, 073) optional, unique on the page */
161
166
  static_id?: string | null;
162
- type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search' | 'form_designer';
167
+ type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search' | 'form_designer' | 'classic_report' | 'breadcrumb' | 'help_text';
163
168
  source: string | null;
164
169
  table_name: string | null;
165
170
  pk_column: string | null;
@@ -182,6 +187,7 @@ export type ItemType =
182
187
  | 'checkbox' | 'switch' | 'hidden' | 'display' | 'password'
183
188
  | 'checkbox_group' | 'multiselect' | 'popup_lov' | 'email' | 'tel' | 'url' | 'color' | 'file' | 'location'
184
189
  | 'richtext' | 'markdown' | 'rating' | 'combobox' | 'daterange' | 'qrcode' | 'plugin'
190
+ | 'shuttle' | 'select_one' | 'select_many'
185
191
  // (086) display image, geocoded address
186
192
  | 'image' | 'address';
187
193
 
@@ -363,12 +369,23 @@ export interface Page extends PageSummary {
363
369
  warn_unsaved: boolean;
364
370
  /** (087) 'minimal': no header and navigation */
365
371
  page_template: 'standard' | 'minimal' | 'left_side_column' | 'right_side_column';
372
+ /** (140) the page's help text (Markdown), shown by a help_text region */
373
+ help: string | null;
366
374
  }
367
375
 
368
- /** The static files a list of names refers to, in that order, with their version. */
376
+ /**
377
+ * The static files a list of names refers to, in that order, with their version; (141) a name
378
+ * #WORKSPACE_FILES#x is the file x of the application's workspace (meta.workspace_file_versions:
379
+ * its own workspace's only). Missing files are left out.
380
+ */
369
381
  const includes = (names: string, appId: string) =>
370
- `coalesce((select jsonb_agg(jsonb_build_object('name', f.name, 'v', floor(extract(epoch from f.updated_at))::bigint) order by u.o)
371
- from unnest(${names}) with ordinality u(n, o) join meta.static_file f on f.app_id = ${appId} and f.name = u.n), '[]')`;
382
+ `coalesce((select jsonb_agg(x.j order by x.o) from (
383
+ select u.o, case when u.n like '#WORKSPACE\\_FILES#%'
384
+ then (select jsonb_build_object('name', substr(u.n, 18), 'v', (w.v ->> substr(u.n, 18))::bigint, 'ws', true) where w.v ? substr(u.n, 18))
385
+ else (select jsonb_build_object('name', f.name, 'v', floor(extract(epoch from f.updated_at))::bigint) from meta.static_file f where f.app_id = ${appId} and f.name = u.n) end as j
386
+ from unnest(${names}) with ordinality u(n, o)
387
+ left join lateral (select meta.workspace_file_versions(${appId}) as v where exists (select 1 from unnest(${names}) z where z like '#WORKSPACE\\_FILES#%')) w on true
388
+ ) x where x.j is not null), '[]')`;
372
389
 
373
390
  // Components whose build option is excluded are left out here, so the runtime
374
391
  // neither renders nor runs them (meta.build_option_on, migration 029).
@@ -406,7 +423,7 @@ export async function loadApp(alias: string) {
406
423
  export async function loadPage(appId: number, pageNo: number) {
407
424
  return runtime.one<Page>(
408
425
  `select p.id, p.app_id, p.page_no, p.name, p.title, p.requires_auth, p.parent_page, p.mode,
409
- p.protection, p.authz, p.warn_unsaved, p.page_template,
426
+ p.protection, p.authz, p.warn_unsaved, p.page_template, p.help,
410
427
  ${agg('meta.region', 'page_id', 'p', 'p.app_id')} as regions,
411
428
  ${agg('meta.item', 'page_id', 'p', 'p.app_id')} as items,
412
429
  ${agg('meta.button', 'page_id', 'p', 'p.app_id')} as buttons,
@@ -0,0 +1,133 @@
1
+ import { owner } from './db.ts';
2
+ import { WebError } from './webclient.ts';
3
+
4
+ // (sprint 141, AP-7; APEX: Remote Servers) A named base URL per workspace
5
+ // (meta.remote_server, migration 141): REST data sources (rest_source.server),
6
+ // REST source catalogs (rest_catalog.server) and AI services
7
+ // (ai_service.server_id) name a server and a path instead of a full URL, so
8
+ // moving from development to production changes one value. The URL is this
9
+ // installation's (base_url), or the server's environment variable
10
+ // PGKILN_SERVER_<...> when it is set (env_var wins). Only the prefix
11
+ // PGKILN_SERVER_ may be named, so no other variable of the server's
12
+ // environment (DATABASE_URL, keys) can become a URL or show up in a message.
13
+ //
14
+ // Resolving happens here, with the owner pool (the runtime role can't read
15
+ // the table), always within the application's own workspace: another
16
+ // workspace's server is never found. The URL it gives goes through every
17
+ // check a full URL went through (src/webclient.ts: allow-list, the
18
+ // workspace's network access list, address checks at connect time).
19
+
20
+ export interface RemoteServer {
21
+ id: number;
22
+ workspace_id: number;
23
+ static_id: string;
24
+ name: string;
25
+ description: string | null;
26
+ base_url: string | null;
27
+ env_var: string | null;
28
+ credential: string | null;
29
+ }
30
+
31
+ export const STATIC_ID = /^[A-Z][A-Z0-9_]{0,59}$/;
32
+ export const ENV_PREFIX = 'PGKILN_SERVER_';
33
+ export const ENV_VAR = /^PGKILN_SERVER_[A-Z0-9_]{1,60}$/;
34
+ const BASE_URL = /^https?:\/\/[^\s<>"{}?#]+$/i;
35
+
36
+ /** Problems with a base URL (empty: fine): http(s), no trailing slash, query, fragment or user name. */
37
+ export function baseUrlProblem(url: string): string | null {
38
+ if (!BASE_URL.test(url) || url.length > 2000) return 'The base URL starts with http:// or https://, without spaces, {parameters}, a query or a fragment, e.g. https://crm.example.com/api.';
39
+ if (url.endsWith('/')) return 'The base URL ends without a slash: the paths after it start with one.';
40
+ try {
41
+ const u = new URL(url);
42
+ if (u.username || u.password) return 'The base URL may not contain a user name or password: use a web credential.';
43
+ } catch {
44
+ return 'The base URL is not valid.';
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /** Problems with a server's fields (empty: fine). */
50
+ export function serverProblems(v: { static_id?: unknown; name?: unknown; base_url?: unknown; env_var?: unknown; credential?: unknown; description?: unknown }): string[] {
51
+ const out: string[] = [];
52
+ if (typeof v.static_id !== 'string' || !STATIC_ID.test(v.static_id)) out.push('The static id is upper case letters, digits and _, starting with a letter (at most 60), e.g. CRM.');
53
+ if (typeof v.name !== 'string' || !/^\S(.{0,98}\S)?$/.test(v.name)) out.push('The name is 1 to 100 characters, without spaces at the start or end.');
54
+ if (v.base_url != null && v.base_url !== '') {
55
+ const p = baseUrlProblem(String(v.base_url));
56
+ if (p) out.push(p);
57
+ }
58
+ if (v.env_var != null && v.env_var !== '' && (typeof v.env_var !== 'string' || !ENV_VAR.test(v.env_var)))
59
+ out.push(`The environment variable starts with ${ENV_PREFIX}, then upper case letters, digits and _, e.g. ${ENV_PREFIX}CRM.`);
60
+ if (!v.base_url && !v.env_var) out.push('Give the base URL, an environment variable that holds it, or both.');
61
+ if (v.credential != null && v.credential !== '' && (typeof v.credential !== 'string' || !STATIC_ID.test(v.credential))) out.push('The web credential is the name of one, e.g. CRM_API.');
62
+ if (v.description != null && (typeof v.description !== 'string' || v.description.length > 2000)) out.push('The description is at most 2000 characters.');
63
+ return out;
64
+ }
65
+
66
+ /**
67
+ * The URL a server stands for now: its environment variable when that is set (and valid), else its
68
+ * stored base URL. `problem` says why there is none (the variable's value is never repeated).
69
+ */
70
+ export function serverUrl(s: Pick<RemoteServer, 'static_id' | 'base_url' | 'env_var'>): { url: string | null; from: 'env' | 'stored' | null; problem: string | null } {
71
+ if (s.env_var && ENV_VAR.test(s.env_var)) {
72
+ const v = (process.env[s.env_var] ?? '').trim().replace(/\/+$/, '');
73
+ if (v) {
74
+ if (baseUrlProblem(v)) return { url: null, from: 'env', problem: `Remote server ${s.static_id}: the server's ${s.env_var} is not a valid base URL (http:// or https://, no query).` };
75
+ return { url: v, from: 'env', problem: null };
76
+ }
77
+ }
78
+ if (s.base_url && !baseUrlProblem(s.base_url)) return { url: s.base_url, from: 'stored', problem: null };
79
+ return { url: null, from: null, problem: `Remote server ${s.static_id} has no base URL${s.env_var ? ` (and ${s.env_var} is not set on the server)` : ''}.` };
80
+ }
81
+
82
+ /** "https://crm.example.com/api" + "/orders/{id}" (a path, a query, or nothing). */
83
+ export function joinUrl(base: string, path: string | null | undefined) {
84
+ const p = (path ?? '').trim();
85
+ if (!p) return base;
86
+ return p.startsWith('/') || p.startsWith('?') ? `${base}${p}` : `${base}/${p}`;
87
+ }
88
+
89
+ /** A server of a workspace by static id (owner pool), or null. */
90
+ export async function workspaceServer(workspaceId: number, staticId: string): Promise<RemoteServer | null> {
91
+ return (await owner.one<RemoteServer>('select * from meta.remote_server where workspace_id = $1 and static_id = $2', [workspaceId, staticId.toUpperCase()])) ?? null;
92
+ }
93
+
94
+ /** A server of the application's own workspace by static id, or null. */
95
+ export async function serverForApp(appId: number, staticId: string): Promise<RemoteServer | null> {
96
+ return (await owner.one<RemoteServer>(
97
+ 'select * from meta.remote_server where workspace_id = meta.app_workspace($1) and static_id = $2', [appId, staticId.toUpperCase()])) ?? null;
98
+ }
99
+
100
+ /** The full URL of a path on a server of the application's workspace; throws a WebError when it can't be made. */
101
+ export async function appServerUrl(appId: number, staticId: string, path: string | null | undefined) {
102
+ const s = await serverForApp(appId, staticId);
103
+ if (!s) throw new WebError(`Remote server ${staticId.toUpperCase()} does not exist in this application's workspace (Workspace utilities → Remote servers).`);
104
+ const u = serverUrl(s);
105
+ if (!u.url) throw new WebError(u.problem!);
106
+ return { url: joinUrl(u.url, path), server: s };
107
+ }
108
+
109
+ /**
110
+ * A REST data source as the request code expects it: on a server, its URL becomes the server's base
111
+ * URL plus the source's path, and a source without a credential of its own uses the server's.
112
+ */
113
+ export async function resolveSource<T extends { app_id: number; url: string; credential: string | null; server?: string | null }>(src: T): Promise<T> {
114
+ if (!src.server) return src;
115
+ const { url, server } = await appServerUrl(src.app_id, src.server, src.url);
116
+ // resolved once: server null, so resolving again changes nothing
117
+ return { ...src, url, credential: src.credential || server.credential || null, server: null };
118
+ }
119
+
120
+ /** The base URL of a catalog on a server of its workspace (null: none, or the catalog has no server). */
121
+ export async function catalogServerUrl(workspaceId: number, staticId: string | null | undefined): Promise<string | null> {
122
+ if (!staticId) return null;
123
+ const s = await workspaceServer(workspaceId, staticId);
124
+ return s ? serverUrl(s).url : null;
125
+ }
126
+
127
+ /** The base URL of an AI service on a server (any workspace: administrators chose it), or an error message. */
128
+ export async function aiServerUrl(serverId: number, path: string | null | undefined): Promise<{ url: string | null; problem: string | null }> {
129
+ const s = await owner.one<RemoteServer>('select * from meta.remote_server where id = $1', [serverId]);
130
+ if (!s) return { url: null, problem: 'Its remote server no longer exists.' };
131
+ const u = serverUrl(s);
132
+ return u.url ? { url: joinUrl(u.url, path), problem: null } : { url: null, problem: u.problem };
133
+ }