pgkiln 0.83.0 → 0.85.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 (126) hide show
  1. package/db/migrations/154_notifications.sql +186 -0
  2. package/docs/apex-feature-parity.md +3 -2
  3. package/docs/guide/03-builder.md +16 -5
  4. package/docs/guide/04-pages-and-regions.md +42 -0
  5. package/docs/guide/06-processing.md +22 -1
  6. package/docs/guide/07-dynamic-actions.md +1 -0
  7. package/docs/guide/09-reference.md +15 -0
  8. package/docs/guide/11-from-apex.md +1 -0
  9. package/docs/guide/12-development.md +13 -1
  10. package/package.json +1 -1
  11. package/public/app.css +29 -0
  12. package/public/app.js +78 -0
  13. package/public/sw.js +5 -1
  14. package/src/app.ts +11 -0
  15. package/src/builder/ai-agents.ts +86 -85
  16. package/src/builder/ai-builder.ts +30 -30
  17. package/src/builder/ai.ts +93 -92
  18. package/src/builder/api.ts +47 -47
  19. package/src/builder/app-groups.ts +33 -32
  20. package/src/builder/appfromfile.ts +57 -58
  21. package/src/builder/appsheets.ts +33 -32
  22. package/src/builder/appwizard.ts +65 -65
  23. package/src/builder/arrange.ts +23 -22
  24. package/src/builder/assistant.ts +39 -38
  25. package/src/builder/automations.ts +32 -31
  26. package/src/builder/availability.ts +24 -24
  27. package/src/builder/backups.ts +30 -31
  28. package/src/builder/blueprints.ts +62 -61
  29. package/src/builder/code-editor.ts +1 -1
  30. package/src/builder/components.ts +10 -6
  31. package/src/builder/create-page.ts +52 -50
  32. package/src/builder/cross-app.ts +37 -35
  33. package/src/builder/dataload.ts +2 -2
  34. package/src/builder/datasets.ts +107 -106
  35. package/src/builder/dependencies.ts +20 -19
  36. package/src/builder/designer.ts +36 -36
  37. package/src/builder/diagnostics.ts +58 -57
  38. package/src/builder/features.ts +39 -35
  39. package/src/builder/feedback.ts +23 -20
  40. package/src/builder/find-workspace.ts +28 -24
  41. package/src/builder/form-designer.ts +36 -35
  42. package/src/builder/gallery.ts +25 -29
  43. package/src/builder/globalization.ts +50 -49
  44. package/src/builder/i18n/de.ts +3461 -0
  45. package/src/builder/i18n/en.ts +3461 -0
  46. package/src/builder/i18n/es.ts +3461 -0
  47. package/src/builder/i18n/fr.ts +3461 -0
  48. package/src/builder/i18n/it.ts +3461 -0
  49. package/src/builder/i18n/ja.ts +3461 -0
  50. package/src/builder/i18n/ko.ts +3461 -0
  51. package/src/builder/i18n/nl.ts +3461 -0
  52. package/src/builder/i18n/pt.ts +3461 -0
  53. package/src/builder/i18n/zh-cn.ts +3461 -0
  54. package/src/builder/i18n/zh-tw.ts +3461 -0
  55. package/src/builder/i18n.ts +17 -3
  56. package/src/builder/json-sources.ts +63 -62
  57. package/src/builder/layouts.ts +12 -11
  58. package/src/builder/locks.ts +29 -28
  59. package/src/builder/login-page.ts +26 -25
  60. package/src/builder/mail.ts +148 -147
  61. package/src/builder/newapp.ts +15 -12
  62. package/src/builder/notification-settings.ts +27 -0
  63. package/src/builder/plugins.ts +47 -44
  64. package/src/builder/process-jobs.ts +11 -10
  65. package/src/builder/pwa.ts +22 -21
  66. package/src/builder/remote-databases.ts +55 -56
  67. package/src/builder/report-settings.ts +1 -1
  68. package/src/builder/reporter.ts +43 -42
  69. package/src/builder/rest-catalogs.ts +87 -86
  70. package/src/builder/rest.ts +9 -8
  71. package/src/builder/routes.ts +30 -27
  72. package/src/builder/search.ts +14 -13
  73. package/src/builder/static-files.ts +31 -32
  74. package/src/builder/subscriptions.ts +47 -45
  75. package/src/builder/supporting.ts +23 -18
  76. package/src/builder/team.ts +179 -179
  77. package/src/builder/telemetry.ts +59 -62
  78. package/src/builder/templates.ts +66 -65
  79. package/src/builder/themeroller.ts +60 -63
  80. package/src/builder/ui-defaults.ts +21 -20
  81. package/src/builder/ui.ts +2 -1
  82. package/src/builder/upgrade.ts +25 -24
  83. package/src/builder/utilities.ts +48 -47
  84. package/src/builder/vector-providers.ts +67 -66
  85. package/src/builder/websources.ts +43 -42
  86. package/src/builder/wizards.ts +95 -89
  87. package/src/builder/workflow-migrate.ts +30 -31
  88. package/src/builder/workflows.ts +40 -39
  89. package/src/builder/workingcopies.ts +41 -49
  90. package/src/builder/workspace-backups.ts +21 -20
  91. package/src/builder/workspace-mail.ts +17 -16
  92. package/src/builder/workspace-resources.ts +104 -112
  93. package/src/builder/workspace-transfer.ts +34 -26
  94. package/src/cli/replace.ts +2 -0
  95. package/src/i18n/ar.ts +9 -0
  96. package/src/i18n/cs.ts +9 -0
  97. package/src/i18n/da.ts +9 -0
  98. package/src/i18n/de.ts +9 -0
  99. package/src/i18n/el.ts +9 -0
  100. package/src/i18n/es.ts +9 -0
  101. package/src/i18n/fi.ts +9 -0
  102. package/src/i18n/fr.ts +9 -0
  103. package/src/i18n/he.ts +9 -0
  104. package/src/i18n/it.ts +9 -0
  105. package/src/i18n/ja.ts +9 -0
  106. package/src/i18n/ko.ts +9 -0
  107. package/src/i18n/nb.ts +9 -0
  108. package/src/i18n/pl.ts +9 -0
  109. package/src/i18n/pt.ts +9 -0
  110. package/src/i18n/ru.ts +9 -0
  111. package/src/i18n/sv.ts +9 -0
  112. package/src/i18n/tr.ts +9 -0
  113. package/src/i18n/uk.ts +9 -0
  114. package/src/i18n/zh.ts +9 -0
  115. package/src/i18n.ts +18 -0
  116. package/src/metadata.ts +9 -3
  117. package/src/runtime/engine.ts +2 -1
  118. package/src/runtime/feedback.ts +1 -1
  119. package/src/runtime/lists.ts +43 -11
  120. package/src/runtime/notifications.ts +237 -0
  121. package/src/runtime/processes.ts +45 -1
  122. package/src/runtime/regions.ts +11 -15
  123. package/src/runtime/render.ts +12 -3
  124. package/src/runtime/routes.ts +12 -4
  125. package/src/runtime/signin.ts +2 -1
  126. package/src/session.ts +13 -4
@@ -7,9 +7,10 @@ import { html } from '../html.ts';
7
7
  import { icon } from '../icons.ts';
8
8
  import { readMultipart } from '../runtime/files.ts';
9
9
  import { clientIp, logActivity, type Session } from '../session.ts';
10
- import { describeWorkspaceFile, exportWorkspace, importWorkspace, PART_LABELS, PARTS, type Part } from '../workspace-export.ts';
10
+ import { describeWorkspaceFile, exportWorkspace, importWorkspace, PARTS, type Part } from '../workspace-export.ts';
11
11
  import { back, BASE, csrf, developer, flash, region, send, shell, type Body, type Req } from './ui.ts';
12
12
  import { currentWorkspace, workspaceAdmin } from './workspaces.ts';
13
+ import { scopedTx as tx, type BuilderKey } from './i18n.ts';
13
14
 
14
15
  // Workspace utilities → Export / import (APEX: Workspace Utilities → Export):
15
16
  // the current workspace's components as one file, and such a file imported
@@ -19,7 +20,14 @@ import { currentWorkspace, workspaceAdmin } from './workspaces.ts';
19
20
 
20
21
  const RESULT = '__WS_IMPORT_RESULT';
21
22
  const page = `${BASE}/workspace-export`;
22
- const crumbs: [string, string?][] = [['App Builder', BASE], ['Workspace utilities', `${BASE}/utilities`], ['Export and import']];
23
+ const crumbs = (): [string, string?][] => [[tx('rail.app_builder'), BASE], [tx('wu.title'), `${BASE}/utilities`], [tx('wu.export')]];
24
+ /** The kinds of component in the developer's language (PART_LABELS in English: src/workspace-export.ts). */
25
+ const PART_KEYS: Record<Part, BuilderKey> = {
26
+ settings: 'wtr.part.settings', developers: 'wtr.part.developers', app_groups: 'wtr.part.app_groups', extension_links: 'wtr.part.extension_links',
27
+ rest_catalogs: 'wtr.part.rest_catalogs', sql_scripts: 'wtr.part.sql_scripts', blueprints: 'wtr.part.blueprints', data_generators: 'wtr.part.data_generators',
28
+ datasets: 'wtr.part.datasets', team: 'wtr.part.team', workspace_files: 'wtr.part.workspace_files', remote_servers: 'wtr.part.remote_servers',
29
+ web_credentials: 'wtr.part.web_credentials',
30
+ };
23
31
  const MAX_MB = 20;
24
32
 
25
33
  let version: string | null | undefined;
@@ -41,16 +49,16 @@ const chosen = (b: Body | undefined): Part[] => {
41
49
 
42
50
  async function adminOr403(s: Session, reply: FastifyReply) {
43
51
  if (s.workspace && (await workspaceAdmin(s.username, s.workspace.id))) return true;
44
- await send(reply.code(403), s, shell(s, 'Export and import', crumbs, html`<h1 class="u-mb1">Export and import</h1>
45
- <div class="alert alert-error" role="alert">Only administrators and the workspace's administrators export or import a workspace's components.</div>`));
52
+ await send(reply.code(403), s, shell(s, tx('wu.export'), crumbs(), html`<h1 class="u-mb1">${tx('wu.export')}</h1>
53
+ <div class="alert alert-error" role="alert">${tx('wtr.admins_only')}</div>`));
46
54
  return false;
47
55
  }
48
56
 
49
57
  const partBoxes = (counts?: Partial<Record<Part, number>>) =>
50
- html`<fieldset class="field" data-wide><legend class="label">Components</legend>${PARTS.map((p) =>
58
+ html`<fieldset class="field" data-wide><legend class="label">${tx('wtr.components')}</legend>${PARTS.map((p) =>
51
59
  counts && counts[p] === undefined
52
60
  ? ''
53
- : html`<label class="check"><input type="checkbox" name="part" value="${p}" checked> ${PART_LABELS[p]}${counts ? html` <span class="muted">(${p === 'settings' ? 'in the file' : p === 'team' ? `${counts[p]} issue(s)` : counts[p]})</span>` : ''}</label>`)}</fieldset>`;
61
+ : html`<label class="check"><input type="checkbox" name="part" value="${p}" checked> ${tx(PART_KEYS[p])}${counts ? html` <span class="muted">(${p === 'settings' ? tx('wtr.in_file') : p === 'team' ? tx('wtr.issues', { n: counts[p] }) : counts[p]})</span>` : ''}</label>`)}</fieldset>`;
54
62
 
55
63
  export async function workspaceTransferRoutes(app: FastifyInstance) {
56
64
  app.get(page, async (req: Req, reply) => {
@@ -59,22 +67,22 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
59
67
  const result = s.state[RESULT] ? (JSON.parse(s.state[RESULT]!) as string[]) : null;
60
68
  delete s.state[RESULT];
61
69
  const name = s.workspace!.name;
62
- const main = html`<div class="title-row"><h1>Export and import</h1></div>
63
- <p class="muted u-mt0">The components of the workspace ${name} as one file: to copy them to another workspace or installation, or to keep them with your applications' files. Applications are exported on their own (App Builder → an application → Export).</p>
64
- ${result ? html`<div class="alert alert-success" role="status"><b>Imported into ${name}:</b><ul>${result.map((l) => html`<li>${l}</li>`)}</ul></div>` : ''}
65
- ${region('Export', html`<form method="post" action="${page}">${csrf(s)}
70
+ const main = html`<div class="title-row"><h1>${tx('wu.export')}</h1></div>
71
+ <p class="muted u-mt0">${tx('wtr.intro', { name })}</p>
72
+ ${result ? html`<div class="alert alert-success" role="status"><b>${tx('wtr.imported_into', { name })}</b><ul>${result.map((l) => html`<li>${l}</li>`)}</ul></div>` : ''}
73
+ ${region(tx('wtr.export'), html`<form method="post" action="${page}">${csrf(s)}
66
74
  ${partBoxes()}
67
- <p class="muted">Not in the file: the workspace's name, its isolation and database roles, passwords and secrets, script runs and other history.</p>
68
- <div class="buttons"><button class="btn btn-hot">${icon('download')} Export</button></div></form>`)}
69
- ${region('Import', html`<form method="post" action="${BASE}/workspace-import" enctype="multipart/form-data">${csrf(s)}
70
- <p class="muted u-mt0">A file exported from a workspace, imported into <b>${name}</b>. Nothing is deleted; developers are added only when this installation has a developer of that name.</p>
75
+ <p class="muted">${tx('wtr.not_in_file')}</p>
76
+ <div class="buttons"><button class="btn btn-hot">${icon('download')} ${tx('wtr.export')}</button></div></form>`)}
77
+ ${region(tx('wtr.import'), html`<form method="post" action="${BASE}/workspace-import" enctype="multipart/form-data">${csrf(s)}
78
+ <p class="muted u-mt0">${tx.rich('wtr.import_intro', { name: html`<b>${name}</b>` })}</p>
71
79
  <div class="form-grid">
72
- <div class="field" data-wide><label class="label" for="f_file">Workspace file</label><input id="f_file" type="file" name="file" accept=".json,application/json" required></div>
80
+ <div class="field" data-wide><label class="label" for="f_file">${tx('wtr.file')}</label><input id="f_file" type="file" name="file" accept=".json,application/json" required></div>
73
81
  ${partBoxes()}
74
- <div class="field" data-wide><label class="check"><input type="checkbox" name="replace" value="true"> Replace components with the same name (otherwise this workspace's own are kept)</label></div>
82
+ <div class="field" data-wide><label class="check"><input type="checkbox" name="replace" value="true"> ${tx('wtr.replace')}</label></div>
75
83
  </div>
76
- <div class="buttons"><button class="btn">${icon('upload')} Import</button></div></form>`)}`;
77
- return send(reply, s, shell(s, 'Export and import', crumbs, main));
84
+ <div class="buttons"><button class="btn">${icon('upload')} ${tx('wtr.import')}</button></div></form>`)}`;
85
+ return send(reply, s, shell(s, tx('wu.export'), crumbs(), main));
78
86
  });
79
87
 
80
88
  app.post(page, async (req: Req, reply) => {
@@ -82,7 +90,7 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
82
90
  if (!s || !(await adminOr403(s, reply))) return;
83
91
  const parts = chosen(req.body);
84
92
  if (!parts.length) {
85
- flash(s, 'Choose at least one kind of component.', 'error');
93
+ flash(s, tx('wtr.choose_part'), 'error');
86
94
  return back(reply, s, page);
87
95
  }
88
96
  const doc = await exportWorkspace(currentWorkspace(s), parts, { version: pgkilnVersion() ?? undefined, by: s.username });
@@ -104,23 +112,23 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
104
112
  const s = await developer(req, reply);
105
113
  if (!s || !(await adminOr403(s, reply))) return;
106
114
  try {
107
- if (tooBig) throw new Error(`The file is larger than ${MAX_MB} MB.`);
108
- if (!text.trim()) throw new Error('Choose a workspace file.');
115
+ if (tooBig) throw new Error(tx('wtr.too_big', { n: MAX_MB }));
116
+ if (!text.trim()) throw new Error(tx('wtr.choose_file'));
109
117
  let doc: unknown;
110
118
  try {
111
119
  doc = JSON.parse(text);
112
120
  } catch {
113
- throw new Error('The file is not valid JSON.');
121
+ throw new Error(tx('wtr.not_json'));
114
122
  }
115
123
  describeWorkspaceFile(doc);
116
124
  const parts = chosen(req.body);
117
- if (!parts.length) throw new Error('Choose at least one kind of component.');
125
+ if (!parts.length) throw new Error(tx('wtr.choose_part'));
118
126
  const ws = currentWorkspace(s);
119
127
  const result = await owner.tx((c) => importWorkspace(c, ws, doc, parts, { replace: req.body?.replace === 'true', by: s.username }));
120
128
  await logActivity({ appId: null, username: s.username, event: 'workspace', ip: clientIp(req), detail: `imported into workspace ${ws}: ${result.added} added, ${result.replaced} replaced, ${result.skipped} kept` });
121
129
  s.state[RESULT] = JSON.stringify(result.lines);
122
130
  } catch (e) {
123
- flash(s, `Import failed, nothing was changed: ${friendly(e)}`, 'error');
131
+ flash(s, tx('wtr.failed', { reason: friendly(e) }), 'error');
124
132
  }
125
133
  return back(reply, s, page);
126
134
  });
@@ -129,8 +137,8 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
129
137
  /** A database refusal in words (the transaction rolled back). */
130
138
  function friendly(e: unknown) {
131
139
  const err = e as { message: string; constraint?: string; table?: string; code?: string };
132
- if (err.code === '23514' || err.code === '23502' || err.code === '22P02') return `the file has a value this installation refuses (${err.table ?? ''}${err.constraint ? `: ${err.constraint}` : ''}).`;
133
- if (err.code === '23505') return `the file has two components with the same name (${err.constraint ?? err.table ?? ''}).`;
140
+ if (err.code === '23514' || err.code === '23502' || err.code === '22P02') return tx('wtr.refused_value', { detail: `${err.table ?? ''}${err.constraint ? `: ${err.constraint}` : ''}` });
141
+ if (err.code === '23505') return tx('wtr.duplicate', { detail: err.constraint ?? err.table ?? '' });
134
142
  return err.message;
135
143
  }
136
144
 
@@ -78,6 +78,8 @@ export const KEPT = ['app_access', 'api_client', 'session', 'sso_pending', 'save
78
78
  'rest_object',
79
79
  // (121) the log of AI tool calls (the agents and tools themselves travel in meta.app)
80
80
  'ai_tool_call',
81
+ // (154) users' in-app notifications
82
+ 'notification',
81
83
  // (122) Team Development issues of the workspace, linked to the application (workspace data: the workspace export)
82
84
  'team_issue',
83
85
  // (123) the log of running workflow instances migrated to a newer version (runtime data, like the instances)
package/src/i18n/ar.ts CHANGED
@@ -310,6 +310,15 @@ export const ar: 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/cs.ts CHANGED
@@ -310,6 +310,15 @@ export const cs: Record<MessageKey, string> = {
310
310
  'account.devices_help': "Prohlížeče, ve kterých jste zvolili „Zůstat přihlášen“: {count}.",
311
311
  'account.forget_devices': "Odhlásit na všech zařízeních",
312
312
  'account.devices_forgotten': "Odhlášeno na všech zařízeních.",
313
+ 'notify.title': "Oznámení",
314
+ 'notify.none': "Žádná oznámení.",
315
+ 'notify.mark_all_read': "Označit vše jako přečtené",
316
+ 'notify.all': "Všechna oznámení",
317
+ 'notify.marked': "Oznámení označena jako přečtená.",
318
+ 'notify.gone': "Toto oznámení už neexistuje.",
319
+ 'notify.bell_one': "Oznámení: 1 nepřečtené",
320
+ 'notify.bell_n': "Oznámení: nepřečtených {n}",
321
+ 'notify.unread_one': "Nepřečtené",
313
322
  'push.title': "Oznámení",
314
323
  'push.help': "Dostávejte oznámení z této aplikace na tomto zařízení, i když je aplikace zavřená.",
315
324
  'push.devices': "Zařízení s oznámeními: {n}.",
package/src/i18n/da.ts CHANGED
@@ -310,6 +310,15 @@ export const da: Record<MessageKey, string> = {
310
310
  'account.devices_help': "Browsere, hvor du valgte “Hold mig logget ind”: {count}.",
311
311
  'account.forget_devices': "Log ud på alle enheder",
312
312
  'account.devices_forgotten': "Logget ud på alle enheder.",
313
+ 'notify.title': "Notifikationer",
314
+ 'notify.none': "Ingen notifikationer.",
315
+ 'notify.mark_all_read': "Markér alle som læst",
316
+ 'notify.all': "Alle notifikationer",
317
+ 'notify.marked': "Notifikationerne er markeret som læst.",
318
+ 'notify.gone': "Denne notifikation findes ikke længere.",
319
+ 'notify.bell_one': "Notifikationer: 1 ulæst",
320
+ 'notify.bell_n': "Notifikationer: {n} ulæste",
321
+ 'notify.unread_one': "Ulæst",
313
322
  'push.title': "Notifikationer",
314
323
  'push.help': "Få notifikationer fra denne app på denne enhed, også når appen er lukket.",
315
324
  'push.devices': "Enheder med notifikationer: {n}.",
package/src/i18n/de.ts CHANGED
@@ -310,6 +310,15 @@ export const de: Record<MessageKey, string> = {
310
310
  'account.devices_help': 'Browser, in denen Sie „Angemeldet bleiben“ gewählt haben: {count}.',
311
311
  'account.forget_devices': 'Auf allen Geräten abmelden',
312
312
  'account.devices_forgotten': 'Auf allen Geräten abgemeldet.',
313
+ 'notify.title': "Benachrichtigungen",
314
+ 'notify.none': "Keine Benachrichtigungen.",
315
+ 'notify.mark_all_read': "Alle als gelesen markieren",
316
+ 'notify.all': "Alle Benachrichtigungen",
317
+ 'notify.marked': "Benachrichtigungen als gelesen markiert.",
318
+ 'notify.gone': "Diese Benachrichtigung gibt es nicht mehr.",
319
+ 'notify.bell_one': "Benachrichtigungen: 1 ungelesen",
320
+ 'notify.bell_n': "Benachrichtigungen: {n} ungelesen",
321
+ 'notify.unread_one': "Ungelesen",
313
322
  'push.title': "Benachrichtigungen",
314
323
  'push.help': "Erhalten Sie Benachrichtigungen dieser App auf diesem Gerät, auch wenn die App geschlossen ist.",
315
324
  'push.devices': "Geräte mit Benachrichtigungen: {n}.",
package/src/i18n/el.ts CHANGED
@@ -310,6 +310,15 @@ export const el: 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/es.ts CHANGED
@@ -310,6 +310,15 @@ export const es: Record<MessageKey, string> = {
310
310
  'account.devices_help': 'Navegadores en los que eligió "Mantener la sesión iniciada": {count}.',
311
311
  'account.forget_devices': 'Cerrar sesión en todos los dispositivos',
312
312
  'account.devices_forgotten': 'Sesión cerrada en todos los dispositivos.',
313
+ 'notify.title': "Notificaciones",
314
+ 'notify.none': "No hay notificaciones.",
315
+ 'notify.mark_all_read': "Marcar todas como leídas",
316
+ 'notify.all': "Todas las notificaciones",
317
+ 'notify.marked': "Notificaciones marcadas como leídas.",
318
+ 'notify.gone': "Esta notificación ya no existe.",
319
+ 'notify.bell_one': "Notificaciones: 1 sin leer",
320
+ 'notify.bell_n': "Notificaciones: {n} sin leer",
321
+ 'notify.unread_one': "Sin leer",
313
322
  'push.title': "Notificaciones",
314
323
  'push.help': "Reciba notificaciones de esta aplicación en este dispositivo, también cuando la aplicación está cerrada.",
315
324
  'push.devices': "Dispositivos con notificaciones: {n}.",
package/src/i18n/fi.ts CHANGED
@@ -310,6 +310,15 @@ export const fi: Record<MessageKey, string> = {
310
310
  'account.devices_help': "Selaimet, joissa valitsit ”Pidä minut kirjautuneena”: {count}.",
311
311
  'account.forget_devices': "Kirjaudu ulos kaikilla laitteilla",
312
312
  'account.devices_forgotten': "Kirjauduttu ulos kaikilla laitteilla.",
313
+ 'notify.title': "Ilmoitukset",
314
+ 'notify.none': "Ei ilmoituksia.",
315
+ 'notify.mark_all_read': "Merkitse kaikki luetuiksi",
316
+ 'notify.all': "Kaikki ilmoitukset",
317
+ 'notify.marked': "Ilmoitukset merkittiin luetuiksi.",
318
+ 'notify.gone': "Tätä ilmoitusta ei enää ole.",
319
+ 'notify.bell_one': "Ilmoitukset: 1 lukematon",
320
+ 'notify.bell_n': "Ilmoitukset: {n} lukematonta",
321
+ 'notify.unread_one': "Lukematon",
313
322
  'push.title': "Ilmoitukset",
314
323
  'push.help': "Saa tämän sovelluksen ilmoitukset tälle laitteelle, myös kun sovellus on suljettu.",
315
324
  'push.devices': "Laitteet, joilla on ilmoitukset: {n}.",
package/src/i18n/fr.ts CHANGED
@@ -310,6 +310,15 @@ export const fr: Record<MessageKey, string> = {
310
310
  'account.devices_help': 'Navigateurs où vous avez choisi « Rester connecté » : {count}.',
311
311
  'account.forget_devices': 'Se déconnecter sur tous les appareils',
312
312
  'account.devices_forgotten': 'Déconnecté sur tous les appareils.',
313
+ 'notify.title': "Notifications",
314
+ 'notify.none': "Aucune notification.",
315
+ 'notify.mark_all_read': "Tout marquer comme lu",
316
+ 'notify.all': "Toutes les notifications",
317
+ 'notify.marked': "Notifications marquées comme lues.",
318
+ 'notify.gone': "Cette notification n'existe plus.",
319
+ 'notify.bell_one': "Notifications : 1 non lue",
320
+ 'notify.bell_n': "Notifications : {n} non lues",
321
+ 'notify.unread_one': "Non lue",
313
322
  'push.title': "Notifications",
314
323
  'push.help': "Recevez les notifications de cette application sur cet appareil, même lorsqu'elle est fermée.",
315
324
  'push.devices': "Appareils avec notifications : {n}.",
package/src/i18n/he.ts CHANGED
@@ -310,6 +310,15 @@ export const he: 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/it.ts CHANGED
@@ -310,6 +310,15 @@ export const it: Record<MessageKey, string> = {
310
310
  'account.devices_help': "Browser in cui hai scelto “Mantieni l'accesso”: {count}.",
311
311
  'account.forget_devices': "Esci da tutti i dispositivi",
312
312
  'account.devices_forgotten': "Uscita effettuata da tutti i dispositivi.",
313
+ 'notify.title': "Notifiche",
314
+ 'notify.none': "Nessuna notifica.",
315
+ 'notify.mark_all_read': "Segna tutte come lette",
316
+ 'notify.all': "Tutte le notifiche",
317
+ 'notify.marked': "Notifiche segnate come lette.",
318
+ 'notify.gone': "Questa notifica non esiste più.",
319
+ 'notify.bell_one': "Notifiche: 1 da leggere",
320
+ 'notify.bell_n': "Notifiche: {n} da leggere",
321
+ 'notify.unread_one': "Da leggere",
313
322
  'push.title': "Notifiche",
314
323
  'push.help': "Ricevi le notifiche di questa app su questo dispositivo, anche quando l'app è chiusa.",
315
324
  'push.devices': "Dispositivi con notifiche: {n}.",
package/src/i18n/ja.ts CHANGED
@@ -310,6 +310,15 @@ export const ja: 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/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 */
@@ -322,7 +326,7 @@ export interface DynamicAction {
322
326
  /** (149) */
323
327
  | 'double_click' | 'key_down' | 'key_press' | 'mouse_enter' | 'mouse_leave' | 'select' | 'resize' | 'scroll' | 'page_unload' | 'before_refresh' | 'facets_change'
324
328
  /** (153) component events: trigger = the grid, calendar, map or tree region */
325
- | 'grid_row_init' | 'grid_save' | 'calendar_date_select' | 'calendar_view_change' | 'map_object_click' | 'map_change' | 'tree_selection_change';
329
+ | 'grid_row_init' | 'grid_save' | 'calendar_date_select' | 'calendar_view_change' | 'map_object_click' | 'map_change' | 'tree_selection_change' | 'timer';
326
330
  trigger_element: string | null;
327
331
  condition_type: 'equals' | 'not_equals' | 'in_list' | 'is_null' | 'is_not_null' | 'greater' | 'greater_equal' | 'less' | 'less_equal' | 'not_in_list' | null;
328
332
  condition_value: string | null;
@@ -383,7 +387,9 @@ export interface Process {
383
387
  type: 'form_dml' | 'grid_dml' | 'sql' | 'data_load' | 'invoke_api' | 'download' | 'chain' | 'workflow' | 'ai_generate' | 'plugin' | 'send_push' | 'send_email'
384
388
  /** (150) */
385
389
  | 'close_dialog' | 'clear_session_state' | 'reset_pagination' | 'user_preferences' | 'server_geocoding' | 'print_report'
386
- | 'human_task_create' | 'human_task_manage' | 'form_init';
390
+ | 'human_task_create' | 'human_task_manage' | 'form_init'
391
+ /** (154) */
392
+ | 'send_notification';
387
393
  region_id: number | null;
388
394
  code: string | null;
389
395
  config: Record<string, unknown> | null;
@@ -443,7 +449,7 @@ const agg = (table: string, fk: string, parent: string, appId: string) =>
443
449
  export async function loadApp(alias: string) {
444
450
  return runtime.one<App>(
445
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,
446
- 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,
447
453
  a.language, a.languages, a.language_from, a.date_format, a.timestamp_format, a.time_zone, a.time_zone_auto, a.currency,
448
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,
449
455
  coalesce((select jsonb_agg(jsonb_build_object('page_no', p.page_no, 'name', p.name, 'title', p.title,
@@ -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);
@@ -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