pgkiln 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/db/migrations/140_item_region_types.sql +27 -0
  2. package/db/migrations/141_workspace_resources.sql +115 -0
  3. package/docs/apex-feature-parity.md +91 -12
  4. package/docs/guide/01-installation.md +1 -0
  5. package/docs/guide/03-builder.md +47 -4
  6. package/docs/guide/04-pages-and-regions.md +41 -1
  7. package/docs/guide/05-items.md +43 -3
  8. package/docs/guide/11-from-apex.md +3 -0
  9. package/docs/guide/12-development.md +25 -3
  10. package/docs/guide/16-files.md +29 -0
  11. package/docs/guide/19-rest-data-sources.md +41 -3
  12. package/examples/hr/hr_75_item_region_types.sql +81 -0
  13. package/examples/hr/hr_76_remote_servers.sql +48 -0
  14. package/package.json +1 -1
  15. package/public/app.css +48 -0
  16. package/public/app.js +377 -1
  17. package/src/ai/service.ts +10 -1
  18. package/src/app.ts +2 -0
  19. package/src/builder/advisor.ts +18 -2
  20. package/src/builder/ai.ts +29 -3
  21. package/src/builder/arrange.ts +7 -0
  22. package/src/builder/components.ts +5 -5
  23. package/src/builder/designer.ts +9 -3
  24. package/src/builder/home.ts +3 -0
  25. package/src/builder/i18n/de.ts +6 -0
  26. package/src/builder/i18n/en.ts +6 -0
  27. package/src/builder/i18n/es.ts +6 -0
  28. package/src/builder/i18n/fr.ts +6 -0
  29. package/src/builder/i18n/it.ts +6 -0
  30. package/src/builder/i18n/ja.ts +6 -0
  31. package/src/builder/i18n/ko.ts +6 -0
  32. package/src/builder/i18n/nl.ts +6 -0
  33. package/src/builder/i18n/pt.ts +6 -0
  34. package/src/builder/i18n/zh-cn.ts +6 -0
  35. package/src/builder/i18n/zh-tw.ts +6 -0
  36. package/src/builder/network-access.ts +33 -0
  37. package/src/builder/region-help.ts +8 -2
  38. package/src/builder/rest-catalogs.ts +22 -14
  39. package/src/builder/routes.ts +5 -1
  40. package/src/builder/static-files.ts +14 -4
  41. package/src/builder/websources.ts +12 -6
  42. package/src/builder/workflows.ts +2 -1
  43. package/src/builder/workspace-resources.ts +528 -0
  44. package/src/i18n/ar.ts +13 -0
  45. package/src/i18n/cs.ts +13 -0
  46. package/src/i18n/da.ts +13 -0
  47. package/src/i18n/de.ts +13 -0
  48. package/src/i18n/el.ts +13 -0
  49. package/src/i18n/es.ts +13 -0
  50. package/src/i18n/fi.ts +13 -0
  51. package/src/i18n/fr.ts +13 -0
  52. package/src/i18n/he.ts +13 -0
  53. package/src/i18n/it.ts +13 -0
  54. package/src/i18n/ja.ts +13 -0
  55. package/src/i18n/ko.ts +13 -0
  56. package/src/i18n/nb.ts +13 -0
  57. package/src/i18n/pl.ts +13 -0
  58. package/src/i18n/pt.ts +13 -0
  59. package/src/i18n/ru.ts +13 -0
  60. package/src/i18n/sv.ts +13 -0
  61. package/src/i18n/tr.ts +13 -0
  62. package/src/i18n/uk.ts +13 -0
  63. package/src/i18n/zh.ts +13 -0
  64. package/src/i18n.ts +26 -0
  65. package/src/metadata.ts +22 -5
  66. package/src/remote-servers.ts +133 -0
  67. package/src/restcatalog.ts +21 -10
  68. package/src/restsync.ts +1 -1
  69. package/src/runtime/context.ts +2 -0
  70. package/src/runtime/items.ts +32 -3
  71. package/src/runtime/page-regions.ts +50 -0
  72. package/src/runtime/region-cache.ts +1 -1
  73. package/src/runtime/regions.ts +11 -0
  74. package/src/runtime/render.ts +3 -17
  75. package/src/runtime/report.ts +55 -3
  76. package/src/runtime/static-files.ts +46 -17
  77. package/src/runtime/template-components.ts +2 -1
  78. package/src/websources.ts +43 -18
  79. package/src/workspace-export.ts +80 -1
@@ -23,6 +23,9 @@ page, optionally inside a region. Name them `P<page>_<NAME>` (uppercase letters,
23
23
  | `switch` | on/off switch | `true` / `false` |
24
24
  | `checkbox_group` | several checkboxes | **colon-separated** return values, e.g. `10:30`; (137) `{"switches": true}` shows each as a switch, one per line |
25
25
  | `multiselect` | multi-select list | **colon-separated** return values |
26
+ | `shuttle` | (140) two lists, *Available* and *Selected*, with buttons to move values across and put the chosen ones in order ([Shuttle](#shuttle-select-one-and-select-many)); a multi-select list without JavaScript | **colon-separated** return values, in the chosen order |
27
+ | `select_one` | (140) a search field over the list of values: typing filters a list under it ([Select One](#shuttle-select-one-and-select-many)); a select list without JavaScript | the chosen return value |
28
+ | `select_many` | (140) as `select_one`, for several values shown as chips with a remove button; a multi-select list without JavaScript | **colon-separated** return values |
26
29
  | `display` | read-only text (shows the display value for list items) | whatever was set |
27
30
  | `hidden` | not rendered | set by URL (with checksum), fetch or processes |
28
31
  | `file` | file upload ([chapter 16](16-files.md)); on phones the camera with `{"capture": "environment"}`, photos made smaller with `{"max_px": 1600}` ([chapter 17](17-mobile.md)) | the id of the uploaded temporary file (a uuid) |
@@ -191,7 +194,8 @@ pages and applications without sign-in keep nothing.
191
194
  ## Lists of values
192
195
 
193
196
  A list of values (LOV) provides the options of select lists, radio groups, checkbox groups,
194
- multi-selects, popup LOVs, grid columns and display items. Three forms:
197
+ multi-selects, shuttles, Select One and Select Many items, popup LOVs, grid columns and display
198
+ items. Three forms:
195
199
 
196
200
  1. **A SELECT returning (display, return)**. Bind variables are allowed:
197
201
  ```sql
@@ -205,8 +209,9 @@ multi-selects, popup LOVs, grid columns and display items. Three forms:
205
209
  list can also read a web service: give it a **REST data source** and query the rows from
206
210
  `rest` ([chapter 19](19-rest-data-sources.md#lists-of-values)).
207
211
 
208
- On submit, the value of a select list, radio group, checkbox group, multi-select or popup LOV (each
209
- value of a multi-value item), and of a grid column with a list of values, must be one its list
212
+ On submit, the value of a select list, radio group, checkbox group, multi-select, shuttle, Select
213
+ One, Select Many or popup LOV (each value of a multi-value item), and of a grid column with a list
214
+ of values, must be one its list
210
215
  returns at that moment, for that user: a list that leaves out departments a user may not pick also
211
216
  keeps a crafted request from choosing them. Otherwise the item gets *"choose a value from the
212
217
  list"*. For a list that may legitimately miss the current value, set `"any_value": true` in the
@@ -241,6 +246,41 @@ new search starts over at the first page. A browser without `IntersectionObserve
241
246
  The HR example's page 26 (*Pick an employee*) has one with name, job and department columns;
242
247
  page 47 (*Component details*) one over 6,000 rows, 100 a page.
243
248
 
249
+ ## Shuttle, Select One and Select Many
250
+
251
+ (sprint 140; APEX: Shuttle, and 24.2's Select One and Select Many) Three list items for choosing from
252
+ a list of values, each a plain select list without JavaScript (the page saves the same values):
253
+
254
+ - **Shuttle** (`shuttle`): two lists side by side, *Available* and *Selected* (stacked on phones),
255
+ with a filter field over the available values. Move values across with the buttons between the
256
+ lists (*Move all* », *Move* ›, *Remove* ‹, *Remove all* «), by double-clicking one, or with the
257
+ keyboard: **Enter** moves the marked values across, **Delete** removes them from *Selected*, and in
258
+ *Selected* **Alt+↑ / Alt+↓** move them up or down (with **Shift**: to the top or bottom; the buttons
259
+ ⤒ ↑ ↓ ⤓ do the same). The value is the chosen return values **in their order**, colon-separated
260
+ (`7566:7902`), so `string_to_array(:P50_MEMBERS, ':') with ordinality` keeps the order. Without
261
+ JavaScript the shuttle is a multi-select list (the chosen values first, in their order); a value
262
+ posted from it comes in the list's order.
263
+ - **Select One** (`select_one`): a search field. Clicking it or typing opens a list box under it
264
+ that shows the list's display values containing what was typed (case-insensitive); **↑ / ↓**
265
+ move through it, **Enter** chooses, **Escape** closes. Emptying the field chooses no value.
266
+ `{"null_label": "- None -"}` names the empty entry, `false` leaves it out (a required item).
267
+ - **Select Many** (`select_many`): the same, for several values: the chosen ones are chips above the
268
+ field, each with a remove button (named *Remove …* for screen readers); **Backspace** in the
269
+ empty field removes the last one, Enter on a chosen value in the list removes it again. The
270
+ value is colon-separated, like a multi-select.
271
+
272
+ Like the other list items they are checked against their list of values on submit, take
273
+ `cascade_parents` (a cascading shuttle is reloaded with its new values), show their display values
274
+ (comma-separated, in order) when read-only, and dynamic actions see the colon-separated value
275
+ (*Set value* with `A:B` chooses those values). The list box searches the rows the page read
276
+ (`max_rows`, 5,000 by default): for lists of tens of thousands of rows use a [Popup LOV](#popup-lov),
277
+ which searches on the server. Texts such as *Move all*, *Selected* and *Remove {value}* are runtime
278
+ messages (`shuttle.*`, `lov.search`, `item.tag_remove`) an application can replace with a text
279
+ message of the same name.
280
+
281
+ HR page 50 (*Project team*, Administration) has a Select One of departments, a shuttle of that
282
+ department's employees (cascading) and a Select Many of skills.
283
+
244
284
  ## Cascading lists of values
245
285
 
246
286
  A list that depends on another item, for example managers filtered by department:
@@ -59,6 +59,9 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
59
59
  | `APEX_DATA_EXPORT.EXPORT` | `meta.data_export(query, format, options)` (CSV, JSON, XML, HTML) and the download process's `{"export": "xlsx"}` (also PDF) ([chapter 6](06-processing.md#data-export-from-sql)) |
60
60
  | `APEX_LANG.LANG`, dynamic translations | Shared Components → Dynamic translations and `meta.lang(text)` ([chapter 14](14-globalization.md#dynamic-translations)) |
61
61
  | Display Image, Geocoded Address items | Item types `image` and `address` (geocoder: `GEOCODER_URL`) ([chapter 5](05-items.md#display-image-and-geocoded-address)) |
62
+ | Shuttle, Select One, Select Many (24.2) items | Item types `shuttle` (ordered, colon-separated), `select_one` and `select_many` (a search field over the list of values; chips): plain selects without JavaScript ([chapter 5](05-items.md#shuttle-select-one-and-select-many)) |
63
+ | Classic Report region (report templates, pagination types) | Region type `classic_report`: `report_template` `standard`, `value_pairs`, `badges` or `media_list`; `pagination` `total`, `range` or `none` ([chapter 4](04-pages-and-regions.md#classic_report-classic-report)) |
64
+ | Breadcrumb region (Breadcrumb Bar), Help Text region and page help | Region types `breadcrumb` (from each page's *Breadcrumb parent*; no separate breadcrumb shared component) and `help_text`; the page's *Help text* (Markdown) ([chapter 4](04-pages-and-regions.md#breadcrumb-and-help_text)) |
62
65
  | Translated applications (XLIFF), `APEX_LANG.MESSAGE`, `&APP_TEXT$NAME.` | Translations in the app (XLIFF/CSV import and export), `meta.message()`, `&APP_TEXT$NAME.` ([chapter 14](14-globalization.md)) |
63
66
  | Application date format mask | Settings → Globalization → Date format (Oracle-style masks) |
64
67
  | Number format masks (`FML999G999G990D00`) on columns and items | `{"formats": {...}}` on report, grid and cards columns, `{"format_mask": "..."}` on charts and number/display items ([chapter 14](14-globalization.md#number-formats)) |
@@ -37,7 +37,8 @@ src/
37
37
  restcatalog.ts (095) REST source catalogs: checks, fromOpenApi (OpenAPI 3 / Swagger 2 → services: parameters with "key",
38
38
  rows and columns from the response schema, write-back from collection and item paths, notes), catalog
39
39
  files, load/insert/replace, sourceFromService
40
- workspace-export.ts (sprint 67) a workspace's components as a file (pgkiln-workspace/1) and importWorkspace (one transaction)
40
+ workspace-export.ts (sprint 67) a workspace's components as a file (pgkiln-workspace/1) and importWorkspace (one transaction);
41
+ (sprint 141) workspace_files, remote_servers (an existing server keeps its base URL), web_credentials (no secrets)
41
42
  team.ts (sprint 113) Team Development's data: workspaceDevelopers (members and administrators: assignees, @mentions),
42
43
  issueHtml (markdownHtml + mentions), recordMentions, logIssue (history), teamSetup, teamCounts, pageIssues,
43
44
  exportTeam / importTeam (the workspace export's "team" part)
@@ -134,6 +135,10 @@ src/
134
135
  network-acl.ts (sprint 118, WS-9) workspace network access lists (meta.workspace.network_*): scopeForApp/scopeForWorkspace
135
136
  (no cache), parseHostList, recordRefusal/purgeRefusals (meta.network_refusal, 7 days), mailHostAddress
136
137
  (a workspace mail service's SMTP host: both lists, resolved once, the checked address connected to)
138
+ remote-servers.ts (sprint 141, AP-7) remote servers (meta.remote_server): serverProblems, serverUrl (PGKILN_SERVER_* env var
139
+ wins over base_url), joinUrl, resolveSource (a REST source on a server: full URL + the server's default
140
+ credential; used by loadSource/fetchSource/callOperation), catalog and AI service URLs (owner pool,
141
+ the application's own workspace only)
137
142
  datapump.ts (sprint 103, DA-16) Schema Export / Import jobs (meta.dump_job): findTools (pg_dump/pg_restore ≥ the
138
143
  server's major), startExport (pg_dump, argument array, PGPASSWORD), readToc/parseToc (pg_restore --list),
139
144
  startImport (pg_restore -f - run in one transaction between schema renames; data-only in FK order),
@@ -220,7 +225,12 @@ src/
220
225
  processes.ts download (file or zip from a query, safe headers), workflow processes, configuration checks of chains
221
226
  logic.ts computations (the page's and (102) the application's), branches (page, URL, function returning a URL, another application) and their conditions
222
227
  shortcuts.ts (103) "NAME" shortcuts in static content, help texts and confirm messages
228
+ static-files.ts (068) static application files (/a/<alias>/static/<name>), the <script>/<link> tags of a page; (141) static
229
+ workspace files (/a/<alias>/workspace-files/<name>, the application's own workspace's only, owner pool);
230
+ #APP_FILES# / #WORKSPACE_FILES# are substituted in context.ts substitute and template-components.ts
223
231
  render.ts page chrome (nav, breadcrumb), dynamic action JSON, theme (the page's nonce'd <style>, light/dark and style switches)
232
+ page-regions.ts (sprint 140) breadcrumb and help_text regions: breadcrumbLinks (also the title bar's trail, left out when the
233
+ page has a breadcrumb region), the page's help text (meta.page.help) as Markdown with escaped &ITEM. values
224
234
  styles.ts base styles (BASE_STYLES: Iris, Lavender, Standard; baseStyleOf → html data-style), Theme Roller style variants: fixed lists (fonts, sizes, corners), parseStyle/appStyles checks, the request's
225
235
  style (user choice, default), themeCss() (only hex values and constants reach the CSS); (sprint 117) NAV_APPEARANCES
226
236
  (Pillar Light / Dark), navAppearance() → the <body> classes nav-pillar-*, the header strip (--strip)
@@ -228,7 +238,10 @@ src/
228
238
  regions.ts region shell + chart (drill-down links, gauge settings)/cards/dynamic dispatch with row limits, lazy placeholder and cache, buttons (menu buttons, badges; (124) menu entries {action} → data-menu-entry for menu_entry dynamic actions)
229
239
  report.ts, report-views.ts (group by, pivot, chart), compute.ts (computed column expressions), grid.ts (aggregates, row actions, Actions menu, single row view, layoutFromForm), grid-layout.ts (column layouts: clean, arrange, per user), master-detail.ts (signed master row selection, details), facets.ts (facetChart: a facet's counts as a chart, sprint 65), items.ts
230
240
  (report.ts: paging with row ranges and max_rows, keyset paging (keysetPlan, seekCondition, signed r<id>_k), pagerNav, streamed CSV/Excel downloads with a cursor;
231
- items.ts: lovOptions, searchLov/lovLookup for popup LOVs, served by POST /a/:alias/:page/lov/:item/search in routes.ts)
241
+ (140) classic_report through renderReport: reportState drops the interactive settings, classicTemplate (value_pairs, badges, media_list),
242
+ pagination total/range/none;
243
+ items.ts: lovOptions, searchLov/lovLookup for popup LOVs, served by POST /a/:alias/:page/lov/:item/search in routes.ts;
244
+ LOV_TYPES / LOV_CHECKED / MULTI_VALUE: the list item types (shuttle, select_one, select_many since 140) checked on submit and colon-separated)
232
245
  region-cache.ts region caching (keys per scope, CSRF placeholder, invalidation on submit) and lazy regions (GET …/region/:id is in routes.ts)
233
246
  gantt-move.ts (sprint 69) Gantt drag and drop: ganttEditable, targetTime, moveGanttTask (the task in the region's
234
247
  query, gantt_move with :TASK_ID/:NEW_START/:NEW_END); the dragging itself is in app.js
@@ -375,6 +388,10 @@ src/
375
388
  list (workspaceAdmin), Check a URL (nothing called), the workspaces (administrators), refused calls
376
389
  workspace-mail.ts (sprint 118, WS-9) a workspace's own mail service (/builder/workspaces/:id/mail, WORKSPACE_<id>): its
377
390
  administrators only, write-only secret, checks through mail.ts runCheck with the workspace's guard
391
+ workspace-resources.ts (sprint 141, AP-7) Workspace utilities → Static workspace files (/builder/workspace-files), Remote servers
392
+ (/builder/remote-servers: URL now, network verdict, used by; renaming moves sources/catalogs) and Web
393
+ credentials (/builder/workspace-credentials: write-only secrets): the current workspace, its developers see,
394
+ its administrators change
378
395
  shared.ts Shared Components and access control
379
396
  designer.ts page designer: component tree (with computations and branches), layout canvas and gallery, property editor, toolbar;
380
397
  (sprint 86) GET /builder/apps/:app/pages/:no, /builder/pages/:id redirects browsers there (designerUrl in ui.ts)
@@ -519,7 +536,8 @@ public/
519
536
  (sprint 117) navigation tree states, RTL via logical properties, body.nav-pillar-*, u-color-*-subtle, the busy bar)
520
537
  app.js client runtime: dialogs (dialog_closed actions), popup LOVs (sprint 115: the next page loads on scroll through an
521
538
  IntersectionObserver, ‹/More without one), dynamic actions (focus, classes, messages; sprint 115: wait_popup /
522
- remove_wait_popup as a modal <dialog class="wait-popup">, pgkiln.waitPopup(), the menu_entry event), grids (add/duplicate rows, master-detail refresh, move/resize columns, copy/paste of cell ranges), menus, lazy regions, maps (Leaflet layers, marker clusters, heat layer, layer legend, area/distance filter) (no inline JS)
539
+ remove_wait_popup as a modal <dialog class="wait-popup">, pgkiln.waitPopup(), the menu_entry event), (sprint 140) shuttle()
540
+ and selectPick() (Select One / Many: combobox + listbox, chips) over the item's hidden <select>, grids (add/duplicate rows, master-detail refresh, move/resize columns, copy/paste of cell ranges), menus, lazy regions, maps (Leaflet layers, marker clusters, heat layer, layer legend, area/distance filter) (no inline JS)
523
541
  code-editor.js, .css builder code editor: enhances <textarea data-code>, highlighting, suggestions (no dependencies);
524
542
  (sprint 108) the current line, and read-only <pre data-code-view="sql|plpgsql|json|html"> highlighted;
525
543
  its colours are the --ce-* tokens in builder.css
@@ -545,6 +563,7 @@ test/
545
563
  globalization.test.ts masks on HR page 29, time zones, the de/fr/es texts
546
564
  files.test.ts file items: storage, limits, downloads, temporary files
547
565
  items.test.ts rich text, Markdown, rating, combobox, date range, password reveal and QR code items
566
+ item-region-types.test.ts (sprint 140) shuttle, select one/many, classic report, breadcrumb and help text: gallery, runtime, export, Advisor, HR page 50
548
567
  dataload.test.ts parsing, Load Data, the data_load process
549
568
  sampledata.test.ts Sample Data: CHECK parsing, proposals, option errors, seeds and streams, preview/insert/rollback, parents first, downloads, saved generators
550
569
  unload.test.ts Unload Data: CSV/JSON/XLSX/XML output, read back with Load Data, read-only and one-statement checks, streaming
@@ -622,6 +641,8 @@ test/
622
641
  blueprint-from-app.test.ts a blueprint of HR: checks clean, pages and menu, notes, a working copy with sample rows, workspaces
623
642
  rest-catalogs.test.ts OpenAPI/Swagger reading, parameter keys, catalog files, the builder's pages, a data source from a service
624
643
  workspace-transfer.test.ts a workspace's export, import into another workspace (added, kept, replaced, all or nothing), groups, access
644
+ workspace-resources.test.ts (sprint 141) remote servers (checks, env var, HR's CRM moves its sources, credentials, renames, catalogs,
645
+ AI services, the Advisor), static workspace files styling two applications, workspace credentials, the export
625
646
  tree-charts.test.ts treemap and sunburst (the hierarchy, cycles, squarify), timeline (lanes, rows, points and periods)
626
647
  gantt-move.test.ts an editable Gantt's markup, the move route, dates, who may drag, the builder's settings
627
648
  find-workspace.test.ts developers' addresses, builder mail, the workspaces mail, the same answer, limits
@@ -641,6 +662,7 @@ test/
641
662
  e2e/worksheet.test.ts (sprint 91) the SQL Worksheet in a browser: run keys, tabs, navigator, drag and drop, splitters, phone, no JavaScript;
642
663
  (107) transaction mode: badge, reload, Rollback, closing a tab, Autocommit switch, beforeunload
643
664
  e2e/items.test.ts sprint 26 item types in a browser: editors, tags, stars, dates, reveal; without JavaScript
665
+ e2e/item-region-types.test.ts (sprint 140) the shuttle (buttons, keyboard, order) and Select One / Many in a browser; without JavaScript; 390 px
644
666
  e2e/designer.test.ts page designer: panes per width, drag and drop, keyboard, Arrange buttons, builder theme
645
667
  e2e/calendar.test.ts calendar drag and drop and create on click, view switching, chart drill-down
646
668
  e2e/gantt.test.ts Gantt drag and drop: moving a task, its end, the keyboard, a click still drills down, the phone
@@ -21,6 +21,8 @@ This chapter covers three things that deal with files:
21
21
  | Interactive report → Download → CSV / Excel / PDF | Actions → **Download CSV / Excel / PDF** |
22
22
  | Shared Components → Report Layouts | Shared Components → **Report layouts** |
23
23
  | Print (browser) | Actions → **Print**, and a print stylesheet on every page |
24
+ | Static Application Files (`#APP_FILES#`) | Shared Components → **Static application files** ([chapter 3](03-builder.md#static-application-files)) |
25
+ | Static Workspace Files (`#WORKSPACE_FILES#`) | Workspace utilities → **Static workspace files** ([below](#static-workspace-files)) |
24
26
 
25
27
  ## File upload items
26
28
 
@@ -206,6 +208,33 @@ address or the credential. Downloads are limited to `MAX_UPLOAD_MB`.
206
208
  Large files in bytea columns are fine up to tens of megabytes. For bigger files or large numbers
207
209
  of them, store them in object storage and keep the key in the table.
208
210
 
211
+ ## Static workspace files
212
+
213
+ Files a developer ships with the applications, rather than files users upload: CSS, JavaScript, JSON, images, fonts
214
+ and PDFs. An application has its own (Shared Components → **Static application files**,
215
+ [chapter 3](03-builder.md#static-application-files)); since sprint 141 a **workspace** has them too (Workspace
216
+ utilities → **Static workspace files**; APEX: Static Workspace Files), shared by every application of the workspace,
217
+ for example one stylesheet with the company's colours.
218
+
219
+ | Write | Means |
220
+ |---|---|
221
+ | `#WORKSPACE_FILES#brand.css` in *Every page loads* (or a page's *JavaScript and CSS files*) | Every page (that page) loads the workspace's `brand.css` with `<link>` (a `.js` file with `<script src defer>`), after pgkiln's own, in list order with the application's own files |
222
+ | `#WORKSPACE_FILES#logo.png` in HTML, a template, a list entry, a link | `/a/<alias>/workspace-files/logo.png` |
223
+ | `#APP_FILES#guide.pdf` | `/a/<alias>/static/guide.pdf`, the application's own file |
224
+
225
+ Each application serves the workspace's files from its own address, `/a/<alias>/workspace-files/<name>`, to anyone
226
+ (don't put secrets in them): only its own workspace's files, never another workspace's. Pages link to
227
+ `…?v=<version>`, which browsers keep for a year; a change gives a new version.
228
+
229
+ The rules are those of static application files: names of letters, digits, `_`, `-` and `.` with an extension from a
230
+ fixed list; **no HTML** (the database refuses it too); up to 5 MB a file; `X-Content-Type-Options: nosniff`; an SVG or
231
+ PDF opened on its own is sandboxed, so a script in it never runs. The workspace's administrators upload, write, rename
232
+ and delete them; its developers see the list (with the applications and pages that load each file) and open a file in
233
+ a sandboxed preview. They travel with the [workspace export](03-builder.md#export-and-import), not with an
234
+ application's.
235
+
236
+ HR example (part 76): `brand.css` is a workspace file; HR loads it as `#WORKSPACE_FILES#brand.css`.
237
+
209
238
  ## Data loading
210
239
 
211
240
  Both ways of loading accept:
@@ -84,6 +84,12 @@ Shared Components → **Web credentials** → + Add.
84
84
  | OAuth2 password, Refresh token | `oauth2`: the password of the OAuth2 user (`password` grant), or a refresh token obtained elsewhere (`refresh_token` grant). Write-only like the secret, each with its own **Remove** button |
85
85
  | Valid for URLs | URL prefixes the credential may be sent to, e.g. `https://api.example.com/v2/`. Recommended: a source or process that points elsewhere fails instead of sending the secret |
86
86
 
87
+ **Workspace web credentials** (sprint 141): Workspace utilities → **Web credentials** holds credentials with the same
88
+ properties for every application of the workspace ([chapter 3](03-builder.md#static-workspace-files-remote-servers-and-web-credentials)).
89
+ Wherever a credential is named (a source, the `invoke_api` process and step, `meta.web_request`, a remote server's
90
+ default credential, object storage), the application's own credential of that name is used, else the workspace's.
91
+ Another workspace's credentials are never found.
92
+
87
93
  For `oauth2`, pgkiln asks the token endpoint for an access token, keeps it until 30 seconds before
88
94
  it expires, renews it with the refresh token when the endpoint gave one, and asks for a new one when
89
95
  the service answers 401. Access tokens are kept in the server's memory only.
@@ -118,9 +124,10 @@ Shared Components → **REST data sources** → + Add.
118
124
  | Property | Meaning |
119
125
  |---|---|
120
126
  | Name | Uppercase, e.g. `COUNTRIES` |
121
- | URL | `https://api.example.com/v1/cities/{city}/weather`: `{name}` is a path parameter. The host is fixed: parameters can only come after it |
127
+ | Remote server | Optional (sprint 141): a [remote server](#remote-servers) of the workspace, e.g. `CRM`; the URL is then the path after its base URL |
128
+ | URL | `https://api.example.com/v1/cities/{city}/weather`: `{name}` is a path parameter. The host is fixed: parameters can only come after it. With a remote server: `/v1/cities/{city}/weather` |
122
129
  | Method | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
123
- | Web credential | The name of a web credential, or empty |
130
+ | Web credential | The name of a web credential (the application's, else the workspace's), or empty: on a remote server, the server's default credential |
124
131
  | Parameters | JSON: `[{"name": "city", "in": "path", "default": "&P1_CITY.", "required": true}, {"name": "units", "in": "query", "default": "metric"}]`. `in` is `path`, `query`, `header` or `body`. Defaults may use `&ITEM.` substitutions. `key`: the service's own name for a query, header or body parameter when it isn't lower case with `_`, e.g. `{"name": "page_size", "in": "query", "key": "pageSize"}`; `Authorization`, `Cookie`, `Host` and the transport headers can't be header parameters |
125
132
  | Headers | JSON: extra request headers, e.g. `{"Accept-Language": "en"}`. Not for secrets (use a web credential); `Authorization`, `Cookie`, `Host` and the transport headers can't be set |
126
133
  | Body | For `POST`, `PUT`, `PATCH`: a JSON template, `{name}` becomes the parameter's value as a JSON string. Empty: the `body` parameters as a JSON object |
@@ -143,6 +150,30 @@ call; define them so reports keep their columns when the service changes.
143
150
  Path parameters are URL-encoded (`.` and `..` are refused), query parameters appended, header
144
151
  parameters sent as headers (one line only), so a value from an item can never change the host.
145
152
 
153
+ ### Remote servers
154
+
155
+ A **remote server** (sprint 141; APEX: Remote Servers) is a named base URL of the workspace, kept under Workspace
156
+ utilities → **Remote servers** ([chapter 3](03-builder.md#static-workspace-files-remote-servers-and-web-credentials)):
157
+ a static id (`CRM`), this installation's base URL (`http://127.0.0.1:3100`) and/or an environment variable
158
+ `PGKILN_SERVER_CRM` that wins when set, and an optional default web credential. A source on a server keeps only its
159
+ path (`/a/hr/rest/v1/departments`); every call uses the server's URL **of that moment**, so changing the server (or
160
+ setting `PGKILN_SERVER_CRM=https://crm.example.com` on the production server) moves every source on it at once,
161
+ including write-back, synchronisation, the builder's *Test* and `invoke_api`.
162
+
163
+ - The resolved URL passes every check a full URL does: the allow-list, the workspace's
164
+ [network access list](03-builder.md#network-access) (whose page lists each server's host) and the address checks
165
+ when the connection is made. A server pointed at a private address is refused unless the installation allows it.
166
+ - The path is a path: it starts with `/` or `?` (or is empty), with no `//` or `..` segments, so it can't name another
167
+ host.
168
+ - A server that doesn't exist in the application's workspace fails the call with *Remote server X does not exist in
169
+ this application's workspace*. An application export names the server; it imports where the server is missing, and
170
+ the import message and the [Advisor](03-builder.md#advisor) (a warning) name it.
171
+ - A [REST source catalog](#rest-source-catalogs) can be on a server too, and so can an
172
+ [AI service](03-builder.md#ai-services).
173
+
174
+ HR example (part 76): server **CRM** stands for the HR API on `http://127.0.0.1:3100`; DEPARTMENTS, DEPARTMENT and
175
+ EMPLOYEES_API name it.
176
+
146
177
  ### Regions on a REST data source
147
178
 
148
179
  A region's **REST data source** property (page designer → Source) makes it read the source's
@@ -262,6 +293,9 @@ A catalog comes from:
262
293
  - a **catalog file** downloaded from another workspace or installation (*Download* on the catalog's page);
263
294
  - **by hand**: a name and base URL, then *Add a service*.
264
295
 
296
+ A catalog may name a **remote server** of the workspace (the catalog's page, *Remote server*; sprint 141): its services
297
+ are then paths after the server's URL, and the base URL is optional.
298
+
265
299
  Reading an OpenAPI document, pgkiln takes for each `GET`, `POST`, `PUT`, `PATCH` and `DELETE` operation:
266
300
 
267
301
  | From the document | Becomes |
@@ -279,7 +313,8 @@ YAML documents: convert them to JSON first (most services offer `openapi.json` t
279
313
 
280
314
  **Making a data source from a catalog**: an application's Shared Components → REST data sources → + Add shows *From a
281
315
  REST source catalog* when the application's workspace has catalogs: pick a service, optionally another name and one
282
- of the application's web credentials. The data source is a **copy** (URL = base URL + path): changing or deleting the
316
+ of the web credentials (the application's or the workspace's). The data source is a **copy** (URL = base URL + path; on a
317
+ catalog with a **remote server**, sprint 141: the server and the path, so the source moves with the server): changing or deleting the
283
318
  catalog later doesn't change it, and *Replace from a document* on a catalog's page replaces only the catalog's
284
319
  services. Credentials never belong to a catalog: each application signs in with its own web credential.
285
320
 
@@ -335,6 +370,9 @@ values, processes and workflow steps refer to sources and credentials by name, s
335
370
  internal addresses need `PGKILN_REST_PRIVATE_HOSTS` as well.
336
371
  - A workspace's [network access list](03-builder.md#network-access) narrows the allow-list for its
337
372
  applications (it can't widen it).
373
+ - A [remote server](#remote-servers)'s URL is checked like any other URL; its environment variable can only be a
374
+ `PGKILN_SERVER_…` name, so no other part of the server's environment becomes a URL. Only the workspace's
375
+ administrators change its servers and credentials.
338
376
  - Secrets are encrypted with a key outside the database, write-only in the builder, excluded from
339
377
  exports and from the runtime role, and never logged. Use **Valid for URLs** so a credential is
340
378
  only ever sent to its own service.
@@ -0,0 +1,81 @@
1
+ -- =====================================================================
2
+ -- HR example, part 75: the item and region types of sprint 140 (AP-4;
3
+ -- migration 140; docs/guide/05-items.md, 04-pages-and-regions.md)
4
+ --
5
+ -- Page 50 "Project team" (Administration → Project team) puts a team
6
+ -- together the way an APEX page would:
7
+ --
8
+ -- - P50_DEPTNO, a Select One (a searchable single pick) of the
9
+ -- departments;
10
+ -- - P50_MEMBERS, a Shuttle of that department's employees (a cascading
11
+ -- list of values: choosing another department reloads it). The chosen
12
+ -- employees keep their order (Move up / Move down), stored
13
+ -- colon-separated like a multiselect: 7839:7566:7902;
14
+ -- - P50_SKILLS, a Select Many (chips) of a static list of values.
15
+ -- Without JavaScript the three are a plain select list and two
16
+ -- multi-selection lists, and Save works the same;
17
+ -- - "Chosen team", a Classic Report (no Actions menu) of the chosen
18
+ -- employees in the shuttle's order, and "Head count", a classic report
19
+ -- with the badges template;
20
+ -- - a Breadcrumb region in the Breadcrumb bar position (the title bar
21
+ -- then leaves its own trail out), and a Help Text region showing the
22
+ -- page's help text (Page → Help text, Markdown).
23
+ -- Save keeps the values in the session only: the page has no table.
24
+ -- =====================================================================
25
+
26
+ insert into meta.page (app_id, page_no, name, title, parent_page, help)
27
+ select id, 50, 'Project team', 'Project team', 1,
28
+ $h$Put a **project team** together:
29
+
30
+ 1. Choose a *department* (type to search the list).
31
+ 2. Move its employees from **Available** to **Selected** (double-click, Enter, or the › and » buttons) and put them in order with the arrow buttons.
32
+ 3. Choose the skills the team needs, then **Save**.
33
+
34
+ Team lead for now: &APP_USER..$h$
35
+ from meta.app where alias = 'hr';
36
+
37
+ insert into meta.region (page_id, seq, title, type, columns, template, position, source, config)
38
+ select p.id, r.seq, r.title, r.type, r.cols, r.template, r.position, r.source, r.config::jsonb
39
+ from meta.page p join meta.app a on a.id = p.app_id, (values
40
+ (5, 'Breadcrumb', 'breadcrumb', 12, 'plain', 'breadcrumb', null, '{}'),
41
+ (10, 'Team', 'static', 8, 'standard', null, null, '{}'),
42
+ (20, 'How it works', 'help_text', 4, 'standard', null, null, '{}'),
43
+ (30, 'Chosen team', 'classic_report', 8, 'standard', null,
44
+ 'select e.empno, e.ename, e.job, d.dname
45
+ from unnest(string_to_array(:P50_MEMBERS, '':'')) with ordinality m(empno, n)
46
+ join hr.emp e on e.empno::text = m.empno
47
+ left join hr.dept d on d.deptno = e.deptno
48
+ order by m.n',
49
+ '{"headings": {"empno": "No.", "ename": "Name", "job": "Job", "dname": "Department"}, "sortable": false, "pagination": "none", "empty": "Nobody chosen yet: move employees to Selected and save."}'),
50
+ (40, 'Head count', 'classic_report', 4, 'standard', null,
51
+ 'select d.dname as label, count(e.empno) as value
52
+ from hr.dept d left join hr.emp e on e.deptno = d.deptno
53
+ group by d.dname order by d.dname',
54
+ '{"report_template": "badges", "pagination": "none"}')
55
+ ) as r (seq, title, type, cols, template, position, source, config)
56
+ where a.alias = 'hr' and p.page_no = 50;
57
+
58
+ insert into meta.item (page_id, region_id, seq, name, label, type, lov, config, help)
59
+ select p.id, r.id, i.seq, i.name, i.label, i.type, i.lov, i.config::jsonb, i.help
60
+ from meta.page p join meta.app a on a.id = p.app_id
61
+ join meta.region r on r.page_id = p.id and r.title = 'Team', (values
62
+ (10, 'P50_DEPTNO', 'Department', 'select_one', 'select dname, deptno from hr.dept order by 1', '{"wide": true}', null),
63
+ (20, 'P50_MEMBERS', 'Team members', 'shuttle',
64
+ 'select ename || '' ('' || coalesce(job, ''-'') || '')'' as d, empno as r from hr.emp where deptno = :P50_DEPTNO order by ename',
65
+ '{"cascade_parents": "P50_DEPTNO", "wide": true}', 'In the order they lead the project.'),
66
+ (30, 'P50_SKILLS', 'Skills needed', 'select_many',
67
+ 'STATIC:SQL;SQL,PL/pgSQL;PLPGSQL,TypeScript;TS,Design;DESIGN,Testing;TEST,Accessibility;A11Y',
68
+ '{"wide": true}', null)
69
+ ) as i (seq, name, label, type, lov, config, help)
70
+ where a.alias = 'hr' and p.page_no = 50;
71
+
72
+ insert into meta.button (page_id, region_id, seq, name, label, action, hot)
73
+ select p.id, r.id, 10, 'SAVE', 'Save', 'submit', true
74
+ from meta.page p join meta.app a on a.id = p.app_id
75
+ join meta.region r on r.page_id = p.id and r.title = 'Team'
76
+ where a.alias = 'hr' and p.page_no = 50;
77
+
78
+ insert into meta.nav_entry (app_id, parent_id, seq, label, icon, target_page)
79
+ select a.id, n.id, 70, 'Project team', 'users', 50
80
+ from meta.app a join meta.nav_entry n on n.app_id = a.id and n.label = 'Administration' and n.parent_id is null
81
+ where a.alias = 'hr';
@@ -0,0 +1,48 @@
1
+ -- =====================================================================
2
+ -- HR example, part 76: a remote server and a static workspace file
3
+ -- (sprint 141, AP-7; migration 141; docs/guide/19-rest-data-sources.md
4
+ -- "Remote servers", docs/guide/16-files.md "Static workspace files")
5
+ --
6
+ -- Remote server CRM (Workspace utilities → Remote servers) stands for the
7
+ -- web service of part 23: the HR example's own REST API on
8
+ -- http://127.0.0.1:3100. The REST data sources DEPARTMENTS, DEPARTMENT and
9
+ -- EMPLOYEES_API now name the server and keep only their path
10
+ -- (/a/hr/rest/v1/departments, ...). Moving the API to another host changes
11
+ -- one value: the server's base URL, e.g. https://crm.example.com, or the
12
+ -- server's environment variable PGKILN_SERVER_CRM, which wins when it is
13
+ -- set (one value per environment: development, test, production). Every
14
+ -- source on the server follows at once; the new host must still be on the
15
+ -- allow-list (PGKILN_REST_ALLOWED_HOSTS) and the workspace's network
16
+ -- access list.
17
+ --
18
+ -- brand.css is a static workspace file: every application of the
19
+ -- workspace may load it. HR loads it on every page as
20
+ -- #WORKSPACE_FILES#brand.css (Shared Components → Static application
21
+ -- files → Every page loads); another application of the workspace adds the
22
+ -- same line to share the styles.
23
+ -- =====================================================================
24
+
25
+ insert into meta.remote_server (workspace_id, static_id, name, description, base_url, env_var)
26
+ select meta.app_workspace(a.id), 'CRM', 'HR API (CRM)',
27
+ 'The HR example''s REST API, served by this pgkiln server. Set PGKILN_SERVER_CRM on another environment.',
28
+ 'http://127.0.0.1:3100', 'PGKILN_SERVER_CRM'
29
+ from meta.app a where a.alias = 'hr'
30
+ on conflict (workspace_id, static_id) do nothing;
31
+
32
+ update meta.rest_source s
33
+ set server = 'CRM', url = substr(s.url, length('http://127.0.0.1:3100') + 1)
34
+ from meta.app a
35
+ where a.id = s.app_id and a.alias = 'hr' and s.server is null
36
+ and s.name in ('DEPARTMENTS', 'DEPARTMENT', 'EMPLOYEES_API') and s.url like 'http://127.0.0.1:3100/%';
37
+
38
+ insert into meta.workspace_file (workspace_id, name, mime, content)
39
+ select meta.app_workspace(a.id), 'brand.css', 'text/css', convert_to($css$/* A static workspace file (Workspace utilities → Static workspace files):
40
+ the applications of the workspace load it as #WORKSPACE_FILES#brand.css. */
41
+ .region-header h2 { letter-spacing: .01em; }
42
+ .ws-brand { color: var(--accent, #2b62c4); font-weight: 600; }
43
+ $css$, 'utf8')
44
+ from meta.app a where a.alias = 'hr'
45
+ on conflict (workspace_id, name) do nothing;
46
+
47
+ update meta.app set static_includes = static_includes || '{#WORKSPACE_FILES#brand.css}'
48
+ where alias = 'hr' and not ('#WORKSPACE_FILES#brand.css' = any (static_includes));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.72.0",
3
+ "version": "0.74.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
package/public/app.css CHANGED
@@ -1693,6 +1693,54 @@ td.tc-cell { overflow-wrap: anywhere; }
1693
1693
  }
1694
1694
  .tag-remove:hover, .tag-remove:focus-visible { background: var(--surface-3); }
1695
1695
 
1696
+ /* (140) shuttle: two lists, the move buttons between them and the order buttons after them (stacked on phones) */
1697
+ .shuttle-ui { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto; gap: 0.5rem; align-items: stretch; }
1698
+ .shuttle-col { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; }
1699
+ .shuttle-col .shuttle-list { flex: 1; min-height: 9rem; }
1700
+ .shuttle-moves, .shuttle-order { display: flex; flex-direction: column; justify-content: center; gap: 0.3rem; }
1701
+ .shuttle-btn { min-width: 2.4rem; justify-content: center; font-size: 1rem; line-height: 1; }
1702
+ [dir=rtl] .shuttle-moves .shuttle-btn span { display: inline-block; transform: scaleX(-1); }
1703
+ @media (max-width: 640px) {
1704
+ .shuttle-ui { grid-template-columns: minmax(0, 1fr) auto; }
1705
+ .shuttle-col:first-child { grid-column: 1 / -1; }
1706
+ .shuttle-moves { grid-column: 1 / -1; flex-direction: row; justify-content: center; }
1707
+ .shuttle-moves .shuttle-btn span { display: inline-block; transform: rotate(90deg); }
1708
+ }
1709
+ /* (140) select one / select many: a search field with a list box under it; chips above it */
1710
+ .select-pick-ui { position: relative; display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
1711
+ .select-pick-list {
1712
+ position: absolute; z-index: 30; top: 100%; inset-inline: 0; margin: 0.15rem 0 0; padding: 0.25rem 0; list-style: none; max-height: 16rem; overflow-y: auto;
1713
+ background: var(--surface); border: 1px solid var(--border); border-radius: calc(var(--radius) * 0.75); box-shadow: var(--shadow);
1714
+ }
1715
+ .select-pick-list[hidden] { display: none; }
1716
+ .select-pick-list li { padding: 0.4rem 0.7rem; cursor: pointer; overflow-wrap: anywhere; }
1717
+ .select-pick-list li[aria-selected=true] { font-weight: 600; }
1718
+ .select-pick-list li[aria-selected=true]::before { content: "✓ "; }
1719
+ .select-pick-list li.is-active, .select-pick-list li:hover { background: var(--surface-3); }
1720
+ .select-pick-list li.select-pick-none { cursor: default; color: var(--muted); }
1721
+ /* (140) a breadcrumb region and a help text region */
1722
+ .region-crumbs .crumbs { font-size: 0.9rem; margin: 0; }
1723
+ .region-crumbs .crumbs [aria-current] { color: var(--text); font-weight: 600; }
1724
+ .help-text { overflow-wrap: anywhere; }
1725
+ .help-text > :first-child { margin-top: 0; }
1726
+ .help-text > :last-child { margin-bottom: 0; }
1727
+ /* (140) classic report templates */
1728
+ .cr-pairs { display: flex; flex-direction: column; gap: 0.75rem; }
1729
+ .cr-pairs dl { display: grid; grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr); gap: 0.25rem 1rem; margin: 0; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
1730
+ .cr-pairs dl:last-child { border-bottom: 0; padding-bottom: 0; }
1731
+ .cr-pairs dt { font-weight: 600; color: var(--muted); overflow-wrap: anywhere; }
1732
+ .cr-pairs dd { margin: 0; overflow-wrap: anywhere; }
1733
+ .cr-badges { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 9rem), 1fr)); gap: 0.6rem; list-style: none; margin: 0; padding: 0; }
1734
+ .cr-badges li { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; padding: 0.7rem; border: 1px solid var(--border); border-radius: var(--radius); text-align: center; overflow-wrap: anywhere; }
1735
+ .cr-badges .cr-badge-value { font-size: 1.4rem; font-weight: 700; }
1736
+ .cr-badges .cr-badge-label { font-size: 0.85rem; color: var(--muted); }
1737
+ .cr-media { list-style: none; margin: 0; padding: 0; }
1738
+ .cr-media li { padding: 0.6rem 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
1739
+ .cr-media li:last-child { border-bottom: 0; }
1740
+ .cr-media .cr-media-title { font-weight: 600; }
1741
+ .cr-media .cr-media-text { margin: 0.15rem 0 0; color: var(--muted); }
1742
+ @media (max-width: 640px) { .cr-pairs dl { grid-template-columns: minmax(0, 1fr); } }
1743
+
1696
1744
  .date-range { display: flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; }
1697
1745
  .range-part { display: flex; align-items: center; gap: 0.4rem; flex: 1 1 12rem; min-width: 0; }
1698
1746
  .range-part .sub { font-size: 0.85rem; color: var(--muted); min-width: 2.2rem; }