pgkiln 0.31.0 → 0.36.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 (131) hide show
  1. package/README.md +2 -2
  2. package/SECURITY.md +10 -0
  3. package/db/migrations/076_form_lost_update.sql +50 -0
  4. package/db/migrations/077_warn_unsaved.sql +41 -0
  5. package/db/migrations/078_url_region.sql +12 -0
  6. package/db/migrations/079_developer_toolbar.sql +53 -0
  7. package/db/migrations/080_search.sql +326 -0
  8. package/db/migrations/081_mail.sql +621 -0
  9. package/db/migrations/082_app_backups.sql +73 -0
  10. package/db/migrations/083_availability.sql +156 -0
  11. package/db/migrations/084_feedback.sql +46 -0
  12. package/db/migrations/085_page_behaviour.sql +65 -0
  13. package/db/migrations/086_data_settings.sql +599 -0
  14. package/db/migrations/087_login_page.sql +175 -0
  15. package/db/migrations/088_region_templates.sql +18 -0
  16. package/db/migrations/089_page_groups.sql +330 -0
  17. package/docs/apex-feature-parity.md +61 -39
  18. package/docs/guide/01-installation.md +1 -0
  19. package/docs/guide/03-builder.md +154 -4
  20. package/docs/guide/04-pages-and-regions.md +157 -3
  21. package/docs/guide/05-items.md +47 -0
  22. package/docs/guide/06-processing.md +210 -2
  23. package/docs/guide/07-dynamic-actions.md +60 -2
  24. package/docs/guide/08-security.md +56 -4
  25. package/docs/guide/09-reference.md +21 -0
  26. package/docs/guide/11-from-apex.md +9 -2
  27. package/docs/guide/12-development.md +40 -2
  28. package/docs/guide/14-globalization.md +15 -0
  29. package/docs/guide/15-extensions.md +1 -1
  30. package/docs/guide/17-mobile.md +3 -1
  31. package/docs/guide/18-cli.md +32 -1
  32. package/docs/guide/20-ai-agents.md +5 -2
  33. package/examples/hr/hr_50_search.sql +47 -0
  34. package/examples/hr/hr_51_mail.sql +70 -0
  35. package/examples/hr/hr_52_feedback.sql +15 -0
  36. package/examples/hr/hr_53_data_settings.sql +108 -0
  37. package/examples/hr/hr_54_region_templates.sql +67 -0
  38. package/examples/hr/hr_55_page_groups.sql +24 -0
  39. package/package.json +3 -1
  40. package/public/app.css +122 -3
  41. package/public/app.js +496 -13
  42. package/public/builder.css +22 -0
  43. package/public/builder.js +77 -0
  44. package/public/code-editor.css +3 -3
  45. package/public/sw.js +5 -3
  46. package/src/app.ts +10 -0
  47. package/src/appfiles.ts +13 -0
  48. package/src/automations.ts +12 -0
  49. package/src/builder/advisor.ts +68 -14
  50. package/src/builder/arrange.ts +2 -2
  51. package/src/builder/availability.ts +74 -0
  52. package/src/builder/backups.ts +134 -0
  53. package/src/builder/components.ts +123 -16
  54. package/src/builder/ddl-tools.ts +114 -0
  55. package/src/builder/designer.ts +35 -4
  56. package/src/builder/feedback.ts +82 -0
  57. package/src/builder/forms.ts +10 -5
  58. package/src/builder/home.ts +3 -0
  59. package/src/builder/locks.ts +15 -5
  60. package/src/builder/login-page.ts +82 -0
  61. package/src/builder/mail.ts +425 -0
  62. package/src/builder/newapp.ts +1 -1
  63. package/src/builder/region-help.ts +73 -0
  64. package/src/builder/region-settings.ts +19 -3
  65. package/src/builder/routes.ts +60 -5
  66. package/src/builder/search.ts +4 -4
  67. package/src/builder/shared.ts +3 -2
  68. package/src/builder/sql.ts +3 -0
  69. package/src/builder/ui.ts +9 -3
  70. package/src/cli/apps.ts +46 -5
  71. package/src/cli/main.ts +66 -5
  72. package/src/cli/mcp.ts +58 -4
  73. package/src/cli/replace.ts +40 -4
  74. package/src/ddl.ts +509 -0
  75. package/src/geocode.ts +44 -0
  76. package/src/i18n/ar.ts +56 -0
  77. package/src/i18n/cs.ts +56 -0
  78. package/src/i18n/da.ts +56 -0
  79. package/src/i18n/de.ts +56 -0
  80. package/src/i18n/el.ts +56 -0
  81. package/src/i18n/es.ts +56 -0
  82. package/src/i18n/fi.ts +56 -0
  83. package/src/i18n/fr.ts +56 -0
  84. package/src/i18n/he.ts +56 -0
  85. package/src/i18n/it.ts +56 -0
  86. package/src/i18n/ja.ts +56 -0
  87. package/src/i18n/ko.ts +56 -0
  88. package/src/i18n/nb.ts +56 -0
  89. package/src/i18n/pl.ts +56 -0
  90. package/src/i18n/pt.ts +56 -0
  91. package/src/i18n/ru.ts +56 -0
  92. package/src/i18n/sv.ts +56 -0
  93. package/src/i18n/tr.ts +56 -0
  94. package/src/i18n/uk.ts +56 -0
  95. package/src/i18n/zh.ts +56 -0
  96. package/src/i18n.ts +112 -0
  97. package/src/icons.ts +2 -1
  98. package/src/layout.ts +3 -2
  99. package/src/mail/mail.ts +148 -0
  100. package/src/mail/providers.ts +444 -0
  101. package/src/metadata.ts +34 -10
  102. package/src/runtime/ajax.ts +97 -0
  103. package/src/runtime/authz.ts +1 -1
  104. package/src/runtime/availability.ts +37 -0
  105. package/src/runtime/context.ts +46 -6
  106. package/src/runtime/data-export.ts +94 -0
  107. package/src/runtime/data-items.ts +194 -0
  108. package/src/runtime/devbar.ts +184 -0
  109. package/src/runtime/display-selector.ts +12 -4
  110. package/src/runtime/engine.ts +26 -1
  111. package/src/runtime/feedback.ts +128 -0
  112. package/src/runtime/grid.ts +170 -26
  113. package/src/runtime/items.ts +8 -0
  114. package/src/runtime/lists.ts +11 -1
  115. package/src/runtime/password-reset.ts +171 -0
  116. package/src/runtime/processes.ts +83 -2
  117. package/src/runtime/pwa.ts +7 -10
  118. package/src/runtime/regions.ts +41 -0
  119. package/src/runtime/render.ts +48 -7
  120. package/src/runtime/report-views.ts +4 -2
  121. package/src/runtime/report.ts +36 -22
  122. package/src/runtime/rest.ts +4 -0
  123. package/src/runtime/routes.ts +94 -37
  124. package/src/runtime/search.ts +252 -0
  125. package/src/runtime/signin.ts +80 -0
  126. package/src/runtime/url-region.ts +93 -0
  127. package/src/security.ts +11 -1
  128. package/src/server.ts +2 -0
  129. package/src/session.ts +8 -2
  130. package/src/static-version.ts +34 -0
  131. package/src/workingcopy.ts +2 -2
@@ -13,6 +13,32 @@ A page has a number (unique in the app), a name, a title, and these behaviours:
13
13
  | `requires_auth` | boolean | `false` makes the page public in an app with a login |
14
14
  | `authz` | scheme name | Who may open it (403 otherwise) |
15
15
  | `protection` | `checksum`, `unrestricted` | Whether URL item values need a checksum |
16
+ | `warn_unsaved` | boolean | Ask before the user leaves the page with unsaved changes, see [below](#warn-on-unsaved-changes) |
17
+
18
+ ### Warn on unsaved changes
19
+
20
+ With **Warn on unsaved changes** (Page Designer → Page → Appearance; APEX: the page attribute of the
21
+ same name), the browser asks *"You have unsaved changes. Leave this page anyway?"* when the user
22
+ leaves the page while an item has a value other than the one the page was shown with: a link, a
23
+ redirect button (such as *Cancel*), the navigation menu, closing a dialog (its × or Escape),
24
+ reloading or closing the tab. Submitting the page with a button doesn't ask (that saves). After a
25
+ submit that failed (a validation error), the values on the page are not saved yet, so leaving asks
26
+ even without a further change. The check is in the browser (`pgkiln.isChanged()` tells your own
27
+ JavaScript, like APEX's `apex.page.isChanged()`); without JavaScript there is none.
28
+
29
+ - **Leave an item out** with `{"warn_unsaved": false}` in its attributes (APEX: the item's *Warn on
30
+ Unsaved Changes: Ignore*), for example a search field.
31
+ - The interactive grid has its own check of unsaved rows, which is always on.
32
+ - A value a dynamic action changes on page load counts as a change.
33
+ - The create page wizards switch it on for the form pages they make; existing pages have it off
34
+ until you switch it on.
35
+
36
+ ### Minimal pages
37
+
38
+ Page template **Minimal (no navigation)** (Page Designer → Page → Appearance; APEX: the page
39
+ template of the same name) shows the page without the header, the navigation menu and the
40
+ navigation bar: the title and the regions only, centred. The application's
41
+ [login page](08-security.md#the-login-page) uses it and gets the sign-in page's backdrop.
16
42
 
17
43
  ### Dialogs and drawers
18
44
 
@@ -43,6 +69,18 @@ Region **templates**:
43
69
  | `standard` | A card with a header (title and buttons) and a body |
44
70
  | `plain` | No frame and no header: content only (good for KPI cards and intro text) |
45
71
  | `collapsible` | A standard card whose body can be collapsed by clicking the header |
72
+ | `hero` | A large banner: an icon (`{"icon": "users"}` in the attributes), the title, the text and the buttons (APEX: Hero) |
73
+ | `title_bar` | The title and buttons on one line over the content, without a card (APEX: Title Bar) |
74
+ | `buttons_container` | The buttons only (after any text), without a header (APEX: Buttons Container) |
75
+ | `inline_dialog` | Hidden until opened, then a dialog over the page with a close button; Esc and a click beside it close it too (APEX: Inline Dialog) |
76
+ | `inline_drawer` | The same, as a panel docked to the right edge (APEX: Inline Drawer) |
77
+ | `wizard` | A narrow, centred card for one step of a wizard; a [list](#list-lists) region with template `wizard_progress` above it shows the steps (APEX: Wizard Container) |
78
+
79
+ An inline dialog or drawer opens with the dynamic action **Open region** (and closes with **Close
80
+ region**), e.g. on the click of a button with action *Defined by dynamic action*, or with a link to
81
+ `#R<id>` (which also works without JavaScript). Items in it are part of the page: a submit sends
82
+ them along. The carousel is a style of the [display selector](#display_selector-region-display-selector)
83
+ region (`{"style": "carousel"}`). The HR example's page 44 (*Region templates*) shows them all.
46
84
 
47
85
  Every region also has `seq` (order), `condition` (a SQL boolean expression; the region renders
48
86
  only when true) and `authz` (an authorization scheme).
@@ -97,7 +135,7 @@ database only checks the shape (at most 12 names of lower case letters, digits a
97
135
  | Type | Purpose |
98
136
  |---|---|
99
137
  | [`report`](#report-interactive-report) | Read-only table from a SELECT, with search, filters, sorting, control break, aggregates, highlights, computed columns, group by, pivot and chart views, row selection, saved reports, paging and CSV/Excel/PDF download |
100
- | [`grid`](#grid-interactive-grid) | Editable table on one database table, with aggregates, frozen, movable and resizable columns, saved grid reports, a row actions menu, master-detail and copy/paste of cells |
138
+ | [`grid`](#grid-interactive-grid) | Editable table on one database table, with aggregates, frozen, movable and resizable columns, saved grid reports, a row actions menu, master-detail, copy/paste of cells, and single row, group by and chart views |
101
139
  | [`form`](#form) | Fields for one row of a table, with automatic fetch and save |
102
140
  | [`chart`](#chart) | Bar, column, stacked, line, area, combo, scatter, bubble, donut, pie, gauge, funnel, radar, Gantt, pyramid or polar chart from a SELECT, with drill-down links |
103
141
  | [`cards`](#cards) | Cards or KPI tiles from a SELECT |
@@ -113,6 +151,9 @@ database only checks the shape (at most 12 names of lower case letters, digits a
113
151
  | [`template_component`](#template-components) | Each row (or all rows) of a SELECT through a template component: badges, contact cards, timelines, your own |
114
152
  | [`tasks`](06-processing.md#approvals-and-the-task-list) | Task list: approvals and actions for the signed-in user |
115
153
  | [`workflows`](06-processing.md#workflows) | Workflow console: the workflows the user started or administers |
154
+ | [`search`](#search-search-region) | One search field over several sources (search configurations), results grouped per source |
155
+ | [`url`](#url-url-region) | Another site's page (or a page of this server) in a sandboxed frame |
156
+ | `login` | The sign-in form of the application's own login page (no attributes): identity providers, user name and password, *Keep me signed in*, *Forgot password?*; see [chapter 8](08-security.md#the-login-page) |
116
157
  | [`static`](#static-and-dynamic-content) | Fixed HTML with `&ITEM.` substitutions |
117
158
  | [`dynamic`](#static-and-dynamic-content) | HTML produced by a SELECT |
118
159
 
@@ -333,6 +374,7 @@ Attributes:
333
374
  | `master` | | Makes it (or a report, chart, cards …) a **detail**: `{"item": "P27_DEPTNO", "column": "deptno"}` (below) |
334
375
  | `actions` | `true` | `false` hides the Actions menu (columns, aggregates, saved reports) |
335
376
  | `saved_reports`, `public_reports` | `true`, none | As for reports |
377
+ | `views` | all | The views users may switch to besides the grid: `["row", "group", "chart"]`; `[]` for the grid only |
336
378
 
337
379
  Cell editors follow the column type: number, date, date-time, checkbox (boolean), select list
338
380
  (with `lov`) or text.
@@ -377,6 +419,23 @@ user can only select a row the server showed them; the item is not taken from th
377
419
  submit otherwise. A detail **grid** with `master.column` fills that column with the selected value
378
420
  on its new rows (and never lets users edit it); saving new rows without a selection is refused.
379
421
 
422
+ **Views** (APEX: Single Row View, Group By, Chart). Besides the rows, a grid has three views
423
+ (`r<id>_v=row|group|chart` in the URL, kept in saved grid reports); `views` limits them:
424
+
425
+ - **Single row view**: one row at a time as a form, opened from **Actions → Single row view** or
426
+ a row's ⋮ menu. *First*, *Previous*, *Next* and *Last* move through the rows of the search in
427
+ the grid's order (`r<id>_row=N`, the position from 1); **Add row** opens an empty form
428
+ (`r<id>_row=new`). It edits the same fields as the grid and is saved by the same `grid_dml`
429
+ process, with the same checks (signed keys, required columns, lists of values); the user is
430
+ warned before leaving with unsaved changes. Give the query an `order by` (or sort the grid)
431
+ so that positions stay the same after a save, as for paging.
432
+ - **Group by** and **Chart**: set up in the Actions menu as for an
433
+ [interactive report](#report-interactive-report), over every row of the search. Once
434
+ set up, a switcher above the grid moves between *Grid* and these views.
435
+
436
+ The developer's `hidden` columns can't be grouped, charted or shown in the single row view.
437
+ After **Save** the grid comes back as it was: its search, page, sort and view.
438
+
380
439
  **Copy and paste.** Click a cell and Shift+click another to select a range; Ctrl+C copies it
381
440
  as tab-separated text (which spreadsheets paste as cells). Ctrl+V pastes such text (from a
382
441
  spreadsheet or another grid) from the focused cell on, adding rows when the grid allows inserts;
@@ -411,6 +470,18 @@ that column.
411
470
  Typical form buttons (as generated by the wizard): *Cancel* (redirect), *Delete* (condition
412
471
  `:P3_ID is not null`), *Apply Changes* (same condition) and *Create* (condition `:P3_ID is null`).
413
472
 
473
+ **Lost updates** (APEX: *Prevent Lost Updates* and *Lock Row*). When the form fetches its row, the
474
+ page also gets a checksum of the row's form columns (file columns aside). On *Save* or *Delete*,
475
+ `form_dml` locks the row (`SELECT … FOR UPDATE`), computes the checksum again and refuses the
476
+ change when it differs: *"Someone else changed this record after you opened it. Reload the page to
477
+ see the current values, then make your changes again."* Nothing is written, and the other user's
478
+ change is kept. Columns that are not on the form don't count, and the checksum doesn't depend on
479
+ the user's time zone or date format. The check is on by default; switch it off for one form with
480
+ `{"lost_update": false}` in the region's attributes. The checksum is `meta.row_version(table,
481
+ key_column, key, columns)`, which runs as the application's role (so its grants and row level
482
+ security apply). A process that changes the same row *before* `form_dml` in the same submit makes
483
+ the check fail: change the row after `form_dml`, or switch the check off.
484
+
414
485
  The easiest way to get a form is the **Report and form** wizard, which opens the form as a modal
415
486
  dialog from the report, or the **Form** wizard for a form page on its own (see
416
487
  [Create pages from a table](03-builder.md#create-pages-from-a-table-wizards)). The Cards, Calendar,
@@ -746,7 +817,8 @@ region display selector. Regions take part through their own attributes:
746
817
 
747
818
  | Key | Meaning |
748
819
  |---|---|
749
- | `style` | `tabs` (default) or `select` |
820
+ | `style` | `tabs` (default), `select`, or `carousel`: one region at a time with *Previous* and *Next* and a dot per region, without *Show all* (APEX: the Carousel template) |
821
+ | `interval` | A carousel turns to the next region every so many seconds (1 to 120); not while the pointer or the focus is in it, nor for users who prefer reduced motion |
750
822
  | `show_all` | `false` hides the *Show all* tab (default: shown) |
751
823
  | `remember` | `false`: don't remember the chosen tab for the browser session (default: remembered per page) |
752
824
 
@@ -946,7 +1018,7 @@ Attributes (the region's **Settings** in the page designer):
946
1018
  | Key | Meaning |
947
1019
  |---|---|
948
1020
  | `list` | The list's name (upper case) |
949
- | `template` | `links` (default: nested links), `badges` (a row of tiles with the badge as the value), `cards` (a card per entry with its description) or `tabs` (the top-level entries as tabs) |
1021
+ | `template` | `links` (default: nested links), `badges` (a row of tiles with the badge as the value), `cards` (a card per entry with its description), `tabs` (the top-level entries as tabs) or `wizard_progress` (numbered steps: the entry of the current page is the current step, the ones before it are done and link back; APEX: Wizard Progress) |
950
1022
 
951
1023
  The HR example's page 31 (Shortcuts) shows a static list with each template (with a badge from an
952
1024
  item, child entries, a manager-only entry and one of an excluded build option) and a department
@@ -1264,6 +1336,88 @@ first rows and a *Show all* button), `char-counter` (item: a maximum length with
1264
1336
  an event in `pgkiln_plugins.event_log`). HR example part 47 installs them and uses all four on page
1265
1337
  40 (*Plug-ins*).
1266
1338
 
1339
+ ### `search` (search region)
1340
+
1341
+ One search field over several sources, with the results grouped per source (APEX 22.2: the Search
1342
+ region and search configurations). Each source is a **search configuration** (Shared Components →
1343
+ Search configurations):
1344
+
1345
+ | Property | Meaning |
1346
+ |---|---|
1347
+ | Name | The static ID the region names, e.g. `CUSTOMERS` |
1348
+ | Label | The heading of its results (translated like the page's texts) |
1349
+ | Sequence | The order of the groups |
1350
+ | Source type, source | `table`: a table or view (`sales.customers`) · `sql`: a SELECT, `:ITEM` binds allowed · `list`: the entries of a [list](#list-lists) the user may see (their label and description are searched, they link where the entry links) |
1351
+ | Searchable columns | The columns the search text is looked for in. Empty: the title, subtitle and description columns |
1352
+ | Title, subtitle, description, badge column | What each result shows. The title is required (table, sql), and results are ordered by it |
1353
+ | Icon | An [icon name](09-reference.md#icons), or `#COLUMN#` for a column that holds icon names |
1354
+ | Link | Where a result goes: `{"page": 3, "items": {"P3_ID": "#ID#"}}` (`#COLUMN#` is the row's value; the link gets a checksum, a modal page opens as a dialog) |
1355
+ | Maximum results | Per configuration, 1–100 (default 10); "Showing the first *n* rows." when there are more |
1356
+
1357
+ **How it searches.** Each word of the search text must be found (case-insensitively, anywhere in
1358
+ the value) in one of the searchable columns, so `clerk mil` finds MILLER the clerk. `%` and `_` are
1359
+ plain characters. The text is a query parameter, never SQL; the queries run as the application's
1360
+ role (its grants and row level security apply), each on its own, so a configuration that fails
1361
+ shows its error and the others still answer. The found words are marked in the results.
1362
+
1363
+ ```sql
1364
+ -- a configuration on a query: employees with their department
1365
+ select e.empno, initcap(e.ename) as name, initcap(e.job) as job, d.dname as department
1366
+ from hr.emp e left join hr.dept d on d.deptno = e.deptno
1367
+ ```
1368
+
1369
+ with searchable columns `name, job, department`, title `name`, subtitle `job`, badge `department`
1370
+ and link `{"page": 3, "items": {"P3_EMPNO": "#EMPNO#"}}`.
1371
+
1372
+ **The region**: attributes
1373
+
1374
+ | Key | Meaning |
1375
+ |---|---|
1376
+ | `configs` | The configurations it searches, e.g. `["CUSTOMERS", "ORDERS"]`; empty or left out: all of the application's |
1377
+ | `item` | Search with this page item's value (`"P1_SEARCH"`) instead of a field of the region's own; refresh the region from a dynamic action, or let the item submit the page |
1378
+ | `placeholder` | The field's placeholder |
1379
+ | `min_length` | Search only from this many characters (default 1) |
1380
+ | `empty` | The text when nothing is found (default "Nothing found.") |
1381
+
1382
+ The region's own field is a GET form (`?s<region id>=text`, so a search can be bookmarked) and works
1383
+ without JavaScript; with JavaScript it searches while the user types and replaces only the results.
1384
+ The Advisor plans every configuration with the columns it names and reports names a region uses
1385
+ that don't exist. Search configurations are exported with the application. The HR example's page 42
1386
+ searches employees, departments and the shortcuts list.
1387
+
1388
+ ### `url` (URL region)
1389
+
1390
+ Shows another site's page in the region, in a sandboxed `<iframe>` (APEX: URL region).
1391
+
1392
+ **Source**: the URL. Its scheme and host are written out, so an item can never choose which site is
1393
+ shown; `&ITEM.` substitutions are allowed after the host and are URL-encoded:
1394
+
1395
+ ```text
1396
+ https://reports.example.com/dashboard?customer=&P3_CUSTOMER_ID.&lang=&APP_LANGUAGE.
1397
+ ```
1398
+
1399
+ - `https://` only (`http://` for `localhost` during development), no user name or password in the
1400
+ URL. A path that starts with `/` shows a page of this server, such as another pgkiln application
1401
+ (`/a/portal/1`).
1402
+ - The page's Content-Security-Policy gets `frame-src` for exactly the origins of its visible URL
1403
+ regions; other pages frame nothing. The Advisor reports a URL that isn't allowed.
1404
+ - The other site decides whether it may be framed: many sites forbid it (`X-Frame-Options` or
1405
+ `frame-ancestors`), and the browser then shows an empty frame. Under the frame, *Open in a new tab*
1406
+ opens the page itself.
1407
+ - The region's title is the frame's accessible name.
1408
+
1409
+ Attributes:
1410
+
1411
+ | Key | Meaning |
1412
+ |---|---|
1413
+ | `height` | The frame's height in pixels, 100–4000 (default 480); the width is the region's |
1414
+ | `sandbox` | The frame's sandbox flags, from `allow-scripts`, `allow-forms`, `allow-popups`, `allow-same-origin`, `allow-downloads`, `allow-modals` and `allow-popups-to-escape-sandbox`. Default: the first four (most sites need them to work). `false`: no sandbox. Navigating the whole window (`allow-top-navigation`) is never allowed |
1415
+ | `fullscreen` | `true`: the framed page may go full screen (videos, maps) |
1416
+
1417
+ ```json
1418
+ {"height": 640, "sandbox": ["allow-scripts", "allow-same-origin"], "fullscreen": true}
1419
+ ```
1420
+
1267
1421
  ### `static` and `dynamic` content
1268
1422
 
1269
1423
  - **`static`**: `source` is HTML written by the developer, with `&ITEM.` substitutions
@@ -33,6 +33,8 @@ page, optionally inside a region. Name them `P<page>_<NAME>` (uppercase letters,
33
33
  | `combobox` | free text with suggestions from the list of values; several values become tags | **colon-separated** values, e.g. `SQL:Sales` |
34
34
  | `daterange` | two date pickers, *From* and *To* | `from:to`, e.g. `2026-01-01:2026-06-30` |
35
35
  | `qrcode` | display only: the value as a QR code (SVG drawn on the server) | whatever was set |
36
+ | `image` | display only: a picture from a URL in the value, or a BLOB from a query ([Display image](#display-image-and-geocoded-address)) | a URL, or nothing |
37
+ | `address` | a text field with *Check address*: the installation's geocoder offers matching addresses ([Geocoded address](#display-image-and-geocoded-address)) | the address as text |
36
38
  | `plugin` | an [item plug-in](04-pages-and-regions.md#plug-ins-with-their-own-code): a text field its JavaScript enhances; `config`: `{"plugin": "char_counter", "attributes": {"MAX": "140"}}` | text |
37
39
 
38
40
  Use multi-value items in SQL with `string_to_array`:
@@ -70,6 +72,7 @@ Attributes (`config`):
70
72
  | Key | Meaning |
71
73
  |---|---|
72
74
  | `submit_on_change` | `true`: submit the page when the value changes (typical for report filters) |
75
+ | `warn_unsaved` | `false`: a change of this item doesn't count for the page's [warn on unsaved changes](04-pages-and-regions.md#warn-on-unsaved-changes) |
73
76
  | `null_label` | Text of the empty option in select lists (default `- Select -`); `false` removes the empty option |
74
77
  | `cascade_parents` | Comma-separated items this list depends on, see [cascading lists](#cascading-lists-of-values) |
75
78
  | `wide` | `true`: take the full width of the form |
@@ -129,6 +132,50 @@ the Markdown is shown as text, and the result passes through the same allow-list
129
132
 
130
133
  The HR example's page 20 *Reviews* (`examples/hr/hr_20_items.sql`) uses all of them.
131
134
 
135
+ ## Display image and geocoded address
136
+
137
+ **Display image** (`image`, APEX: *Display Image*) shows a picture and can't be submitted. Two
138
+ sources:
139
+
140
+ - **A URL in the item's value** (a computation, a process or a form fetch sets it): an `https://`
141
+ URL, a path on this server (`/a/hr/static/logo.png`) or a `data:image/png;base64,…` URL. Other
142
+ schemes (`http:`, `javascript:`, URLs with a user name) show nothing. The page's
143
+ Content-Security-Policy allows pictures from exactly the `https` origins its image items show
144
+ when it is rendered.
145
+ - **A BLOB from a query**: `{"sql": "select photo, photo_mime from hr.emp where empno = :P43_EMPNO::int"}`.
146
+ The first column of the first row is the picture (`bytea`), the second its MIME type (else it is
147
+ recognised from the bytes: PNG, JPEG, GIF, WebP). The browser loads it from
148
+ `/a/<alias>/<page>/image/<item>`, which runs the query as the application's role with the
149
+ session's item values, after the page's authorization and the item's own; it is never cached,
150
+ sandboxed and at most 20 MB.
151
+
152
+ `config`: `{"alt": "Photo of &P43_ENAME.", "width": 160, "height": 160}` (the alternative text,
153
+ `&ITEM.` substitutions allowed; it defaults to the label).
154
+
155
+ **Geocoded address** (`address`, APEX: *Geocoded Address*) is a text field with a *Check address*
156
+ button. The server asks the installation's geocoder for the typed text and shows up to five
157
+ matching addresses; the one chosen replaces the text, and with
158
+ `{"coordinates_item": "P43_LOCATION"}` its position goes into that item as `latitude,longitude`
159
+ (the format of a `location` item, so a map region can show it). `{"country": "nl"}` (or `"nl,be"`)
160
+ limits the search to those countries. The answers come in the user's language.
161
+
162
+ The geocoder is set per installation with **`GEOCODER_URL`**, any API that speaks Nominatim's
163
+ search (`?q=…&format=jsonv2`): your own [Nominatim](https://nominatim.org), LocationIQ
164
+ (`https://eu1.locationiq.com/v1/search` with `GEOCODER_KEY`) or, within its usage policy (an
165
+ absolute maximum of one request per second, no heavy use), `https://nominatim.openstreetmap.org/search`.
166
+ Each session may ask once a second. Without `GEOCODER_URL` an address item is a plain text item.
167
+
168
+ The HR example's page 43 *Data and settings* (`examples/hr/hr_53_data_settings.sql`) has both.
169
+
170
+ ## Keeping values per user
171
+
172
+ `{"persist": "user"}` on any item (APEX: *Maintain session state: Per User*) keeps its value for
173
+ the signed-in user across sessions: when a page shows the item and the session has no value for it
174
+ yet, the user's last value comes back; a submit of the page (also a plain submit, e.g. *submit on
175
+ change*) saves the value the user could change, in the same transaction as the page's processes.
176
+ The value is a [user preference](06-processing.md#user-preferences) named like the item. Public
177
+ pages and applications without sign-in keep nothing.
178
+
132
179
  ## Lists of values
133
180
 
134
181
  A list of values (LOV) provides the options of select lists, radio groups, checkbox groups,
@@ -167,6 +167,36 @@ end if;
167
167
  All changes of the submit are rolled back, and the page is shown again with the entered
168
168
  values so the user can correct them.
169
169
 
170
+ **Error handling function** (Settings → *Errors and sessions*; APEX: Error Handling Function). A
171
+ function of your own rewrites every error message users see: those raised in PL/pgSQL, the friendly
172
+ constraint messages and the "reference" messages of unexpected errors.
173
+
174
+ ```sql
175
+ create function sales.error_message(e jsonb) returns jsonb language plpgsql as $$
176
+ begin
177
+ if e->>'constraint' = 'order_customer_fk' then
178
+ return jsonb_build_object('message', 'Choose an existing customer.');
179
+ end if;
180
+ if (e->>'internal')::boolean then
181
+ return jsonb_build_object('message', 'Something went wrong. Quote ' || (e->>'reference') || ' to the help desk.');
182
+ end if;
183
+ return null; -- keep the message
184
+ end $$;
185
+ ```
186
+
187
+ It gets `message` (what pgkiln would show), `original_message`, `code` (SQLSTATE), `constraint`,
188
+ `table`, `column`, `detail`, `hint`, `component` (where it happened, e.g. `process "Save order"`),
189
+ `page`, `user`, `internal` (true for an unexpected error) and `reference` (its activity log id), and
190
+ returns `{"message": "…"}` or null. It runs as the application's role, in a transaction of its own,
191
+ with a 2-second limit; when it fails or returns null the message stays as it was. Debug mode shows
192
+ the real error and doesn't call it.
193
+
194
+ ### Ajax callbacks
195
+
196
+ A process with point **ajax** runs when JavaScript calls it by name, `pgkiln.server.process('NAME')`
197
+ (APEX: On Demand processes, `apex.server.process`), and answers JSON; see
198
+ [chapter 7](07-dynamic-actions.md#ajax-callbacks).
199
+
170
200
  ### Debug messages
171
201
 
172
202
  Like APEX's debug, pgkiln can record what each request of an application did and how long every
@@ -182,6 +212,10 @@ application's settings):
182
212
  | 6 trace | Also each region, process, application process and computation with its duration, and the session (page, user, language, time zone) |
183
213
  | 9 everything | Also the posted item values, computed values, skipped processes, hidden regions and other SQL notices |
184
214
 
215
+ A developer can also switch debug on for **their own session** only, from the
216
+ [developer toolbar](03-builder.md#developer-toolbar) on the running application: the application
217
+ stays out of debug for everyone else.
218
+
185
219
  Each recorded request lists its entries with the time since the start of the request and, for
186
220
  timed steps, their duration. Add your own from any application SQL (processes, region sources,
187
221
  conditions, your PL/pgSQL functions), as with `apex_debug.message`:
@@ -240,6 +274,27 @@ a MIME type that doesn't look like one becomes `application/octet-stream`; the r
240
274
  sniffed (`nosniff`), is sandboxed by its Content-Security-Policy and isn't cached. At most 1000
241
275
  files and 100 MB per download.
242
276
 
277
+ #### Data export
278
+
279
+ With `{"export": "<format>"}` the query returns **rows**, and the download is those rows as one
280
+ file (APEX: `APEX_DATA_EXPORT`): `csv`, `json`, `xml`, `html`, `xlsx` (Excel) or `pdf`.
281
+
282
+ ```json
283
+ {"export": "xlsx", "file_name": "employees-&P43_DEPTNO.", "title": "Employees"}
284
+ ```
285
+
286
+ | Key | Meaning |
287
+ |---|---|
288
+ | `export` | The format |
289
+ | `file_name` | The name without extension, `&ITEM.` substitutions allowed (default: the process name) |
290
+ | `title` | The title of an HTML or PDF file |
291
+ | `layout` | A [report layout](16-files.md#report-layouts) for the PDF (paper, margins, logo) |
292
+ | `separator` | CSV: one character instead of `,` |
293
+
294
+ Column names are the headings (`hire_date` → *Hire Date*, translated like report headings in Excel
295
+ and PDF). Excel keeps numbers and dates as numbers and dates; Excel and PDF hold at most 100,000
296
+ rows. From SQL, `meta.data_export` makes the same CSV, JSON, XML and HTML ([below](#data-export-from-sql)).
297
+
243
298
  ### Execution chains
244
299
 
245
300
  A `chain` process runs its **children**: the processes of the page whose `parent_process` is the
@@ -295,6 +350,30 @@ looks up the employee (a `sql` child) and then starts the ONBOARDING workflow (a
295
350
  *Stop onboarding* terminates it; *Year-end check* is a background chain whose jobs the region
296
351
  "My background jobs" lists from `meta.process_jobs`.
297
352
 
353
+ ### Send e-mail
354
+
355
+ A `send_email` process (APEX: *Send E-Mail*) queues a mail through the application's [mail
356
+ service](#sending-e-mail), without SQL:
357
+
358
+ ```json
359
+ {"to": "&P5_EMAIL.", "cc": "&P5_MANAGER_EMAIL.", "subject": "Order &P5_ID. shipped",
360
+ "body": "Dear &P5_NAME., your order is on its way.",
361
+ "body_html": "<p>Dear &P5_NAME.,</p><p>your order is on its way.</p>",
362
+ "attachments": "select content, filename, mime from sales.invoice where order_id = :P5_ID"}
363
+ ```
364
+
365
+ or with an [e-mail template](#e-mail-templates):
366
+
367
+ ```json
368
+ {"to": "&P5_EMAIL.", "template": "ORDER_SHIPPED", "placeholders": {"NAME": "&P5_NAME.", "ORDER_ID": "&P5_ID."}}
369
+ ```
370
+
371
+ `to`, `cc`, `bcc` (several addresses separated by commas), `from`, `reply_to`, `subject` and `body`
372
+ take `&ITEM.` substitutions as they are; in `body_html` the item values are HTML-escaped.
373
+ `attachments` is a SELECT (binds allowed) with the columns `content` (bytea), `filename` and `mime`,
374
+ one attachment per row. The mail is queued in the page's transaction: when a later process fails,
375
+ nothing is sent. It can run in a background chain too.
376
+
298
377
  ### Send push notification
299
378
 
300
379
  A `send_push` process (APEX: *Send Push Notification*) sends a [push
@@ -514,6 +593,61 @@ select (select empno from hr.emp where lower(username) = lower(:APP_USER)) as ai
514
593
 
515
594
  `&AI_ENAME.` then greets the user on the dashboard.
516
595
 
596
+ ## Application settings
597
+
598
+ **Shared Components → Application settings** (APEX: *Application Settings*, `APEX_APP_SETTING`)
599
+ are named values an administrator changes after deployment: a support address, a limit, a switch.
600
+ Each has a name (`SUPPORT_EMAIL`), a value, a description, optionally a list of **valid values**,
601
+ and *Value required*. Application code reads and changes them:
602
+
603
+ ```sql
604
+ select meta.get_app_setting('SUPPORT_EMAIL');
605
+ select meta.set_app_setting('LEAVE_APPROVAL', 'hr'); -- one of its valid values
606
+ ```
607
+
608
+ Both work for the current application only, called as its own database role (application code),
609
+ and raise an error for an unknown name, a value outside the valid values or an empty required one.
610
+ Settings are exported with the application, values included. An import over an installed
611
+ application (CLI, MCP, a working copy merge) keeps the **installed** value of every setting marked
612
+ *Keep the installed value on upgrade* (the default); with the mark off, the file's value wins. When
613
+ the application's build status is *Run only*, its existing settings can still be edited.
614
+
615
+ ## User preferences
616
+
617
+ Values kept per user and application across sessions (APEX: `APEX_UTIL.GET_PREFERENCE`,
618
+ `SET_PREFERENCE`, `REMOVE_PREFERENCE`):
619
+
620
+ ```sql
621
+ select meta.set_preference('report.rows', '50'); -- the signed-in user
622
+ select meta.get_preference('report.rows'); -- null when there is none
623
+ select meta.remove_preference('report.rows');
624
+ select meta.get_preference('report.rows', 'blake'); -- another user of the application
625
+ ```
626
+
627
+ Names are letters, digits, `_`, `.` and `-` (at most 100); values at most 32,000 bytes; at most 500
628
+ preferences per user. Setting one needs a signed-in user. Items with `{"persist": "user"}` keep their
629
+ values this way ([Keeping values per user](05-items.md#keeping-values-per-user)). Preferences are data of
630
+ the installation: not exported, kept by an import over the application, deleted with it.
631
+
632
+ ## Data export from SQL
633
+
634
+ `meta.data_export(query, format, options)` (APEX: `APEX_DATA_EXPORT.EXPORT`) returns a query's
635
+ rows as a file (`bytea`): `csv`, `json` (an array of objects), `xml` (`query_to_xml`) or `html` (a
636
+ table). It runs with the **caller's** privileges, so grants and row level security apply as to the
637
+ query itself. Attach the file to a mail, store it, or send it with a download process:
638
+
639
+ ```sql
640
+ select meta.add_mail_attachment(
641
+ meta.send_mail('boss@example.com', 'Staff', 'Attached.'),
642
+ meta.data_export('select empno, ename, sal from hr.emp order by ename', 'csv', '{"separator": ";"}'),
643
+ 'staff.csv', 'text/csv');
644
+ ```
645
+
646
+ Options: `separator` (CSV, one character), `header` (`false`: no heading line), `bom` (`false`: no
647
+ UTF-8 byte order mark, which Excel needs to read UTF-8), `title` (HTML). CSV fields are quoted when
648
+ needed, and text that a spreadsheet would run as a formula (`=`, `+`, `-`, `@`) gets a leading
649
+ apostrophe, as in report downloads. Excel and PDF come from the [download process](#data-export).
650
+
517
651
  ## Automations
518
652
 
519
653
  **Shared Components → Automations** run SQL or PL/pgSQL on a schedule, like APEX automations:
@@ -604,6 +738,79 @@ creates or replaces the single action (the column itself stays empty).
604
738
 
605
739
  If you prefer the database to schedule work, [pg_cron](15-extensions.md) still works next to this.
606
740
 
741
+ ## Sending e-mail
742
+
743
+ Like APEX's `APEX_MAIL`, applications send e-mail from SQL, from a *Send e-mail* process and from
744
+ e-mail templates. Where APEX sends only through an SMTP server with a user name and password,
745
+ pgkiln sends through any of these **mail services** (Workspace utilities → [Mail
746
+ services](03-builder.md#mail-services), administrators):
747
+
748
+ | Provider | How pgkiln hands mail over | Credentials |
749
+ |---|---|---|
750
+ | SMTP | Any SMTP server or relay, STARTTLS or TLS | User name and password, or **OAuth2** (SASL XOAUTH2 with a token from the client credentials grant: Microsoft Entra for Exchange Online, or another identity provider's token URL) |
751
+ | Microsoft 365 (Graph) | Microsoft Graph `sendMail` as the From mailbox | An Entra application (tenant, client ID, client secret) with the application permission **Mail.Send**; restrict it to the sending mailboxes with an application access policy |
752
+ | Amazon SES | SES API v2, the raw MIME message, Signature Version 4 | Region, access key ID and secret access key (an IAM user allowed `ses:SendEmail`) |
753
+ | Resend, Postmark, SendGrid, Mailgun | Their HTTP APIs | An API key (Postmark: a server token; Mailgun: also the sending domain and region) |
754
+ | Log only | Nothing is sent: the server's log shows each mail | – (development and demos) |
755
+
756
+ Microsoft retires basic authentication (a password) for SMTP in Exchange Online: use SMTP with OAuth2
757
+ or the Graph provider. Secrets are stored encrypted with `PGKILN_SECRET_KEY`, are write-only in the
758
+ builder, never exported and never readable by applications. Each service lists the **From
759
+ addresses** it may send as (an address, or `@example.com` for a whole domain); an administrator
760
+ chooses each application's service, its From address and an optional **limit per day**.
761
+
762
+ **How mail is sent.** `meta.send_mail` and the others only *queue* the mail, in the caller's
763
+ transaction: a transaction that rolls back sends nothing. After the commit, the server sends it at
764
+ once (`NOTIFY`), or on the scheduler's next pass. A failure that may pass (a network problem, HTTP
765
+ 429 or 5xx, an SMTP 4xx answer) is tried again after 1, 4, 16 and 64 minutes; the fifth failure, or a
766
+ refusal (an unknown recipient, a sender the provider doesn't accept), marks the mail *error*.
767
+ Applications → Activity → **Mail** shows each mail with its outcome, and sends a failed one again or
768
+ cancels a queued one. Mail is kept 30 days.
769
+
770
+ ### From SQL
771
+
772
+ ```sql
773
+ -- APEX_MAIL.SEND
774
+ select meta.send_mail(
775
+ p_to => 'jane@example.com, john@example.com',
776
+ p_subject => 'Your leave was approved',
777
+ p_body => 'Enjoy your holiday!', -- plain text
778
+ p_body_html => '<p>Enjoy your <b>holiday</b>!</p>', -- HTML (optional)
779
+ p_cc => null, p_bcc => null, p_reply_to => 'hr@example.com',
780
+ p_from => null); -- null: the application's From address
781
+
782
+ -- APEX_MAIL.ADD_ATTACHMENT, in the same transaction
783
+ with m as (select meta.send_mail('jane@example.com', 'Invoice 42', 'Attached.') as id)
784
+ select meta.add_mail_attachment(m.id, i.pdf, 'invoice-42.pdf', 'application/pdf')
785
+ from m, sales.invoice i where i.id = 42;
786
+ ```
787
+
788
+ See [chapter 9](09-reference.md#e-mail-from-sql) for every function and its limits. The functions
789
+ work for the **current application** only when the caller runs as that application's database role
790
+ (pages, processes, automations, workflows and background chains do); the SQL Workshop is not an
791
+ application.
792
+
793
+ ### E-mail templates
794
+
795
+ Shared Components → **E-mail templates** (APEX: Email Templates): a name, a subject, an HTML body
796
+ and a plain text body with `#PLACEHOLDERS#`. In the HTML body `#NAME#` is the value HTML-escaped and
797
+ `#NAME!RAW#` the value as it is (trusted HTML only); in the subject and the plain text the value is
798
+ as it is. Names are case-insensitive; a placeholder without a value stays as written.
799
+
800
+ ```sql
801
+ select meta.send_mail_template('LEAVE_DECIDED',
802
+ jsonb_build_object('NAME', 'Jane', 'STATUS', 'approved', 'START', '1 Aug 2026'),
803
+ 'jane@example.com');
804
+ ```
805
+
806
+ Templates are exported with the application; mail services, the application's service and the
807
+ mail log belong to the installation and are not.
808
+
809
+ **HR example.** The template `LEAVE_DECIDED` tells an employee that a leave request was approved or
810
+ rejected: a trigger on `hr.leave_request` queues it when the status changes. The example's mail
811
+ service `DEVELOPMENT_LOG` is of provider *log only*, so nothing leaves the server until an
812
+ administrator gives HR a real service.
813
+
607
814
  ## Approvals and the task list
608
815
 
609
816
  Approvals (APEX: Task Definitions, the Approvals component and the Unified Task List) let an
@@ -676,8 +883,9 @@ open tasks so they don't linger: `select meta.close_tasks('EXPENSE_APPROVAL', :P
676
883
  | `meta.task_events` | View: the history and comments of those tasks |
677
884
 
678
885
  The task tables themselves are closed to the application's role: rights are checked by these
679
- functions and views, for every call. pgkiln sends no e-mail; put a task list on the home page, or
680
- use an automation to remind owners of overdue tasks.
886
+ functions and views, for every call. To tell owners by e-mail, call
887
+ [`meta.send_mail`](#sending-e-mail) from the task definition's SQL or from an automation that
888
+ reminds owners of overdue tasks; or put a task list on the home page.
681
889
 
682
890
  **Example:** in the HR sample application, a leave request creates a `LEAVE_APPROVAL` task for the
683
891
  employee's manager; *My tasks* (page 14) approves or rejects it through `hr.decide_leave`, the same
@@ -10,8 +10,8 @@ your own from a static application file.
10
10
 
11
11
  | Property | Meaning |
12
12
  |---|---|
13
- | `event` | `change` (an item's value changed), `click` (a button was clicked), `load` (the page loaded) or `dialog_closed` (a modal dialog opened from the page was submitted and closed) |
14
- | `trigger_element` | For `change`: one or more item names, comma separated. For `click`: a button name. For `dialog_closed`: the dialog page numbers, comma separated (empty: any dialog) |
13
+ | `event` | When it runs, see [Events](#events) |
14
+ | `trigger_element` | What it listens to: item names (`change`, `focus`, `blur`, `key_release`), a button name (`click`), region ids or static ids (`after_refresh`, `selection_change`), dialog page numbers (`dialog_closed`; empty: any dialog) or event names (`custom`), comma separated. Empty for `load` and `before_submit` |
15
15
  | `condition_type`, `condition_value` | Optional client-side condition on the (first) trigger item's value (for `dialog_closed`: on the dialog's page number): `equals`, `not_equals`, `in_list` (comma-separated values), `is_null`, `is_not_null` |
16
16
  | `action` | What to do, see below |
17
17
  | `affected_items`, `affected_region_id` | What it acts on |
@@ -23,6 +23,31 @@ your own from a static application file.
23
23
  | `authz` | Only for authorized users |
24
24
  | `build_option` | Only while the [build option](06-processing.md#build-options) is included |
25
25
 
26
+ ## Events
27
+
28
+ | Event | Runs when | Trigger |
29
+ |---|---|---|
30
+ | `change` | An item's value changed | Item names |
31
+ | `click` | A button was clicked | A button name |
32
+ | `load` | The page has loaded | – |
33
+ | `dialog_closed` | A modal dialog opened from the page was submitted and closed ([below](#dialog-closed)) | Dialog page numbers (empty: any) |
34
+ | `focus` / `blur` | An item gets / loses the focus (APEX: Get Focus, Lose Focus) | Item names |
35
+ | `key_release` | A key is released in an item | Item names |
36
+ | `after_refresh` | A region was refreshed (`refresh_region`), loaded lazily or shown for another master row | Region ids or static ids |
37
+ | `selection_change` | Rows are selected in a report with [row selection](04-pages-and-regions.md#row-selection), or another master row in a grid; the condition sees the selected values, colon separated | Region ids or static ids |
38
+ | `before_submit` | The page is about to be submitted; a declined `confirm` keeps the page | – |
39
+ | `custom` | JavaScript raised the event: `pgkiln.trigger('ORDER_SAVED', value)` (APEX: custom events, `apex.event.trigger`) | Event names (case-insensitive) |
40
+
41
+ **The actions of one event and trigger run one after the other, in sequence order** (APEX: the true
42
+ actions of a dynamic action), each waiting for the one before (a server action for its answer). A
43
+ `confirm` that is declined stops the actions after it, and for `before_submit` the submit too:
44
+
45
+ | Sequence | Event | Action | Message / items |
46
+ |---|---|---|---|
47
+ | 10 | `click` on `DELETE_ALL` | `confirm` | *Delete every selected row?* |
48
+ | 20 | `click` on `DELETE_ALL` | `execute_sql` | `delete from sales.order_line where id = any(string_to_array(:P5_SELECTED, ':')::int[])` |
49
+ | 30 | `click` on `DELETE_ALL` | `refresh_region` | the order lines |
50
+
26
51
  ## Actions
27
52
 
28
53
  | Action | Runs in | Effect |
@@ -45,11 +70,44 @@ your own from a static application file.
45
70
  | `plugin` | browser | Runs the [dynamic action plug-in](04-pages-and-regions.md#plug-ins-with-their-own-code) named in `code`, with `config` `{"attributes": {…}}` (`&ITEM.` filled in); the function gets the same context as *Execute JavaScript* plus `attributes` |
46
71
  | `execute_javascript` | browser | Calls the function named in `code`, which a [static application file](03-builder.md#static-application-files) registered; see [below](#execute-javascript) |
47
72
  | `push_subscribe` | browser | Turns on [push notifications](17-mobile.md#push-notifications) on the user's device: the browser asks for permission, then the device is registered. Use it with `click` (browsers ask only after one). `message` is shown when it worked, the browser's reason when not. The app needs push notifications on |
73
+ | `confirm` | browser | Asks `message`; **No stops the actions after it** (and the submit, for `before_submit`) |
74
+ | `close_dialog` | browser | In a modal page: closes it; the page below refreshes, or runs its `dialog_closed` actions (APEX: Close Dialog) |
75
+ | `cancel_dialog` | browser | In a modal page: closes it without refreshing the page below |
76
+ | `clear` | browser | Empties the affected items |
77
+ | `open_region` / `close_region` | browser | Opens or closes a *collapsible* region; another region is shown or hidden |
78
+ | `download` | server | Downloads the file of the [download process](06-processing.md#download) named in `code` (a process of the page, with its authorization and condition), after sending `items_to_submit`; the page stays as it is |
79
+ | `print` | browser | The browser's print dialog |
80
+ | `current_position` | browser | The device's position as `latitude,longitude` (5 decimals) into the first affected item |
81
+ | `share` | browser | The device's share sheet with the page's title, `message` and address; where there is none, the address is copied |
48
82
 
49
83
  Server-side actions first store `items_to_submit` in session state, run as the application's
50
84
  database role like everything else, and check the page's and the dynamic action's authorization.
51
85
  The SQL itself never reaches the browser.
52
86
 
87
+ ## Ajax callbacks
88
+
89
+ A page process with point **ajax** (APEX: an On Demand process) runs only when JavaScript calls it by
90
+ name, and answers JSON; an application process with point *ajax* can be called from every page (a
91
+ page's own process of that name wins). Only processes of type `sql` answer.
92
+
93
+ ```js
94
+ // in a static application file
95
+ const r = await pgkiln.server.process('ORDER_TOTAL', { items: ['P5_ORDER_ID'], x01: 'EUR' });
96
+ r.data.total; // the first row of the process's last statement
97
+ r.rows; // its rows (at most 1000)
98
+ r.items; // the page and application items it set (also set on the page)
99
+ ```
100
+
101
+ ```sql
102
+ -- the process ORDER_TOTAL (point ajax): :X01 … :X10 are the x01 … x10 sent
103
+ select sum(qty * price) as total, :X01 as currency from sales.order_line where order_id = :P5_ORDER_ID::int
104
+ ```
105
+
106
+ `items` is a list of item names (their current values are sent) or `{"P5_ID": "7"}`; only items the
107
+ user may change are set, as on a submit. The process runs as the application's role with its
108
+ authorization, condition and build option; errors come back as `{ "error": "…" }` (the promise
109
+ rejects). The request carries the page's CSRF token and needs page access.
110
+
53
111
  ## Execute JavaScript
54
112
 
55
113
  APEX's *Execute JavaScript Code* runs code typed into the action. pgkiln's Content-Security-Policy