pgkiln 0.82.0 → 0.84.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.
- package/db/migrations/153_component_events.sql +21 -0
- package/db/migrations/154_notifications.sql +186 -0
- package/docs/apex-feature-parity.md +4 -3
- package/docs/guide/03-builder.md +10 -1
- package/docs/guide/04-pages-and-regions.md +44 -1
- package/docs/guide/06-processing.md +22 -1
- package/docs/guide/07-dynamic-actions.md +28 -0
- package/docs/guide/09-reference.md +15 -0
- package/docs/guide/11-from-apex.md +3 -1
- package/docs/guide/12-development.md +16 -1
- package/package.json +1 -1
- package/public/app.css +31 -0
- package/public/app.js +163 -4
- package/public/builder.css +25 -2
- package/public/data-modeler.js +8 -5
- package/public/sql-monitor.js +5 -2
- package/public/sw.js +5 -1
- package/src/app.ts +2 -0
- package/src/builder/ai-builder.ts +50 -46
- package/src/builder/api.ts +49 -49
- package/src/builder/autorest.ts +92 -87
- package/src/builder/changelog.ts +39 -37
- package/src/builder/code-editor.ts +1 -1
- package/src/builder/components.ts +13 -8
- package/src/builder/data-modeler.ts +89 -84
- package/src/builder/data-security.ts +39 -37
- package/src/builder/dataload.ts +69 -65
- package/src/builder/datapump.ts +108 -97
- package/src/builder/ddl-tools.ts +38 -34
- package/src/builder/document-view.ts +11 -11
- package/src/builder/i18n/de.ts +1635 -0
- package/src/builder/i18n/en.ts +1635 -0
- package/src/builder/i18n/es.ts +1635 -0
- package/src/builder/i18n/fr.ts +1635 -0
- package/src/builder/i18n/it.ts +1635 -0
- package/src/builder/i18n/ja.ts +1635 -0
- package/src/builder/i18n/ko.ts +1635 -0
- package/src/builder/i18n/nl.ts +1635 -0
- package/src/builder/i18n/pt.ts +1635 -0
- package/src/builder/i18n/zh-cn.ts +1635 -0
- package/src/builder/i18n/zh-tw.ts +1635 -0
- package/src/builder/i18n.ts +18 -0
- package/src/builder/notification-settings.ts +27 -0
- package/src/builder/object-actions.ts +18 -16
- package/src/builder/object-browser.ts +2 -2
- package/src/builder/querybuilder.ts +42 -36
- package/src/builder/quicksql.ts +16 -18
- package/src/builder/region-settings.ts +201 -199
- package/src/builder/report-settings.ts +64 -60
- package/src/builder/rest-modules.ts +145 -143
- package/src/builder/routes.ts +3 -0
- package/src/builder/sampledata.ts +59 -52
- package/src/builder/scheduling.ts +180 -159
- package/src/builder/scripts.ts +71 -65
- package/src/builder/sql-charts.ts +36 -30
- package/src/builder/sql-monitor.ts +95 -89
- package/src/builder/sql-utilities.ts +47 -40
- package/src/builder/top-sql.ts +21 -17
- package/src/builder/unload.ts +48 -44
- package/src/builder/worksheet.ts +3 -3
- package/src/cli/replace.ts +2 -0
- package/src/cron-schedule.ts +47 -17
- package/src/i18n/ar.ts +9 -0
- package/src/i18n/cs.ts +9 -0
- package/src/i18n/da.ts +9 -0
- package/src/i18n/de.ts +9 -0
- package/src/i18n/el.ts +9 -0
- package/src/i18n/es.ts +9 -0
- package/src/i18n/fi.ts +9 -0
- package/src/i18n/fr.ts +9 -0
- package/src/i18n/he.ts +9 -0
- package/src/i18n/it.ts +9 -0
- package/src/i18n/ja.ts +9 -0
- package/src/i18n/ko.ts +9 -0
- package/src/i18n/nb.ts +9 -0
- package/src/i18n/pl.ts +9 -0
- package/src/i18n/pt.ts +9 -0
- package/src/i18n/ru.ts +9 -0
- package/src/i18n/sv.ts +9 -0
- package/src/i18n/tr.ts +9 -0
- package/src/i18n/uk.ts +9 -0
- package/src/i18n/zh.ts +9 -0
- package/src/i18n.ts +18 -0
- package/src/metadata.ts +11 -3
- package/src/runtime/calendar.ts +3 -2
- package/src/runtime/context.ts +2 -0
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/feedback.ts +1 -1
- package/src/runtime/grid.ts +1 -0
- package/src/runtime/lists.ts +43 -11
- package/src/runtime/maps.ts +7 -4
- package/src/runtime/notifications.ts +237 -0
- package/src/runtime/processes.ts +45 -1
- package/src/runtime/regions.ts +11 -15
- package/src/runtime/render.ts +18 -4
- package/src/runtime/routes.ts +16 -4
- package/src/runtime/signin.ts +2 -1
- package/src/runtime/tree.ts +4 -3
- package/src/session.ts +13 -4
package/src/i18n/ko.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const ko: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "'로그인 유지'를 선택한 브라우저: {count}개.",
|
|
311
311
|
'account.forget_devices': "모든 기기에서 로그아웃",
|
|
312
312
|
'account.devices_forgotten': "모든 기기에서 로그아웃했습니다.",
|
|
313
|
+
'notify.title': "알림",
|
|
314
|
+
'notify.none': "알림이 없습니다.",
|
|
315
|
+
'notify.mark_all_read': "모두 읽음으로 표시",
|
|
316
|
+
'notify.all': "모든 알림",
|
|
317
|
+
'notify.marked': "알림을 읽음으로 표시했습니다.",
|
|
318
|
+
'notify.gone': "이 알림은 더 이상 없습니다.",
|
|
319
|
+
'notify.bell_one': "알림: 읽지 않음 1개",
|
|
320
|
+
'notify.bell_n': "알림: 읽지 않음 {n}개",
|
|
321
|
+
'notify.unread_one': "읽지 않음",
|
|
313
322
|
'push.title': "알림",
|
|
314
323
|
'push.help': "앱이 닫혀 있을 때도 이 기기에서 이 앱의 알림을 받습니다.",
|
|
315
324
|
'push.devices': "알림을 받는 기기: {n}.",
|
package/src/i18n/nb.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const nb: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "Nettlesere der du valgte «Hold meg pålogget»: {count}.",
|
|
311
311
|
'account.forget_devices': "Logg ut på alle enheter",
|
|
312
312
|
'account.devices_forgotten': "Logget ut på alle enheter.",
|
|
313
|
+
'notify.title': "Varsler",
|
|
314
|
+
'notify.none': "Ingen varsler.",
|
|
315
|
+
'notify.mark_all_read': "Merk alle som lest",
|
|
316
|
+
'notify.all': "Alle varsler",
|
|
317
|
+
'notify.marked': "Varslene er merket som lest.",
|
|
318
|
+
'notify.gone': "Dette varselet finnes ikke lenger.",
|
|
319
|
+
'notify.bell_one': "Varsler: 1 ulest",
|
|
320
|
+
'notify.bell_n': "Varsler: {n} uleste",
|
|
321
|
+
'notify.unread_one': "Ulest",
|
|
313
322
|
'push.title': "Varsler",
|
|
314
323
|
'push.help': "Få varsler fra denne appen på denne enheten, også når appen er lukket.",
|
|
315
324
|
'push.devices': "Enheter med varsler: {n}.",
|
package/src/i18n/pl.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const pl: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "Przeglądarki, w których wybrano „Nie wylogowuj mnie”: {count}.",
|
|
311
311
|
'account.forget_devices': "Wyloguj na wszystkich urządzeniach",
|
|
312
312
|
'account.devices_forgotten': "Wylogowano na wszystkich urządzeniach.",
|
|
313
|
+
'notify.title': "Powiadomienia",
|
|
314
|
+
'notify.none': "Brak powiadomień.",
|
|
315
|
+
'notify.mark_all_read': "Oznacz wszystkie jako przeczytane",
|
|
316
|
+
'notify.all': "Wszystkie powiadomienia",
|
|
317
|
+
'notify.marked': "Powiadomienia oznaczono jako przeczytane.",
|
|
318
|
+
'notify.gone': "To powiadomienie już nie istnieje.",
|
|
319
|
+
'notify.bell_one': "Powiadomienia: 1 nieprzeczytane",
|
|
320
|
+
'notify.bell_n': "Powiadomienia: nieprzeczytane: {n}",
|
|
321
|
+
'notify.unread_one': "Nieprzeczytane",
|
|
313
322
|
'push.title': "Powiadomienia",
|
|
314
323
|
'push.help': "Otrzymuj powiadomienia z tej aplikacji na tym urządzeniu, także gdy aplikacja jest zamknięta.",
|
|
315
324
|
'push.devices': "Urządzenia z powiadomieniami: {n}.",
|
package/src/i18n/pt.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const pt: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "Navegadores onde escolheu “Manter a sessão iniciada”: {count}.",
|
|
311
311
|
'account.forget_devices': "Terminar sessão em todos os dispositivos",
|
|
312
312
|
'account.devices_forgotten': "Sessão terminada em todos os dispositivos.",
|
|
313
|
+
'notify.title': "Notificações",
|
|
314
|
+
'notify.none': "Sem notificações.",
|
|
315
|
+
'notify.mark_all_read': "Marcar todas como lidas",
|
|
316
|
+
'notify.all': "Todas as notificações",
|
|
317
|
+
'notify.marked': "Notificações marcadas como lidas.",
|
|
318
|
+
'notify.gone': "Esta notificação já não existe.",
|
|
319
|
+
'notify.bell_one': "Notificações: 1 por ler",
|
|
320
|
+
'notify.bell_n': "Notificações: {n} por ler",
|
|
321
|
+
'notify.unread_one': "Por ler",
|
|
313
322
|
'push.title': "Notificações",
|
|
314
323
|
'push.help': "Receba notificações desta aplicação neste dispositivo, mesmo com a aplicação fechada.",
|
|
315
324
|
'push.devices': "Dispositivos com notificações: {n}.",
|
package/src/i18n/ru.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const ru: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "Браузеров, где выбрано «Не выходить»: {count}.",
|
|
311
311
|
'account.forget_devices': "Выйти на всех устройствах",
|
|
312
312
|
'account.devices_forgotten': "Выход выполнен на всех устройствах.",
|
|
313
|
+
'notify.title': "Уведомления",
|
|
314
|
+
'notify.none': "Уведомлений нет.",
|
|
315
|
+
'notify.mark_all_read': "Отметить все как прочитанные",
|
|
316
|
+
'notify.all': "Все уведомления",
|
|
317
|
+
'notify.marked': "Уведомления отмечены как прочитанные.",
|
|
318
|
+
'notify.gone': "Этого уведомления больше нет.",
|
|
319
|
+
'notify.bell_one': "Уведомления: 1 непрочитанное",
|
|
320
|
+
'notify.bell_n': "Уведомления: непрочитанных — {n}",
|
|
321
|
+
'notify.unread_one': "Непрочитанное",
|
|
313
322
|
'push.title': "Уведомления",
|
|
314
323
|
'push.help': "Получайте уведомления этого приложения на этом устройстве, даже когда приложение закрыто.",
|
|
315
324
|
'push.devices': "Устройства с уведомлениями: {n}.",
|
package/src/i18n/sv.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const sv: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "Webbläsare där du valde ”Håll mig inloggad”: {count}.",
|
|
311
311
|
'account.forget_devices': "Logga ut på alla enheter",
|
|
312
312
|
'account.devices_forgotten': "Utloggad på alla enheter.",
|
|
313
|
+
'notify.title': "Aviseringar",
|
|
314
|
+
'notify.none': "Inga aviseringar.",
|
|
315
|
+
'notify.mark_all_read': "Markera alla som lästa",
|
|
316
|
+
'notify.all': "Alla aviseringar",
|
|
317
|
+
'notify.marked': "Aviseringarna har markerats som lästa.",
|
|
318
|
+
'notify.gone': "Den här aviseringen finns inte längre.",
|
|
319
|
+
'notify.bell_one': "Aviseringar: 1 oläst",
|
|
320
|
+
'notify.bell_n': "Aviseringar: {n} olästa",
|
|
321
|
+
'notify.unread_one': "Oläst",
|
|
313
322
|
'push.title': "Aviseringar",
|
|
314
323
|
'push.help': "Få aviseringar från den här appen på den här enheten, även när appen är stängd.",
|
|
315
324
|
'push.devices': "Enheter med aviseringar: {n}.",
|
package/src/i18n/tr.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const tr: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "“Oturumum açık kalsın” seçtiğiniz tarayıcılar: {count}.",
|
|
311
311
|
'account.forget_devices': "Tüm cihazlarda oturumu kapat",
|
|
312
312
|
'account.devices_forgotten': "Tüm cihazlarda oturum kapatıldı.",
|
|
313
|
+
'notify.title': "Bildirimler",
|
|
314
|
+
'notify.none': "Bildirim yok.",
|
|
315
|
+
'notify.mark_all_read': "Tümünü okundu olarak işaretle",
|
|
316
|
+
'notify.all': "Tüm bildirimler",
|
|
317
|
+
'notify.marked': "Bildirimler okundu olarak işaretlendi.",
|
|
318
|
+
'notify.gone': "Bu bildirim artık yok.",
|
|
319
|
+
'notify.bell_one': "Bildirimler: 1 okunmamış",
|
|
320
|
+
'notify.bell_n': "Bildirimler: {n} okunmamış",
|
|
321
|
+
'notify.unread_one': "Okunmamış",
|
|
313
322
|
'push.title': "Bildirimler",
|
|
314
323
|
'push.help': "Uygulama kapalıyken de bu cihazda bu uygulamanın bildirimlerini alın.",
|
|
315
324
|
'push.devices': "Bildirim alan cihazlar: {n}.",
|
package/src/i18n/uk.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const uk: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "Браузерів, де вибрано «Не виходити»: {count}.",
|
|
311
311
|
'account.forget_devices': "Вийти на всіх пристроях",
|
|
312
312
|
'account.devices_forgotten': "Вихід виконано на всіх пристроях.",
|
|
313
|
+
'notify.title': "Сповіщення",
|
|
314
|
+
'notify.none': "Сповіщень немає.",
|
|
315
|
+
'notify.mark_all_read': "Позначити всі як прочитані",
|
|
316
|
+
'notify.all': "Усі сповіщення",
|
|
317
|
+
'notify.marked': "Сповіщення позначено як прочитані.",
|
|
318
|
+
'notify.gone': "Цього сповіщення більше немає.",
|
|
319
|
+
'notify.bell_one': "Сповіщення: 1 непрочитане",
|
|
320
|
+
'notify.bell_n': "Сповіщення: непрочитаних — {n}",
|
|
321
|
+
'notify.unread_one': "Непрочитане",
|
|
313
322
|
'push.title': "Сповіщення",
|
|
314
323
|
'push.help': "Отримуйте сповіщення цього застосунку на цьому пристрої, навіть коли застосунок закрито.",
|
|
315
324
|
'push.devices': "Пристрої зі сповіщеннями: {n}.",
|
package/src/i18n/zh.ts
CHANGED
|
@@ -310,6 +310,15 @@ export const zh: Record<MessageKey, string> = {
|
|
|
310
310
|
'account.devices_help': "您选择了“保持登录”的浏览器:{count}。",
|
|
311
311
|
'account.forget_devices': "在所有设备上退出登录",
|
|
312
312
|
'account.devices_forgotten': "已在所有设备上退出登录。",
|
|
313
|
+
'notify.title': "通知",
|
|
314
|
+
'notify.none': "没有通知。",
|
|
315
|
+
'notify.mark_all_read': "全部标为已读",
|
|
316
|
+
'notify.all': "所有通知",
|
|
317
|
+
'notify.marked': "通知已标为已读。",
|
|
318
|
+
'notify.gone': "此通知已不存在。",
|
|
319
|
+
'notify.bell_one': "通知:1 条未读",
|
|
320
|
+
'notify.bell_n': "通知:{n} 条未读",
|
|
321
|
+
'notify.unread_one': "未读",
|
|
313
322
|
'push.title': "通知",
|
|
314
323
|
'push.help': "在此设备上接收此应用的通知,即使应用已关闭。",
|
|
315
324
|
'push.devices': "接收通知的设备:{n}。",
|
package/src/i18n.ts
CHANGED
|
@@ -337,6 +337,15 @@ const en = {
|
|
|
337
337
|
'account.devices_help': 'Browsers where you chose "Keep me signed in": {count}.',
|
|
338
338
|
'account.forget_devices': 'Sign out on all devices',
|
|
339
339
|
'account.devices_forgotten': 'Signed out on all devices.',
|
|
340
|
+
'notify.title': "Notifications",
|
|
341
|
+
'notify.none': "No notifications.",
|
|
342
|
+
'notify.mark_all_read': "Mark all read",
|
|
343
|
+
'notify.all': "All notifications",
|
|
344
|
+
'notify.marked': "Notifications marked as read.",
|
|
345
|
+
'notify.gone': "This notification no longer exists.",
|
|
346
|
+
'notify.bell_one': "Notifications: 1 unread",
|
|
347
|
+
'notify.bell_n': "Notifications: {n} unread",
|
|
348
|
+
'notify.unread_one': "Unread",
|
|
340
349
|
'push.title': "Notifications",
|
|
341
350
|
'push.help': "Get notifications from this app on this device, also when the app is closed.",
|
|
342
351
|
'push.devices': "Devices with notifications: {n}.",
|
|
@@ -1247,6 +1256,15 @@ const nl: Record<MessageKey, string> = {
|
|
|
1247
1256
|
'account.devices_help': 'Browsers waarin je "Ingelogd blijven" koos: {count}.',
|
|
1248
1257
|
'account.forget_devices': 'Overal afmelden',
|
|
1249
1258
|
'account.devices_forgotten': 'Op alle apparaten afgemeld.',
|
|
1259
|
+
'notify.title': "Meldingen",
|
|
1260
|
+
'notify.none': "Geen meldingen.",
|
|
1261
|
+
'notify.mark_all_read': "Alles gelezen",
|
|
1262
|
+
'notify.all': "Alle meldingen",
|
|
1263
|
+
'notify.marked': "Meldingen als gelezen gemarkeerd.",
|
|
1264
|
+
'notify.gone': "Deze melding bestaat niet meer.",
|
|
1265
|
+
'notify.bell_one': "Meldingen: 1 ongelezen",
|
|
1266
|
+
'notify.bell_n': "Meldingen: {n} ongelezen",
|
|
1267
|
+
'notify.unread_one': "Ongelezen",
|
|
1250
1268
|
'push.title': "Meldingen",
|
|
1251
1269
|
'push.help': "Ontvang meldingen van deze app op dit apparaat, ook als de app gesloten is.",
|
|
1252
1270
|
'push.devices': "Apparaten met meldingen: {n}.",
|
package/src/metadata.ts
CHANGED
|
@@ -112,6 +112,10 @@ export interface App {
|
|
|
112
112
|
availability_users: string[];
|
|
113
113
|
/** (084) users can send feedback from the navigation bar */
|
|
114
114
|
feedback: boolean;
|
|
115
|
+
/** (154) the notification bell in the header */
|
|
116
|
+
notifications: boolean;
|
|
117
|
+
/** (154) refresh the badges every so many seconds (null: never) */
|
|
118
|
+
live_badges_s: number | null;
|
|
115
119
|
/** (107) the application's version (Release 1.0), shown in its pages' footer */
|
|
116
120
|
version: string | null;
|
|
117
121
|
/** (085) schema.function(jsonb) returns jsonb that rewrites error messages; seconds of the session timeout warning */
|
|
@@ -320,7 +324,9 @@ export interface DynamicAction {
|
|
|
320
324
|
/** (124) a menu button's entry {"action": NAME} was chosen (trigger: entry or menu button names) */
|
|
321
325
|
| 'menu_entry'
|
|
322
326
|
/** (149) */
|
|
323
|
-
| 'double_click' | 'key_down' | 'key_press' | 'mouse_enter' | 'mouse_leave' | 'select' | 'resize' | 'scroll' | 'page_unload' | 'before_refresh' | 'facets_change'
|
|
327
|
+
| 'double_click' | 'key_down' | 'key_press' | 'mouse_enter' | 'mouse_leave' | 'select' | 'resize' | 'scroll' | 'page_unload' | 'before_refresh' | 'facets_change'
|
|
328
|
+
/** (153) component events: trigger = the grid, calendar, map or tree region */
|
|
329
|
+
| 'grid_row_init' | 'grid_save' | 'calendar_date_select' | 'calendar_view_change' | 'map_object_click' | 'map_change' | 'tree_selection_change' | 'timer';
|
|
324
330
|
trigger_element: string | null;
|
|
325
331
|
condition_type: 'equals' | 'not_equals' | 'in_list' | 'is_null' | 'is_not_null' | 'greater' | 'greater_equal' | 'less' | 'less_equal' | 'not_in_list' | null;
|
|
326
332
|
condition_value: string | null;
|
|
@@ -381,7 +387,9 @@ export interface Process {
|
|
|
381
387
|
type: 'form_dml' | 'grid_dml' | 'sql' | 'data_load' | 'invoke_api' | 'download' | 'chain' | 'workflow' | 'ai_generate' | 'plugin' | 'send_push' | 'send_email'
|
|
382
388
|
/** (150) */
|
|
383
389
|
| 'close_dialog' | 'clear_session_state' | 'reset_pagination' | 'user_preferences' | 'server_geocoding' | 'print_report'
|
|
384
|
-
| 'human_task_create' | 'human_task_manage' | 'form_init'
|
|
390
|
+
| 'human_task_create' | 'human_task_manage' | 'form_init'
|
|
391
|
+
/** (154) */
|
|
392
|
+
| 'send_notification';
|
|
385
393
|
region_id: number | null;
|
|
386
394
|
code: string | null;
|
|
387
395
|
config: Record<string, unknown> | null;
|
|
@@ -441,7 +449,7 @@ const agg = (table: string, fk: string, parent: string, appId: string) =>
|
|
|
441
449
|
export async function loadApp(alias: string) {
|
|
442
450
|
return runtime.one<App>(
|
|
443
451
|
`select a.id, a.alias, a.name, a.version, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.mfa, a.mfa_roles, a.mfa_email, a.api_self_tokens, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
|
|
444
|
-
a.availability, a.availability_message, a.availability_users, a.feedback, a.error_function, a.session_warning_seconds, a.component_settings,
|
|
452
|
+
a.availability, a.availability_message, a.availability_users, a.feedback, a.notifications, a.live_badges_s, a.error_function, a.session_warning_seconds, a.component_settings,
|
|
445
453
|
a.language, a.languages, a.language_from, a.date_format, a.timestamp_format, a.time_zone, a.time_zone_auto, a.currency,
|
|
446
454
|
coalesce((select jsonb_agg(jsonb_build_object('name', l.name, 'query', l.query, 'rest_source', l.rest_source)) from meta.lov l where l.app_id = a.id), '[]') as lovs,
|
|
447
455
|
coalesce((select jsonb_agg(jsonb_build_object('page_no', p.page_no, 'name', p.name, 'title', p.title,
|
package/src/runtime/calendar.ts
CHANGED
|
@@ -229,7 +229,8 @@ export async function renderCalendar(ctx: PageContext, r: Region): Promise<Raw>
|
|
|
229
229
|
return html`<a class="cal-add" ${linkAttrs(ctx, create.page, items)} aria-label="${label}" title="${label}">+</a>`;
|
|
230
230
|
};
|
|
231
231
|
const cellAttrs = (day: Date, hour: number | null) =>
|
|
232
|
-
|
|
232
|
+
// (161) data-date: what a calendar_date_select dynamic action's condition sees
|
|
233
|
+
raw(`${movable ? ` data-drop="${iso(day)}${hour === null ? '' : `T${pad(hour)}:00`}"` : ''}${createOk ? ' data-add' : ''} data-date="${iso(day)}${hour === null ? '' : `T${pad(hour)}:00`}"`);
|
|
233
234
|
const on = (day: string) => events.filter((ev) => ev.start <= day && ev.end >= day);
|
|
234
235
|
const go = (v: CalendarView, day: Date | null) =>
|
|
235
236
|
regionUrl(ctx, r, (p) => {
|
|
@@ -342,7 +343,7 @@ export async function renderCalendar(ctx: PageContext, r: Region): Promise<Raw>
|
|
|
342
343
|
(v) => html`<a class="btn${v === view ? ' active' : ''}" href="${go(v, anchor)}"${v === view ? raw(' aria-current="page"') : ''}>${t(`calendar.view_${v}`)}</a>`,
|
|
343
344
|
)}</nav>`
|
|
344
345
|
: '';
|
|
345
|
-
return html`<div class="calendar cal-view-${view}"${movable ? raw(` data-calendar="${r.id}"`) : ''}>
|
|
346
|
+
return html`<div class="calendar cal-view-${view}" data-view="${view}"${movable ? raw(` data-calendar="${r.id}"`) : ''}>
|
|
346
347
|
<div class="cal-toolbar">
|
|
347
348
|
<div class="buttons">${nav('‹', prev, prevLabel)}${nav(t('calendar.today'), null)}${nav('›', next, nextLabel)}</div>
|
|
348
349
|
<h3 class="cal-title" aria-live="polite">${title}</h3>
|
package/src/runtime/context.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface PageContext {
|
|
|
38
38
|
ip: string;
|
|
39
39
|
errors: Errors;
|
|
40
40
|
messages: string[];
|
|
41
|
+
/** (161) grid regions whose changes this request saved (POST), or the request before it saved (GET): grid_save dynamic actions */
|
|
42
|
+
savedGrids?: number[];
|
|
41
43
|
/** rendered inside a modal dialog (iframe) */
|
|
42
44
|
dialog: boolean;
|
|
43
45
|
client?: Client;
|
package/src/runtime/engine.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { parseDateMask } from './format.ts';
|
|
|
18
18
|
import { conditionHolds, functionBody } from './logic.ts';
|
|
19
19
|
import { runPluginProcess } from './plugins.ts';
|
|
20
20
|
import { runDeclarative } from './process-types.ts';
|
|
21
|
-
import { BACKGROUND_TYPES, downloadFile, sendEmailProcess, sendPushProcess, workflowProcess, type ChainConfig } from './processes.ts';
|
|
21
|
+
import { BACKGROUND_TYPES, downloadFile, sendEmailProcess, sendNotificationProcess, sendPushProcess, workflowProcess, type ChainConfig } from './processes.ts';
|
|
22
22
|
import { bindValues, dbg, publicError, stripSemicolon, substitute, timed, toState, type Errors, type PageContext } from './context.ts';
|
|
23
23
|
|
|
24
24
|
const ident = pg.escapeIdentifier;
|
|
@@ -621,6 +621,7 @@ async function runOneStep(ctx: PageContext, p: Process, names: Set<string>, dept
|
|
|
621
621
|
case 'invoke_api': return await invokeApi(ctx, p, names);
|
|
622
622
|
case 'workflow': return await workflowProcess(ctx, p, names);
|
|
623
623
|
case 'send_push': return await sendPushProcess(ctx, p);
|
|
624
|
+
case 'send_notification': return await sendNotificationProcess(ctx, p);
|
|
624
625
|
case 'send_email': return await sendEmailProcess(ctx, p);
|
|
625
626
|
case 'ai_generate': return (await runAiProcess(ctx, p, names)).message;
|
|
626
627
|
case 'plugin': return await runPluginProcess(ctx, p.config, p.name);
|
package/src/runtime/feedback.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function feedbackNode(ctx: PageContext): ListNode | null {
|
|
|
29
29
|
if (!ctx.app.feedback || ctx.dialog) return null;
|
|
30
30
|
return {
|
|
31
31
|
label: ctx.locale.t('feedback.link'), href: `${ctx.base}/${ctx.page.page_no}/feedback`, modal: false, external: false,
|
|
32
|
-
icon: 'chat', badge: null, description: null, current: false, inTrail: false, children: [],
|
|
32
|
+
icon: 'chat', badge: null, key: '__feedback', description: null, current: false, inTrail: false, children: [],
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
package/src/runtime/grid.ts
CHANGED
|
@@ -831,5 +831,6 @@ export async function gridDml(ctx: PageContext, p: Process): Promise<string | nu
|
|
|
831
831
|
if (errors.length) throw new Error(errors.join(' '));
|
|
832
832
|
const t = ctx.locale.t;
|
|
833
833
|
const parts = [inserted && t('grid.added', { n: inserted }), updated && t('grid.updated', { n: updated }), deleted && t('grid.deleted', { n: deleted })].filter(Boolean);
|
|
834
|
+
(ctx.savedGrids ??= []).push(r.id);
|
|
834
835
|
return p.success_message ?? (parts.length ? t('grid.saved_parts', { parts: parts.join(', ') }) : t('grid.no_changes'));
|
|
835
836
|
}
|
package/src/runtime/lists.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface ListNode {
|
|
|
28
28
|
external: boolean;
|
|
29
29
|
icon: string | null;
|
|
30
30
|
badge: string | null;
|
|
31
|
+
/** (154) the entry's id (a query's id column, else its label): finds its badge again for a live refresh */
|
|
32
|
+
key: string;
|
|
31
33
|
description: string | null;
|
|
32
34
|
/** the entry's page is the current page */
|
|
33
35
|
current: boolean;
|
|
@@ -50,6 +52,8 @@ interface Entry {
|
|
|
50
52
|
url: string | null;
|
|
51
53
|
icon: string | null;
|
|
52
54
|
badge: string | null;
|
|
55
|
+
/** (154) a SELECT whose first value is the badge (static entries; wins over badge) */
|
|
56
|
+
badge_query?: string | null;
|
|
53
57
|
description: string | null;
|
|
54
58
|
condition?: string | null;
|
|
55
59
|
authz?: string | null;
|
|
@@ -105,7 +109,7 @@ async function entriesOf(ctx: PageContext, name: string): Promise<Entry[] | null
|
|
|
105
109
|
}
|
|
106
110
|
const rows = (
|
|
107
111
|
await runtime.query(
|
|
108
|
-
`select id, parent_id, label, target_page, target_items, target_url, icon, badge, description, condition, authz
|
|
112
|
+
`select id, parent_id, label, target_page, target_items, target_url, icon, badge, badge_query, description, condition, authz
|
|
109
113
|
from meta.list_entry where app_id = $1 and list_name = $2 and meta.build_option_on(app_id, build_option)
|
|
110
114
|
order by seq, id`,
|
|
111
115
|
[ctx.app.id, name.toUpperCase()],
|
|
@@ -113,10 +117,28 @@ async function entriesOf(ctx: PageContext, name: string): Promise<Entry[] | null
|
|
|
113
117
|
).rows;
|
|
114
118
|
return rows.map((r) => ({
|
|
115
119
|
id: r.id, parent_id: r.parent_id, label: ctx.locale.tr(r.label), page: r.target_page, items: r.target_items ?? {}, url: r.target_url,
|
|
116
|
-
icon: r.icon, badge: r.badge, description: r.description ? ctx.locale.tr(r.description) : null, condition: r.condition, authz: r.authz,
|
|
120
|
+
icon: r.icon, badge: r.badge, badge_query: r.badge_query, description: r.description ? ctx.locale.tr(r.description) : null, condition: r.condition, authz: r.authz,
|
|
117
121
|
}));
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
/**
|
|
125
|
+
* (029, 154) A badge from SQL: the first value of the query's first row, run as the application's
|
|
126
|
+
* role with the page's binds; an error is a page error and no badge. Empty values give no badge.
|
|
127
|
+
*/
|
|
128
|
+
export async function queryBadge(ctx: PageContext, query: string, what: string): Promise<string | null> {
|
|
129
|
+
const c = ctx.client;
|
|
130
|
+
if (!c) return null;
|
|
131
|
+
try {
|
|
132
|
+
const sql = stripSemicolon(applyBinds(query, bindValues(ctx)));
|
|
133
|
+
const res = await savepoint(c, () => c.query({ text: sql, rowMode: 'array' }));
|
|
134
|
+
const v = res.rows[0]?.[0];
|
|
135
|
+
return v === null || v === undefined ? null : String(v).trim().slice(0, 100) || null;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
ctx.errors.page.push(await publicError(ctx, e, what));
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
/** The current page and its parent pages (for "in trail"). */
|
|
121
143
|
function trail(ctx: PageContext) {
|
|
122
144
|
const pages = new Set<number>();
|
|
@@ -156,7 +178,10 @@ export async function listTree(ctx: PageContext, name: string): Promise<ListNode
|
|
|
156
178
|
modal: !!e.page && isModal(ctx, e.page),
|
|
157
179
|
external,
|
|
158
180
|
icon: e.icon,
|
|
159
|
-
badge: e.
|
|
181
|
+
badge: e.badge_query?.trim()
|
|
182
|
+
? await queryBadge(ctx, e.badge_query, `badge of list entry "${e.label}"`)
|
|
183
|
+
: e.badge === null ? null : substitute(e.badge, ctx, (x) => x),
|
|
184
|
+
key: String(e.id ?? e.label),
|
|
160
185
|
description: e.description === null ? null : substitute(e.description, ctx, (x) => x),
|
|
161
186
|
current: isCurrent,
|
|
162
187
|
inTrail: isCurrent || (e.page !== null && current.has(e.page)) || children.some((c) => c.inTrail),
|
|
@@ -179,15 +204,22 @@ export async function listTree(ctx: PageContext, name: string): Promise<ListNode
|
|
|
179
204
|
|
|
180
205
|
const linkAttrs = (n: ListNode, cls?: string) =>
|
|
181
206
|
html`href="${n.href}"${cls ? html` class="${cls}"` : ''}${n.current ? raw(' aria-current="page"') : ''}${n.modal ? raw(' data-dialog') : ''}${n.external ? raw(' rel="noopener noreferrer"') : ''}`;
|
|
182
|
-
|
|
207
|
+
/**
|
|
208
|
+
* An entry's badge. live: the prefix of the badge's data-badge key when the application refreshes
|
|
209
|
+
* badges (154); an empty badge is then a hidden placeholder the refresh can fill.
|
|
210
|
+
*/
|
|
211
|
+
const badge = (n: ListNode, live?: string) =>
|
|
212
|
+
n.badge !== null
|
|
213
|
+
? html` <span class="badge"${live ? html` data-badge="${live}${n.key}"` : ''}>${n.badge}</span>`
|
|
214
|
+
: live ? html` <span class="badge" data-badge="${live}${n.key}" hidden></span>` : '';
|
|
183
215
|
|
|
184
216
|
/** Entries as the navigation menu (the same markup as navigation entries). */
|
|
185
|
-
export function navMarkup(nodes: ListNode[], topNav: boolean): Raw[] {
|
|
217
|
+
export function navMarkup(nodes: ListNode[], topNav: boolean, live?: string): Raw[] {
|
|
186
218
|
return nodes.map((n) => {
|
|
187
|
-
const label = html`${icon(n.icon ?? 'chevron')}<span>${n.label}</span>${badge(n)}`;
|
|
219
|
+
const label = html`${icon(n.icon ?? 'chevron')}<span>${n.label}</span>${badge(n, live)}`;
|
|
188
220
|
if (n.children.length)
|
|
189
221
|
return html`<li><details${n.inTrail && !topNav ? raw(' open') : ''}><summary class="${n.inTrail ? 'in-trail' : null}">${label}</summary><ul>${
|
|
190
|
-
n.href ? html`<li><a ${linkAttrs(n)}>${label}</a></li>` : ''}${navMarkup(n.children, topNav)}</ul></details></li>`;
|
|
222
|
+
n.href ? html`<li><a ${linkAttrs(n)}>${label}</a></li>` : ''}${navMarkup(n.children, topNav, live)}</ul></details></li>`;
|
|
191
223
|
return html`<li><a ${linkAttrs(n, !n.current && n.inTrail ? 'in-trail' : undefined)}>${label}</a></li>`;
|
|
192
224
|
});
|
|
193
225
|
}
|
|
@@ -195,13 +227,13 @@ export function navMarkup(nodes: ListNode[], topNav: boolean): Raw[] {
|
|
|
195
227
|
const hasIcon = (n: ListNode) => isIcon(n.icon);
|
|
196
228
|
|
|
197
229
|
/** Entries as the navigation bar in the header: links, with a menu for entries with children (labels of entries with an icon hide on small screens). */
|
|
198
|
-
export function navbarMarkup(nodes: ListNode[], label: string): Raw {
|
|
230
|
+
export function navbarMarkup(nodes: ListNode[], label: string, live?: string): Raw {
|
|
199
231
|
if (!nodes.length) return raw('');
|
|
200
232
|
return html`<nav class="t-navbar" aria-label="${label}"><ul>${nodes.map((n) =>
|
|
201
233
|
n.children.length
|
|
202
|
-
? html`<li><details class="menu"><summary${hasIcon(n) ? html` class="nb-icon" aria-label="${n.label}"` : ''}>${icon(n.icon)}<span>${n.label}</span>${badge(n)}</summary><div class="menu-panel align-right"><ul class="t-navbar-menu">${
|
|
203
|
-
[...(n.href ? [n] : []), ...n.children].map((c) => html`<li><a ${linkAttrs(c)}>${icon(c.icon)}<span>${c.label}</span>${badge(c)}</a></li>`)}</ul></div></details></li>`
|
|
204
|
-
: html`<li><a ${linkAttrs(n, hasIcon(n) ? 'nb-icon' : undefined)}${hasIcon(n) ? html` aria-label="${n.label}"` : ''}>${icon(n.icon)}<span>${n.label}</span>${badge(n)}</a></li>`)}</ul></nav>`;
|
|
234
|
+
? html`<li><details class="menu"><summary${hasIcon(n) ? html` class="nb-icon" aria-label="${n.label}"` : ''}>${icon(n.icon)}<span>${n.label}</span>${badge(n, live)}</summary><div class="menu-panel align-right"><ul class="t-navbar-menu">${
|
|
235
|
+
[...(n.href ? [n] : []), ...n.children].map((c) => html`<li><a ${linkAttrs(c)}>${icon(c.icon)}<span>${c.label}</span>${badge(c, live)}</a></li>`)}</ul></div></details></li>`
|
|
236
|
+
: html`<li><a ${linkAttrs(n, hasIcon(n) ? 'nb-icon' : undefined)}${hasIcon(n) ? html` aria-label="${n.label}"` : ''}>${icon(n.icon)}<span>${n.label}</span>${badge(n, live)}</a></li>`)}</ul></nav>`;
|
|
205
237
|
}
|
|
206
238
|
|
|
207
239
|
function linksMarkup(nodes: ListNode[]): Raw {
|
package/src/runtime/maps.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { encodeTile, tileArea, type TileFeature } from '../mvt.ts';
|
|
|
13
13
|
|
|
14
14
|
// Map region (APEX: Map region). The SELECT returns, per row, a position
|
|
15
15
|
// (columns lat and lng, latitude and longitude, or location as "lat,lng")
|
|
16
|
-
// and optionally title, body and geojson (a GeoJSON geometry or feature for
|
|
16
|
+
// and optionally id (what a map_object_click dynamic action sees), title, body and geojson (a GeoJSON geometry or feature for
|
|
17
17
|
// lines and areas; a GeoJSON point is a place), and weight (for a heat map).
|
|
18
18
|
// With PostGIS installed, a geometry or geography column (WGS 84) is turned
|
|
19
19
|
// into GeoJSON on the server (ST_AsGeoJSON), so no geojson column is needed.
|
|
@@ -59,6 +59,8 @@ interface Point {
|
|
|
59
59
|
body: string;
|
|
60
60
|
href: string | null;
|
|
61
61
|
weight: number;
|
|
62
|
+
/** (161) the row's id column: what a map_object_click dynamic action's condition sees */
|
|
63
|
+
id?: string;
|
|
62
64
|
}
|
|
63
65
|
/** (159, CD-6) a shape layer's look: fixed in the layer's settings, or per row from columns of the same names */
|
|
64
66
|
export interface ShapeStyle {
|
|
@@ -164,6 +166,7 @@ async function loadLayer(ctx: PageContext, def: LayerDef, g: PostGis | null, opt
|
|
|
164
166
|
if ((Number.isNaN(lat) || Number.isNaN(lng)) && loc) [lat, lng] = [Number(loc[1]), Number(loc[2])];
|
|
165
167
|
const title = toState(col(row, 'title')) ?? '';
|
|
166
168
|
const body = toState(col(row, 'body')) ?? '';
|
|
169
|
+
const id = toState(col(row, 'id')) ?? undefined;
|
|
167
170
|
let href: string | null = null;
|
|
168
171
|
if (linkOk && link) {
|
|
169
172
|
const items: Record<string, string> = {};
|
|
@@ -184,14 +187,14 @@ async function loadLayer(ctx: PageContext, def: LayerDef, g: PostGis | null, opt
|
|
|
184
187
|
? { ...def.style, ...shapeStyle({ fill_color: col(row, 'fill_color'), stroke_color: col(row, 'stroke_color'), stroke_width: col(row, 'stroke_width'), opacity: col(row, 'opacity') }) }
|
|
185
188
|
: undefined;
|
|
186
189
|
const h = num(col(row, 'height'));
|
|
187
|
-
shapes.push({ type: 'Feature', geometry, properties: { title, body, href, ...(style ? { style } : {}), ...(def.kind === 'extruded' && Number.isFinite(h) ? { height: h } : {}) } });
|
|
190
|
+
shapes.push({ type: 'Feature', geometry, properties: { title, body, href, ...(id !== undefined ? { id } : {}), ...(style ? { style } : {}), ...(def.kind === 'extruded' && Number.isFinite(h) ? { height: h } : {}) } });
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
// a shape layer draws only shapes
|
|
191
194
|
if (SHAPE_KINDS.has(def.kind)) continue;
|
|
192
195
|
const w = num(col(row, 'weight'));
|
|
193
196
|
const weight = Number.isFinite(w) && w > 0 ? w : Number.isNaN(w) ? 1 : 0;
|
|
194
|
-
if (Math.abs(lat) <= 90 && Math.abs(lng) <= 180) points.push({ lat, lng, title, body, href, weight });
|
|
197
|
+
if (Math.abs(lat) <= 90 && Math.abs(lng) <= 180) points.push({ lat, lng, title, body, href, weight, ...(id !== undefined ? { id } : {}) });
|
|
195
198
|
}
|
|
196
199
|
// (159) extruded: each polygon shaded by its height (from light to strong; no 3D view)
|
|
197
200
|
if (def.kind === 'extruded') {
|
|
@@ -296,7 +299,7 @@ export function parseTile(z: string, x: string, y: string) {
|
|
|
296
299
|
export async function layerTile(ctx: PageContext, def: LayerDef, t: { z: number; x: number; y: number }) {
|
|
297
300
|
const { points, shapes, truncated } = await loadLayer(ctx, def, await postgis(), { area: tileArea(t.z, t.x, t.y, 1 / 16), limit: MAX_TILE_ROWS });
|
|
298
301
|
const features: TileFeature[] = [
|
|
299
|
-
...points.map((p) => ({ geometry: { type: 'Point', coordinates: [p.lng, p.lat] }, properties: { title: p.title, body: p.body, href: p.href, weight: p.weight } })),
|
|
302
|
+
...points.map((p) => ({ geometry: { type: 'Point', coordinates: [p.lng, p.lat] }, properties: { title: p.title, body: p.body, href: p.href, weight: p.weight, ...(p.id !== undefined ? { id: p.id } : {}) } })),
|
|
300
303
|
...(shapes as { geometry: TileFeature['geometry']; properties: TileFeature['properties'] }[]).map((s) => ({ geometry: s.geometry, properties: s.properties })),
|
|
301
304
|
];
|
|
302
305
|
return { tile: encodeTile('places', t.z, t.x, t.y, features), truncated };
|