pgkiln 0.83.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/154_notifications.sql +186 -0
- package/docs/apex-feature-parity.md +2 -1
- package/docs/guide/03-builder.md +2 -0
- package/docs/guide/04-pages-and-regions.md +42 -0
- package/docs/guide/06-processing.md +22 -1
- package/docs/guide/07-dynamic-actions.md +1 -0
- package/docs/guide/09-reference.md +15 -0
- package/docs/guide/11-from-apex.md +1 -0
- package/docs/guide/12-development.md +7 -0
- package/package.json +1 -1
- package/public/app.css +29 -0
- package/public/app.js +78 -0
- package/public/sw.js +5 -1
- package/src/app.ts +2 -0
- package/src/builder/code-editor.ts +1 -1
- package/src/builder/components.ts +10 -6
- package/src/builder/i18n/de.ts +8 -0
- package/src/builder/i18n/en.ts +8 -0
- package/src/builder/i18n/es.ts +8 -0
- package/src/builder/i18n/fr.ts +8 -0
- package/src/builder/i18n/it.ts +8 -0
- package/src/builder/i18n/ja.ts +8 -0
- package/src/builder/i18n/ko.ts +8 -0
- package/src/builder/i18n/nl.ts +8 -0
- package/src/builder/i18n/pt.ts +8 -0
- package/src/builder/i18n/zh-cn.ts +8 -0
- package/src/builder/i18n/zh-tw.ts +8 -0
- package/src/builder/notification-settings.ts +27 -0
- package/src/builder/routes.ts +3 -0
- package/src/cli/replace.ts +2 -0
- 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 +9 -3
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/feedback.ts +1 -1
- package/src/runtime/lists.ts +43 -11
- 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 +12 -3
- package/src/runtime/routes.ts +12 -4
- package/src/runtime/signin.ts +2 -1
- package/src/session.ts +13 -4
package/public/sw.js
CHANGED
|
@@ -210,7 +210,11 @@ self.addEventListener('push', (event) => {
|
|
|
210
210
|
data: { url: appUrl(m.url) || appUrl(`${PGKILN.base}/`) },
|
|
211
211
|
};
|
|
212
212
|
if (typeof m.tag === 'string' && m.tag) Object.assign(options, { tag: m.tag, renotify: true });
|
|
213
|
-
event.waitUntil(
|
|
213
|
+
event.waitUntil(Promise.all([
|
|
214
|
+
self.registration.showNotification(typeof m.title === 'string' && m.title ? m.title : 'Notification', options),
|
|
215
|
+
// (165) open pages refresh their badges and the bell now
|
|
216
|
+
self.clients.matchAll({ type: 'window' }).then((wins) => wins.forEach((w) => w.postMessage({ type: 'pgkiln:pushed' }))),
|
|
217
|
+
]));
|
|
214
218
|
});
|
|
215
219
|
|
|
216
220
|
self.addEventListener('notificationclick', (event) => {
|
package/src/app.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { assistantBuilderRoutes } from './builder/assistant.ts';
|
|
|
60
60
|
import { aiBuilderRoutes } from './builder/ai-builder.ts';
|
|
61
61
|
import { blueprintRoutes } from './builder/blueprints.ts';
|
|
62
62
|
import { accountRoutes } from './runtime/account.ts';
|
|
63
|
+
import { notificationRoutes } from './runtime/notifications.ts';
|
|
63
64
|
import { taskRoutes } from './runtime/tasks.ts';
|
|
64
65
|
import { dataReporterRoutes } from './runtime/data-reporter.ts';
|
|
65
66
|
import { datasetRoutes } from './builder/datasets.ts';
|
|
@@ -210,6 +211,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
210
211
|
});
|
|
211
212
|
await app.register(runtimeRoutes);
|
|
212
213
|
await app.register(accountRoutes);
|
|
214
|
+
await app.register(notificationRoutes);
|
|
213
215
|
await app.register(taskRoutes);
|
|
214
216
|
await app.register(workflowRoutes);
|
|
215
217
|
await app.register(pwaRoutes);
|
|
@@ -33,7 +33,7 @@ const CHECKS: Record<string, SqlShape> = {
|
|
|
33
33
|
'process.code': 'statements', 'authz_scheme.value': 'boolean',
|
|
34
34
|
'lov.query': 'select', 'app_process.code': 'statements', 'automation.query': 'select', 'automation_action.code': 'statements', 'automation_action.condition': 'boolean',
|
|
35
35
|
'document_template.query': 'select', 'report_query.query': 'select', 'task_definition.action_code': 'statements', 'dynamic_action.code': 'statements',
|
|
36
|
-
'button.badge_query': 'select', 'list.query': 'select', 'list_entry.condition': 'boolean',
|
|
36
|
+
'button.badge_query': 'select', 'list.query': 'select', 'list_entry.condition': 'boolean', 'list_entry.badge_query': 'select',
|
|
37
37
|
};
|
|
38
38
|
/** (147) The expression fields of server-side conditions (their type select names how to read them). */
|
|
39
39
|
const CONDITION_EXPRS = ['computation', 'app_computation', 'branch', 'process', 'item', 'validation'].map((k) => `${k}.condition_expr`).concat('dynamic_action.server_condition_expr');
|
|
@@ -225,7 +225,9 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
|
|
|
225
225
|
summary: (d) => d.name,
|
|
226
226
|
defaults: { event: 'change', action: 'show' },
|
|
227
227
|
validate: (v) =>
|
|
228
|
-
v.
|
|
228
|
+
v.event === 'timer' && !/^\s*\d{1,4}\s*$/.test(String(v.trigger_element ?? '')) || (v.event === 'timer' && (Number(v.trigger_element) < 5 || Number(v.trigger_element) > 3600))
|
|
229
|
+
? 'Timer: the trigger is a number of seconds from 5 to 3600 (how often the actions run).'
|
|
230
|
+
: v.css_classes && !CLASS_LIST.test(String(v.css_classes))
|
|
229
231
|
? 'CSS classes: up to five names of lower case letters, digits, - and _, separated by spaces.'
|
|
230
232
|
: v.action === 'execute_javascript' && !/^[A-Za-z_$][\w$.-]{0,99}$/.test(String(v.code ?? '').trim())
|
|
231
233
|
? 'Execute JavaScript: Code is the name of a function a static file registers with pgkiln.actions.register(name, fn), e.g. highlightLate.'
|
|
@@ -236,8 +238,8 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
|
|
|
236
238
|
{ name: 'name', label: 'Name', kind: 'text', group: 'When' },
|
|
237
239
|
{ name: 'event', label: 'Event', kind: 'select', options: ['change', 'click', 'load', 'dialog_closed', 'focus', 'blur', 'key_release', 'after_refresh', 'selection_change', 'before_submit', 'custom', 'menu_entry',
|
|
238
240
|
'double_click', 'key_down', 'key_press', 'mouse_enter', 'mouse_leave', 'select', 'resize', 'scroll', 'page_unload', 'before_refresh', 'facets_change',
|
|
239
|
-
'grid_row_init', 'grid_save', 'calendar_date_select', 'calendar_view_change', 'map_object_click', 'map_change', 'tree_selection_change'], group: 'When',
|
|
240
|
-
help: '(sprint 161) Component events, trigger: the region. grid_row_init: a row was added to an editable grid (Add row, Duplicate). grid_save: the grid\'s changes were saved (runs on the page that comes back). calendar_date_select: an empty day or hour was chosen (the condition sees YYYY-MM-DD or YYYY-MM-DDTHH:MM; cancel_event keeps the Create link closed). calendar_view_change: another view or period (the condition sees month, week, day or list). map_object_click: a marker or shape was clicked (the condition sees its id column, else its title). map_change: the map was moved or zoomed (the condition sees the zoom level). tree_selection_change: a tree node was chosen (the condition sees its id; cancel_event keeps its link closed). (sprint 156) double_click, mouse_enter, mouse_leave: items, buttons or regions. key_down, key_press (a key that types a character), select (text selected): items. resize, scroll: the window (no trigger). page_unload: the page is left (only quick, local actions finish). before_refresh: a region is about to be refreshed by a Refresh action. facets_change: a faceted search or smart filters region\'s facets were applied (trigger: that region). ' + 'dialog_closed: a modal dialog opened from this page was submitted and closed (instead of reloading the page), e.g. refresh a region. focus / blur / key_release: an item gets or loses the focus, a key is released in it. after_refresh: a region was refreshed or loaded. selection_change: rows were selected in a report (row selection) or a grid\'s master row. before_submit: the page is about to be submitted (a declined confirm keeps it). custom: JavaScript raised it with pgkiln.trigger(\'NAME\', value). menu_entry (Menu entry chosen): an entry {"label": …, "action": "NAME"} of a menu button was chosen. The actions of one event and trigger run in sequence order; a declined confirm stops the ones after it.' },
|
|
241
|
+
'grid_row_init', 'grid_save', 'calendar_date_select', 'calendar_view_change', 'map_object_click', 'map_change', 'tree_selection_change', 'timer'], group: 'When',
|
|
242
|
+
help: '(sprint 165) timer: every so many seconds while the page is open and visible (the trigger: a number of seconds, 5 to 3600), e.g. refresh_region of a report. (sprint 161) Component events, trigger: the region. grid_row_init: a row was added to an editable grid (Add row, Duplicate). grid_save: the grid\'s changes were saved (runs on the page that comes back). calendar_date_select: an empty day or hour was chosen (the condition sees YYYY-MM-DD or YYYY-MM-DDTHH:MM; cancel_event keeps the Create link closed). calendar_view_change: another view or period (the condition sees month, week, day or list). map_object_click: a marker or shape was clicked (the condition sees its id column, else its title). map_change: the map was moved or zoomed (the condition sees the zoom level). tree_selection_change: a tree node was chosen (the condition sees its id; cancel_event keeps its link closed). (sprint 156) double_click, mouse_enter, mouse_leave: items, buttons or regions. key_down, key_press (a key that types a character), select (text selected): items. resize, scroll: the window (no trigger). page_unload: the page is left (only quick, local actions finish). before_refresh: a region is about to be refreshed by a Refresh action. facets_change: a faceted search or smart filters region\'s facets were applied (trigger: that region). ' + 'dialog_closed: a modal dialog opened from this page was submitted and closed (instead of reloading the page), e.g. refresh a region. focus / blur / key_release: an item gets or loses the focus, a key is released in it. after_refresh: a region was refreshed or loaded. selection_change: rows were selected in a report (row selection) or a grid\'s master row. before_submit: the page is about to be submitted (a declined confirm keeps it). custom: JavaScript raised it with pgkiln.trigger(\'NAME\', value). menu_entry (Menu entry chosen): an entry {"label": …, "action": "NAME"} of a menu button was chosen. The actions of one event and trigger run in sequence order; a declined confirm stops the ones after it.' },
|
|
241
243
|
{ name: 'trigger_element', label: 'Item(s) / button / region / page / event', kind: 'upper', group: 'When', help: 'Comma separated: item names (change, focus, blur, key_release), a button name (click), region ids or static ids (after_refresh, selection_change, before_refresh, facets_change and the component events grid_*, calendar_*, map_*, tree_selection_change), dialog page numbers (dialog_closed; empty: any dialog), event names (custom), menu entry action names or menu button names (menu_entry: any of its entries; a client-side condition tests the entry\'s name). Empty for load and before_submit.' },
|
|
242
244
|
{ name: 'condition_type', label: 'Client-side condition', kind: 'select', options: ['', 'equals', 'not_equals', 'greater', 'greater_equal', 'less', 'less_equal', 'in_list', 'not_in_list', 'is_null', 'is_not_null'], group: 'When',
|
|
243
245
|
help: 'On the (first) trigger item\'s value. greater / less: numbers as numbers, other values (ISO dates) as text; an empty value fails them. in_list / not_in_list: comma-separated values.' },
|
|
@@ -299,7 +301,7 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
|
|
|
299
301
|
},
|
|
300
302
|
fields: [
|
|
301
303
|
{ name: 'name', label: 'Name', kind: 'text' },
|
|
302
|
-
{ name: 'type', label: 'Type', kind: 'select', options: ['sql', 'form_dml', 'grid_dml', 'data_load', 'invoke_api', 'download', 'chain', 'workflow', 'ai_generate', 'plugin', 'send_push', 'send_email',
|
|
304
|
+
{ name: 'type', label: 'Type', kind: 'select', options: ['sql', 'form_dml', 'grid_dml', 'data_load', 'invoke_api', 'download', 'chain', 'workflow', 'ai_generate', 'plugin', 'send_push', 'send_email', 'send_notification',
|
|
303
305
|
'close_dialog', 'clear_session_state', 'reset_pagination', 'user_preferences', 'server_geocoding', 'print_report', 'human_task_create', 'human_task_manage', 'form_init'],
|
|
304
306
|
help: '(sprint 157, settings in Attributes) close_dialog: {"items": ["P5_ID"]} closes the modal page, handing these items to the page below · clear_session_state: {"items": [...], "pages": [3], "all": false} · reset_pagination: reports and grids start on their first page · user_preferences: {"action": "set", "preference": "DEFAULT_DEPT", "item": "P1_DEPT"} or {"action": "reset", …} · server_geocoding: {"address_item": "P3_ADDRESS", "coordinates_item": "P3_LOCATION"} · print_report: {"document": "EMPLOYEE_SHEET"} (a document template or report query) · human_task_create: {"definition": "LEAVE_APPROVAL", "detail_pk": "&P7_ID.", "parameters": {"DAYS": "&P7_DAYS."}, "id_item": "P7_TASK_ID"} · human_task_manage: {"action": "approve", "task": "&P8_TASK_ID.", "comment": "&P8_COMMENT."} (approve, reject, complete with "outcome", claim, release, delegate with "to", cancel) · form_init: APEX\'s Form – Initialization (the form region fetches its row by itself) · download: a file from the query in Code · chain: runs the processes that name it as their chain, in sequence (optionally in the background) · workflow: start, terminate or retry a workflow · ai_generate: Generate text with AI (an AI service the application may use) · send_push: a push notification to a user\'s devices (Settings → Progressive Web App → push notifications)' },
|
|
305
307
|
{ name: 'point', label: 'Point', kind: 'select', options: ['submit', 'load', 'ajax'], help: 'A chain\'s processes run when the chain does (their own point is not used). ajax: an Ajax callback (APEX: On Demand) that JavaScript calls by name with pgkiln.server.process(\'NAME\', {items: [\'P1_X\'], x01: …}); SQL processes only; the last statement\'s rows come back as JSON, and the items it sets are set on the page.' },
|
|
@@ -307,8 +309,8 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
|
|
|
307
309
|
{ name: 'code', label: 'Code (SQL / PL/pgSQL call)', kind: 'code', wide: true,
|
|
308
310
|
help: 'e.g. select sales.ship_order(:P3_ID::int) as p3_status — returned columns named like items set them. RAISE EXCEPTION messages are shown to the user; USING COLUMN = \'sal\' puts it on that field. · download: a query returning the file\'s content (bytea or text), file name and MIME type, e.g. select content, filename, mime_type from docs.file where id = :P5_ID::int — several rows are sent as one zip file.' },
|
|
309
311
|
{ name: 'region_id', label: 'Form / grid region (form_dml, grid_dml)', kind: 'region' },
|
|
310
|
-
{ name: 'config', label: 'Configuration (data_load, invoke_api, download, chain, workflow, ai_generate, plugin, send_push, send_email)', kind: 'json', wide: true,
|
|
311
|
-
help: 'data_load: {"file_item":"P5_FILE","definition":"ORDERS_LOAD"} (a data load definition: table, format, mapping, transformations) or {"file_item":"P5_FILE","table":"sales.orders","mode":"append | merge | replace","skip_errors":false,"headers":true,"format":"auto | csv | xlsx | json | xml","row_tag":"order","columns":{"Heading in file":"column"}} — runs as the app\'s database role; columns match by name unless mapped. · invoke_api: {"source":"WEATHER","params":{"city":"&P5_CITY."},"items":{"P5_TEMP":"current.temp"},"status_item":"P5_STATUS"} or {"url":"https://api.example.com/orders/&P5_ID.","method":"POST","credential":"SHOP_API","body":"{\\"note\\": &P5_NOTE.}","items":{…}} — without "items", the first row\'s columns set the items named like them. · download: {"content_column":"content","filename_column":"filename","mime_column":"mime_type","zip_name":"files-&P5_ID..zip","disposition":"attachment | inline"} (all optional; default: the first three columns). Or {"export":"csv | json | xml | html | xlsx | pdf","file_name":"orders-&P5_ID.","title":"Orders","layout":"A4_LANDSCAPE","separator":";"}: the query\'s rows as one file (APEX_DATA_EXPORT; meta.data_export does csv, json, xml and html from SQL). · chain: {"background": true, "status_item": "P5_JOB_ID"} runs it after the submit, on the server (status in meta.process_jobs). · workflow: {"action":"start","definition":"APPROVAL","version":"2","detail_pk":"&P5_ID.","variables":{"AMOUNT":"&P5_AMOUNT."},"id_item":"P5_WORKFLOW_ID"} or {"action":"terminate | retry","instance":"&P5_WORKFLOW_ID.","comment":"…"} · send_push: {"to":"&P5_OWNER.","title":"Order &P5_ID. needs approval","body":"…","page":5,"items":{"P5_ID":"&P5_ID."},"tag":"order-&P5_ID.","urgency":"normal | high | low | very-low"} — to the user\'s devices that turned notifications on; the link is signed for that user · ai_generate: {"service":"CLAUDE","system":"You write short, friendly summaries.","prompt":"Summarise: &P5_NOTES.","output_item":"P5_SUMMARY"} (text), {"service":…,"prompt":"Extract the contact from: &P5_TEXT.","output_items":["P5_NAME","P5_EMAIL"]} (structured: a JSON schema built from the items, their labels describe the fields) or {"service":…,"prompt":…,"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"items":{"P5_NAME":"name"}} (your schema; items map to properties or paths); optional "max_tokens", "error_message". &ITEM. values are sent as delimited data, never as instructions. · plugin: {"plugin":"name","attributes":{"NAME":"value or &ITEM."}} (a process plug-in: its function returns the message) · send_email: {"to":"&P5_EMAIL.","cc":"…","bcc":"…","from":"…","reply_to":"…","subject":"Order &P5_ID.","body":"plain text","body_html":"<p>HTML (item values escaped)</p>"} or {"to":"&P5_EMAIL.","template":"ORDER_SHIPPED","placeholders":{"NAME":"&P5_NAME."}}; "attachments":"select content, filename, mime from sales.invoice where order_id = :P5_ID" — queued in the page\'s transaction, sent after the commit through the application\'s mail service' },
|
|
312
|
+
{ name: 'config', label: 'Configuration (data_load, invoke_api, download, chain, workflow, ai_generate, plugin, send_push, send_email, send_notification)', kind: 'json', wide: true,
|
|
313
|
+
help: 'data_load: {"file_item":"P5_FILE","definition":"ORDERS_LOAD"} (a data load definition: table, format, mapping, transformations) or {"file_item":"P5_FILE","table":"sales.orders","mode":"append | merge | replace","skip_errors":false,"headers":true,"format":"auto | csv | xlsx | json | xml","row_tag":"order","columns":{"Heading in file":"column"}} — runs as the app\'s database role; columns match by name unless mapped. · invoke_api: {"source":"WEATHER","params":{"city":"&P5_CITY."},"items":{"P5_TEMP":"current.temp"},"status_item":"P5_STATUS"} or {"url":"https://api.example.com/orders/&P5_ID.","method":"POST","credential":"SHOP_API","body":"{\\"note\\": &P5_NOTE.}","items":{…}} — without "items", the first row\'s columns set the items named like them. · download: {"content_column":"content","filename_column":"filename","mime_column":"mime_type","zip_name":"files-&P5_ID..zip","disposition":"attachment | inline"} (all optional; default: the first three columns). Or {"export":"csv | json | xml | html | xlsx | pdf","file_name":"orders-&P5_ID.","title":"Orders","layout":"A4_LANDSCAPE","separator":";"}: the query\'s rows as one file (APEX_DATA_EXPORT; meta.data_export does csv, json, xml and html from SQL). · chain: {"background": true, "status_item": "P5_JOB_ID"} runs it after the submit, on the server (status in meta.process_jobs). · workflow: {"action":"start","definition":"APPROVAL","version":"2","detail_pk":"&P5_ID.","variables":{"AMOUNT":"&P5_AMOUNT."},"id_item":"P5_WORKFLOW_ID"} or {"action":"terminate | retry","instance":"&P5_WORKFLOW_ID.","comment":"…"} · send_push: {"to":"&P5_OWNER.","title":"Order &P5_ID. needs approval","body":"…","page":5,"items":{"P5_ID":"&P5_ID."},"tag":"order-&P5_ID.","urgency":"normal | high | low | very-low"} — to the user\'s devices that turned notifications on; the link is signed for that user · send_notification: {"to":"&P5_OWNER.","title":"Order &P5_ID. needs approval","body":"…","page":5,"items":{"P5_ID":"&P5_ID."},"icon":"bell","push":false} — an in-app notification (the bell; Settings → User Interface → Notifications); push: also to the user\'s devices · ai_generate: {"service":"CLAUDE","system":"You write short, friendly summaries.","prompt":"Summarise: &P5_NOTES.","output_item":"P5_SUMMARY"} (text), {"service":…,"prompt":"Extract the contact from: &P5_TEXT.","output_items":["P5_NAME","P5_EMAIL"]} (structured: a JSON schema built from the items, their labels describe the fields) or {"service":…,"prompt":…,"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"items":{"P5_NAME":"name"}} (your schema; items map to properties or paths); optional "max_tokens", "error_message". &ITEM. values are sent as delimited data, never as instructions. · plugin: {"plugin":"name","attributes":{"NAME":"value or &ITEM."}} (a process plug-in: its function returns the message) · send_email: {"to":"&P5_EMAIL.","cc":"…","bcc":"…","from":"…","reply_to":"…","subject":"Order &P5_ID.","body":"plain text","body_html":"<p>HTML (item values escaped)</p>"} or {"to":"&P5_EMAIL.","template":"ORDER_SHIPPED","placeholders":{"NAME":"&P5_NAME."}}; "attachments":"select content, filename, mime from sales.invoice where order_id = :P5_ID" — queued in the page\'s transaction, sent after the commit through the application\'s mail service' },
|
|
312
314
|
{ name: 'when_button', label: 'When button pressed', kind: 'upper' },
|
|
313
315
|
...conditionFields('Condition'),
|
|
314
316
|
{ name: 'success_message', label: 'Success message', kind: 'text' },
|
|
@@ -413,6 +415,8 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
|
|
|
413
415
|
{ name: 'parent_id', label: 'Parent entry', kind: 'list_parent', group: 'Entry', help: 'An entry of the same list: this entry is shown below it (a sub menu).' },
|
|
414
416
|
{ name: 'seq', label: 'Sequence', kind: 'int', group: 'Entry' },
|
|
415
417
|
{ name: 'badge', label: 'Badge', kind: 'text', group: 'Entry', help: 'A short text or number, &ITEM. allowed (e.g. &P1_OPEN_COUNT.).' },
|
|
418
|
+
{ name: 'badge_query', label: 'Badge (SQL)', kind: 'code', group: 'Entry',
|
|
419
|
+
help: '(sprint 165) A SELECT whose first value is the badge, run as the application\'s role; wins over Badge. No row or an empty value: no badge. E.g. select nullif(count(*), 0) from sales.orders where status = \'NEW\' · select meta.unread_notifications(). Refreshed while the page is open with Settings → User Interface → Live badges.' },
|
|
416
420
|
{ name: 'description', label: 'Description', kind: 'textarea', group: 'Entry', help: 'Shown by the cards template.' },
|
|
417
421
|
{ name: 'target_page', label: 'Page', kind: 'page', group: 'Target', help: 'Empty with no URL: a heading for its child entries.' },
|
|
418
422
|
{ name: 'target_items', label: 'Set items (JSON)', kind: 'json', group: 'Target', help: '{"P3_ID": "&P1_ID."} — sent with a checksum.' },
|
package/src/builder/i18n/de.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const de: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': 'View',
|
|
4050
4050
|
'docv.doc_column': 'Dokumentspalte',
|
|
4051
4051
|
'docv.intro': 'Eine Dokument-View zeigt jede Zeile von {table} mit ihren Kindzeilen als ein JSON-Dokument, wie eine Oracle JSON Relational Duality View. Berichte, REST (AutoREST auf der View) und MCP lesen ganze Dokumente; ein Update der Dokumentspalte schreibt die Zeile und ihre Kinder in einer Anweisung zurück. Row Level Security und Berechtigungen der Tabelle gelten (die View ist security_invoker und erhält die Berechtigungen der Tabelle).',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': 'Benachrichtigungen',
|
|
4054
|
+
'nfy.bell': 'Benachrichtigungsglocke in der Kopfzeile',
|
|
4055
|
+
'nfy.bell_help': 'Angemeldete Benutzer sehen ihre ungelesenen und neuesten Benachrichtigungen; Öffnen markiert sie als gelesen und führt zu ihrer Seite. Hinzufügen aus SQL mit `meta.notify(user, title, body, page, items)` oder mit einem Prozess vom Typ send_notification; `meta.unread_notifications()` ist die Anzahl, etwa für ein Badge.',
|
|
4056
|
+
'nfy.live': 'Live-Badges (Sekunden)',
|
|
4057
|
+
'nfy.live_ph': 'z. B. 60 (leer: aus)',
|
|
4058
|
+
'nfy.live_help': 'Alle so viele Sekunden (10 bis 3600) aktualisiert eine offene Seite ihre Badges an Ort und Stelle: Navigationsmenü und -leiste, Schaltflächen mit Badge-Abfrage und die Glocke. Nicht, solange der Tab verborgen ist; hält keine Sitzung am Leben.',
|
|
4059
|
+
'nfy.live_invalid': 'Live-Badges: eine ganze Zahl von Sekunden von 10 bis 3600 oder leer.',
|
|
4052
4060
|
};
|
package/src/builder/i18n/en.ts
CHANGED
|
@@ -4053,4 +4053,12 @@ export const en = {
|
|
|
4053
4053
|
'docv.view': 'View',
|
|
4054
4054
|
'docv.doc_column': 'Document column',
|
|
4055
4055
|
'docv.intro': 'A document view shows each row of {table} with its child rows as one JSON document, like an Oracle JSON Relational Duality View. Reports, REST (AutoREST on the view) and MCP read whole documents; an update of the document column writes the row and its children back in one statement. The table\'s row level security and grants apply (the view is security_invoker and gets the table\'s grants).',
|
|
4056
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4057
|
+
'nfy.heading': 'Notifications',
|
|
4058
|
+
'nfy.bell': 'Notification bell in the header',
|
|
4059
|
+
'nfy.bell_help': 'Signed-in users see their unread notifications and the newest ones; opening one marks it read and goes to its page. Add them from SQL with `meta.notify(user, title, body, page, items)` or with a process of type send_notification; `meta.unread_notifications()` is the count, e.g. for a badge.',
|
|
4060
|
+
'nfy.live': 'Live badges (seconds)',
|
|
4061
|
+
'nfy.live_ph': 'e.g. 60 (empty: off)',
|
|
4062
|
+
'nfy.live_help': 'Every so many seconds (10 to 3600) an open page refreshes its badges in place: the navigation menu and bar, buttons with a badge query and the bell. Not while the tab is hidden; it doesn\'t keep a session alive.',
|
|
4063
|
+
'nfy.live_invalid': 'Live badges: a whole number of seconds from 10 to 3600, or empty.',
|
|
4056
4064
|
} as const;
|
package/src/builder/i18n/es.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const es: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': 'Vista',
|
|
4050
4050
|
'docv.doc_column': 'Columna del documento',
|
|
4051
4051
|
'docv.intro': 'Una vista de documentos muestra cada fila de {table} con sus filas hijas como un documento JSON, como una JSON Relational Duality View de Oracle. Los informes, REST (AutoREST sobre la vista) y MCP leen documentos enteros; un update de la columna del documento escribe la fila y sus hijas en una sentencia. Se aplican la seguridad a nivel de fila y los permisos de la tabla (la vista es security_invoker y recibe los permisos de la tabla).',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': 'Notificaciones',
|
|
4054
|
+
'nfy.bell': 'Campana de notificaciones en la cabecera',
|
|
4055
|
+
'nfy.bell_help': 'Los usuarios con sesión ven sus notificaciones sin leer y las más recientes; abrir una la marca como leída y va a su página. Añádalas desde SQL con `meta.notify(user, title, body, page, items)` o con un proceso de tipo send_notification; `meta.unread_notifications()` es el número, p. ej. para una insignia.',
|
|
4056
|
+
'nfy.live': 'Insignias en vivo (segundos)',
|
|
4057
|
+
'nfy.live_ph': 'p. ej. 60 (vacío: desactivado)',
|
|
4058
|
+
'nfy.live_help': 'Cada tantos segundos (10 a 3600) una página abierta actualiza sus insignias en su sitio: el menú y la barra de navegación, los botones con consulta de insignia y la campana. No mientras la pestaña está oculta; no mantiene viva la sesión.',
|
|
4059
|
+
'nfy.live_invalid': 'Insignias en vivo: un número entero de segundos de 10 a 3600, o vacío.',
|
|
4052
4060
|
};
|
package/src/builder/i18n/fr.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const fr: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': 'Vue',
|
|
4050
4050
|
'docv.doc_column': 'Colonne document',
|
|
4051
4051
|
'docv.intro': 'Une vue document montre chaque ligne de {table} avec ses lignes enfants comme un seul document JSON, comme une JSON Relational Duality View d\'Oracle. Les rapports, REST (AutoREST sur la vue) et MCP lisent des documents entiers ; un update de la colonne document réécrit la ligne et ses enfants en une instruction. La sécurité au niveau des lignes et les droits de la table s\'appliquent (la vue est security_invoker et reçoit les droits de la table).',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': 'Notifications',
|
|
4054
|
+
'nfy.bell': 'Cloche de notifications dans l\'en-tête',
|
|
4055
|
+
'nfy.bell_help': 'Les utilisateurs connectés voient leurs notifications non lues et les plus récentes ; en ouvrir une la marque comme lue et mène à sa page. Ajoutez-les en SQL avec `meta.notify(user, title, body, page, items)` ou avec un processus de type send_notification ; `meta.unread_notifications()` en donne le nombre, p. ex. pour un badge.',
|
|
4056
|
+
'nfy.live': 'Badges en direct (secondes)',
|
|
4057
|
+
'nfy.live_ph': 'p. ex. 60 (vide : désactivé)',
|
|
4058
|
+
'nfy.live_help': 'Toutes les N secondes (10 à 3600), une page ouverte actualise ses badges sur place : menu et barre de navigation, boutons avec requête de badge et cloche. Pas tant que l\'onglet est masqué ; cela ne prolonge pas la session.',
|
|
4059
|
+
'nfy.live_invalid': 'Badges en direct : un nombre entier de secondes de 10 à 3600, ou vide.',
|
|
4052
4060
|
};
|
package/src/builder/i18n/it.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const it: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': 'Vista',
|
|
4050
4050
|
'docv.doc_column': 'Colonna documento',
|
|
4051
4051
|
'docv.intro': 'Una vista documento mostra ogni riga di {table} con le sue righe figlie come un unico documento JSON, come una JSON Relational Duality View di Oracle. Report, REST (AutoREST sulla vista) e MCP leggono documenti interi; un update della colonna documento riscrive la riga e i suoi figli in un\'istruzione. Valgono la sicurezza a livello di riga e i permessi della tabella (la vista è security_invoker e riceve i permessi della tabella).',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': 'Notifiche',
|
|
4054
|
+
'nfy.bell': 'Campanella delle notifiche nell\'intestazione',
|
|
4055
|
+
'nfy.bell_help': 'Gli utenti connessi vedono le notifiche da leggere e le più recenti; aprirne una la segna come letta e porta alla sua pagina. Aggiungile da SQL con `meta.notify(user, title, body, page, items)` o con un processo di tipo send_notification; `meta.unread_notifications()` è il numero, es. per un badge.',
|
|
4056
|
+
'nfy.live': 'Badge in tempo reale (secondi)',
|
|
4057
|
+
'nfy.live_ph': 'es. 60 (vuoto: disattivato)',
|
|
4058
|
+
'nfy.live_help': 'Ogni tot secondi (da 10 a 3600) una pagina aperta aggiorna i suoi badge sul posto: menu e barra di navigazione, pulsanti con query del badge e campanella. Non mentre la scheda è nascosta; non tiene viva la sessione.',
|
|
4059
|
+
'nfy.live_invalid': 'Badge in tempo reale: un numero intero di secondi da 10 a 3600, o vuoto.',
|
|
4052
4060
|
};
|
package/src/builder/i18n/ja.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const ja: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': 'ビュー',
|
|
4050
4050
|
'docv.doc_column': 'ドキュメント列',
|
|
4051
4051
|
'docv.intro': 'ドキュメント・ビューは、{table} の各行を子行とともに 1 つの JSON ドキュメントとして表示します (Oracle の JSON Relational Duality View と同様)。レポート、REST (ビュー上の AutoREST)、MCP はドキュメント全体を読み取り、ドキュメント列を更新すると行と子が 1 文で書き戻されます。表の行レベル・セキュリティと権限が適用されます (ビューは security_invoker で、表の権限を受け取ります)。',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': '通知',
|
|
4054
|
+
'nfy.bell': 'ヘッダーの通知ベル',
|
|
4055
|
+
'nfy.bell_help': 'サインインしたユーザーには未読の通知と最新の通知が表示され、開くと既読になってそのページに移動します。SQL から `meta.notify(user, title, body, page, items)` または send_notification タイプのプロセスで追加します。`meta.unread_notifications()` は件数で、バッジなどに使えます。',
|
|
4056
|
+
'nfy.live': 'ライブ・バッジ (秒)',
|
|
4057
|
+
'nfy.live_ph': '例: 60 (空: オフ)',
|
|
4058
|
+
'nfy.live_help': '開いているページは指定秒数 (10 から 3600) ごとにバッジをその場で更新します: ナビゲーション・メニューとバー、バッジ問合せのあるボタン、ベル。タブが非表示の間は更新せず、セッションを延長しません。',
|
|
4059
|
+
'nfy.live_invalid': 'ライブ・バッジ: 10 から 3600 の整数秒、または空。',
|
|
4052
4060
|
};
|
package/src/builder/i18n/ko.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const ko: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': '뷰',
|
|
4050
4050
|
'docv.doc_column': '문서 열',
|
|
4051
4051
|
'docv.intro': '문서 뷰는 {table}의 각 행을 하위 행과 함께 하나의 JSON 문서로 보여 줍니다(Oracle JSON Relational Duality View와 유사). 보고서, REST(뷰의 AutoREST), MCP는 문서 전체를 읽고, 문서 열을 업데이트하면 행과 하위 행이 한 문으로 다시 기록됩니다. 테이블의 행 수준 보안과 권한이 적용됩니다(뷰는 security_invoker이며 테이블의 권한을 받음).',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': '알림',
|
|
4054
|
+
'nfy.bell': '헤더의 알림 벨',
|
|
4055
|
+
'nfy.bell_help': '로그인한 사용자는 읽지 않은 알림과 최신 알림을 봅니다. 알림을 열면 읽음으로 표시되고 해당 페이지로 이동합니다. SQL에서 `meta.notify(user, title, body, page, items)` 또는 send_notification 유형의 프로세스로 추가하세요. `meta.unread_notifications()`는 개수로, 예를 들어 배지에 사용합니다.',
|
|
4056
|
+
'nfy.live': '실시간 배지(초)',
|
|
4057
|
+
'nfy.live_ph': '예: 60(비우면 끔)',
|
|
4058
|
+
'nfy.live_help': '열린 페이지가 지정한 초(10~3600)마다 배지를 제자리에서 새로 고칩니다: 탐색 메뉴와 탐색 표시줄, 배지 쿼리가 있는 버튼, 벨. 탭이 숨겨져 있는 동안은 하지 않으며 세션을 연장하지 않습니다.',
|
|
4059
|
+
'nfy.live_invalid': '실시간 배지: 10~3600 사이의 정수 초 또는 빈 값.',
|
|
4052
4060
|
};
|
package/src/builder/i18n/nl.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const nl: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': 'View',
|
|
4050
4050
|
'docv.doc_column': 'Documentkolom',
|
|
4051
4051
|
'docv.intro': 'Een documentview toont elke rij van {table} met zijn kindrijen als één JSON-document, zoals een Oracle JSON Relational Duality View. Rapporten, REST (AutoREST op de view) en MCP lezen hele documenten; een update van de documentkolom schrijft de rij en zijn kinderen in één opdracht terug. De row level security en rechten van de tabel gelden (de view is security_invoker en krijgt de rechten van de tabel).',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': 'Meldingen',
|
|
4054
|
+
'nfy.bell': 'Meldingenbel in de kop',
|
|
4055
|
+
'nfy.bell_help': 'Aangemelde gebruikers zien hun ongelezen en nieuwste meldingen; een melding openen markeert haar als gelezen en gaat naar haar pagina. Voeg ze toe vanuit SQL met `meta.notify(user, title, body, page, items)` of met een proces van het type send_notification; `meta.unread_notifications()` is het aantal, bijv. voor een badge.',
|
|
4056
|
+
'nfy.live': 'Live badges (seconden)',
|
|
4057
|
+
'nfy.live_ph': 'bijv. 60 (leeg: uit)',
|
|
4058
|
+
'nfy.live_help': 'Elke zoveel seconden (10 tot 3600) ververst een open pagina haar badges ter plekke: het navigatiemenu en de navigatiebalk, knoppen met een badge-query en de bel. Niet terwijl het tabblad verborgen is; het houdt geen sessie in leven.',
|
|
4059
|
+
'nfy.live_invalid': 'Live badges: een geheel aantal seconden van 10 tot 3600, of leeg.',
|
|
4052
4060
|
};
|
package/src/builder/i18n/pt.ts
CHANGED
|
@@ -4049,4 +4049,12 @@ export const pt: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': 'View',
|
|
4050
4050
|
'docv.doc_column': 'Coluna do documento',
|
|
4051
4051
|
'docv.intro': 'Uma view de documentos mostra cada linha de {table} com as suas linhas filhas como um único documento JSON, como uma JSON Relational Duality View da Oracle. Relatórios, REST (AutoREST sobre a view) e MCP leem documentos inteiros; um update da coluna do documento escreve a linha e os seus filhos numa instrução. Aplicam-se a segurança em nível de linha e as permissões da tabela (a view é security_invoker e recebe as permissões da tabela).',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': 'Notificações',
|
|
4054
|
+
'nfy.bell': 'Sino de notificações no cabeçalho',
|
|
4055
|
+
'nfy.bell_help': 'Os utilizadores com sessão veem as notificações por ler e as mais recentes; abrir uma marca-a como lida e vai para a sua página. Adicione-as a partir de SQL com `meta.notify(user, title, body, page, items)` ou com um processo do tipo send_notification; `meta.unread_notifications()` é o número, p. ex. para um distintivo.',
|
|
4056
|
+
'nfy.live': 'Distintivos em direto (segundos)',
|
|
4057
|
+
'nfy.live_ph': 'p. ex. 60 (vazio: desligado)',
|
|
4058
|
+
'nfy.live_help': 'A cada tantos segundos (10 a 3600) uma página aberta atualiza os seus distintivos no lugar: o menu e a barra de navegação, os botões com consulta de distintivo e o sino. Não enquanto o separador está oculto; não mantém a sessão viva.',
|
|
4059
|
+
'nfy.live_invalid': 'Distintivos em direto: um número inteiro de segundos de 10 a 3600, ou vazio.',
|
|
4052
4060
|
};
|
|
@@ -4049,4 +4049,12 @@ export const zhCN: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': '视图',
|
|
4050
4050
|
'docv.doc_column': '文档列',
|
|
4051
4051
|
'docv.intro': '文档视图将 {table} 的每一行及其子行显示为一个 JSON 文档,类似 Oracle JSON Relational Duality View。报表、REST(视图上的 AutoREST)和 MCP 读取整个文档;更新文档列会在一条语句中写回该行及其子行。表的行级安全性和授权适用(视图为 security_invoker,并获得表的授权)。',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': '通知',
|
|
4054
|
+
'nfy.bell': '页眉中的通知铃铛',
|
|
4055
|
+
'nfy.bell_help': '已登录用户可看到未读通知和最新通知;打开一条通知会将其标为已读并转到其页面。在 SQL 中用 `meta.notify(user, title, body, page, items)` 或类型为 send_notification 的流程添加通知;`meta.unread_notifications()` 返回数量,例如用作徽章。',
|
|
4056
|
+
'nfy.live': '实时徽章(秒)',
|
|
4057
|
+
'nfy.live_ph': '例如 60(空:关闭)',
|
|
4058
|
+
'nfy.live_help': '打开的页面每隔若干秒(10 到 3600)就地刷新其徽章:导航菜单和导航栏、带徽章查询的按钮以及铃铛。标签页隐藏时不刷新;不会让会话保持活动。',
|
|
4059
|
+
'nfy.live_invalid': '实时徽章:10 到 3600 的整数秒,或留空。',
|
|
4052
4060
|
};
|
|
@@ -4049,4 +4049,12 @@ export const zhTW: BuilderMessages = {
|
|
|
4049
4049
|
'docv.view': '檢視',
|
|
4050
4050
|
'docv.doc_column': '文件欄',
|
|
4051
4051
|
'docv.intro': '文件檢視將 {table} 的每一列及其子列顯示為一個 JSON 文件,類似 Oracle JSON Relational Duality View。報表、REST(檢視上的 AutoREST)和 MCP 讀取整個文件;更新文件欄會在一個陳述式中寫回該列及其子列。資料表的列層級安全性和授權適用(檢視為 security_invoker,並獲得資料表的授權)。',
|
|
4052
|
+
// ------------------------------------------------------------ Notifications and live badges (sprint 165)
|
|
4053
|
+
'nfy.heading': '通知',
|
|
4054
|
+
'nfy.bell': '頁首中的通知鈴鐺',
|
|
4055
|
+
'nfy.bell_help': '已登入使用者可看到未讀通知和最新通知;開啟一則通知會將其標為已讀並前往其頁面。在 SQL 中用 `meta.notify(user, title, body, page, items)` 或類型為 send_notification 的流程新增通知;`meta.unread_notifications()` 傳回數量,例如用作徽章。',
|
|
4056
|
+
'nfy.live': '即時徽章(秒)',
|
|
4057
|
+
'nfy.live_ph': '例如 60(空:關閉)',
|
|
4058
|
+
'nfy.live_help': '開啟的頁面每隔若干秒(10 到 3600)就地重新整理其徽章:導覽選單和導覽列、帶徽章查詢的按鈕以及鈴鐺。分頁隱藏時不重新整理;不會讓工作階段保持活動。',
|
|
4059
|
+
'nfy.live_invalid': '即時徽章:10 到 3600 的整數秒,或留空。',
|
|
4052
4060
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { owner } from '../db.ts';
|
|
2
|
+
import { html, raw } from '../html.ts';
|
|
3
|
+
import type { BuilderTranslate } from './i18n.ts';
|
|
4
|
+
import { input } from './ui.ts';
|
|
5
|
+
|
|
6
|
+
// (sprint 165, migration 154) Settings → User Interface → Notifications: the notification bell
|
|
7
|
+
// (meta.app.notifications) and live badges (meta.app.live_badges_s, 10 to 3600 seconds or none).
|
|
8
|
+
|
|
9
|
+
/** The fields, in the User Interface tab of the application's settings. */
|
|
10
|
+
export function notificationSettings(a: { notifications?: boolean | null; live_badges_s?: number | null }, tx: BuilderTranslate) {
|
|
11
|
+
return html`<h3 id="notifications">${tx('nfy.heading')}</h3>
|
|
12
|
+
<input type="hidden" name="notification_settings" value="1">
|
|
13
|
+
<div class="field"><label class="check"><input type="checkbox" name="notifications" value="true"${a.notifications ? raw(' checked') : ''}> ${tx('nfy.bell')}</label>
|
|
14
|
+
<small class="help">${tx.code('nfy.bell_help')}</small></div>
|
|
15
|
+
<div class="form-grid">
|
|
16
|
+
${input('live_badges_s', tx('nfy.live'), a.live_badges_s ?? '', { type: 'number', placeholder: tx('nfy.live_ph'), help: tx('nfy.live_help') })}
|
|
17
|
+
</div>`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Save them (only when the form had them); a number outside 10 to 3600 is an error. */
|
|
21
|
+
export async function saveNotificationSettings(appId: string, b: Record<string, string | undefined>, tx: BuilderTranslate) {
|
|
22
|
+
if (b.notification_settings !== '1') return;
|
|
23
|
+
const raw = (b.live_badges_s ?? '').trim();
|
|
24
|
+
const seconds = raw === '' ? null : Number(raw);
|
|
25
|
+
if (seconds !== null && !(Number.isInteger(seconds) && seconds >= 10 && seconds <= 3600)) throw new Error(tx('nfy.live_invalid'));
|
|
26
|
+
await owner.query('update meta.app set notifications = $2, live_badges_s = $3 where id = $1', [appId, b.notifications === 'true', seconds]);
|
|
27
|
+
}
|
package/src/builder/routes.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { currentWorkspace, placeApp, workshopDb, workspaceRoutes } from './works
|
|
|
23
23
|
import { securityRoutes } from './security.ts';
|
|
24
24
|
import { instanceRoutes } from './instance.ts';
|
|
25
25
|
import { saveTimeZoneSettings, timeZoneSettings } from './globalization.ts';
|
|
26
|
+
import { notificationSettings, saveNotificationSettings } from './notification-settings.ts';
|
|
26
27
|
import { WIZARD_KINDS, wizardRoutes, wizardTables } from './wizards.ts';
|
|
27
28
|
import { createPageRoutes } from './create-page.ts';
|
|
28
29
|
import { featureRoutes } from './features.ts';
|
|
@@ -881,6 +882,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
881
882
|
<small class="help">${tx('st.user_choice_help')}</small></div>
|
|
882
883
|
<p><a class="btn" href="${BASE}/apps/${a.id}/theme">${icon('settings')} ${tx('st.roller')}</a>
|
|
883
884
|
<span class="muted">${(() => { const n = appStyles(a.theme).length; return n ? tx(n === 1 ? 'st.styles_one' : 'st.styles', { n }) : tx('st.no_styles'); })()}</span></p>
|
|
885
|
+
${notificationSettings(a, tx)}
|
|
884
886
|
`)}${end}
|
|
885
887
|
${panel('globalization')}${region(tx('st.tab.globalization'), html`
|
|
886
888
|
<div class="form-grid">
|
|
@@ -992,6 +994,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
992
994
|
(b.version ?? '').trim().slice(0, 40) || null],
|
|
993
995
|
);
|
|
994
996
|
await saveTimeZoneSettings(req.params.id, b);
|
|
997
|
+
await saveNotificationSettings(req.params.id, b, tr(s));
|
|
995
998
|
await saveLoginSettings(req.params.id, b);
|
|
996
999
|
flash(s, tr(s)('st.settings_saved'));
|
|
997
1000
|
} catch (e) {
|
package/src/cli/replace.ts
CHANGED
|
@@ -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}.",
|