pgkiln 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/100_generated_labels.sql +381 -0
- package/db/migrations/101_theme_choice.sql +15 -0
- package/db/migrations/102_app_computations.sql +345 -0
- package/db/migrations/103_shortcuts.sql +341 -0
- package/db/migrations/104_report_queries.sql +413 -0
- package/db/migrations/105_component_settings_groups.sql +367 -0
- package/docs/apex-feature-parity.md +12 -12
- package/docs/guide/02-concepts.md +24 -0
- package/docs/guide/03-builder.md +48 -9
- package/docs/guide/04-pages-and-regions.md +20 -4
- package/docs/guide/06-processing.md +22 -0
- package/docs/guide/09-reference.md +14 -0
- package/docs/guide/12-development.md +2 -1
- package/docs/guide/14-globalization.md +5 -1
- package/docs/guide/16-files.md +23 -0
- package/docs/guide/18-cli.md +4 -0
- package/package.json +1 -1
- package/public/app.css +40 -1
- package/public/app.js +69 -1
- package/src/appfiles.ts +18 -2
- package/src/builder/advisor.ts +7 -2
- package/src/builder/appsheets.ts +53 -13
- package/src/builder/appwizard.ts +5 -3
- package/src/builder/code-editor.ts +6 -1
- package/src/builder/components.ts +85 -3
- package/src/builder/forms.ts +1 -1
- package/src/builder/shared.ts +39 -6
- package/src/builder/subscriptions.ts +30 -4
- package/src/cli/replace.ts +6 -2
- package/src/i18n/ar.ts +2 -0
- package/src/i18n/cs.ts +2 -0
- package/src/i18n/da.ts +2 -0
- package/src/i18n/de.ts +2 -0
- package/src/i18n/el.ts +2 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/fi.ts +2 -0
- package/src/i18n/fr.ts +2 -0
- package/src/i18n/he.ts +2 -0
- package/src/i18n/it.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/nb.ts +2 -0
- package/src/i18n/pl.ts +2 -0
- package/src/i18n/pt.ts +2 -0
- package/src/i18n/ru.ts +2 -0
- package/src/i18n/sv.ts +2 -0
- package/src/i18n/tr.ts +2 -0
- package/src/i18n/uk.ts +2 -0
- package/src/i18n/zh.ts +2 -0
- package/src/i18n.ts +4 -0
- package/src/metadata.ts +14 -1
- package/src/runtime/account.ts +5 -5
- package/src/runtime/component-settings.ts +42 -0
- package/src/runtime/context.ts +4 -0
- package/src/runtime/devbar.ts +16 -13
- package/src/runtime/documents.ts +29 -3
- package/src/runtime/engine.ts +2 -2
- package/src/runtime/facets.ts +34 -3
- package/src/runtime/grid.ts +22 -4
- package/src/runtime/items.ts +14 -11
- package/src/runtime/locale.ts +4 -1
- package/src/runtime/logic.ts +12 -7
- package/src/runtime/regions.ts +4 -3
- package/src/runtime/render.ts +8 -2
- package/src/runtime/report.ts +16 -10
- package/src/runtime/routes.ts +9 -6
- package/src/runtime/shortcuts.ts +43 -0
- package/src/security.ts +2 -1
- package/src/subscriptions.ts +67 -0
package/docs/guide/03-builder.md
CHANGED
|
@@ -371,15 +371,20 @@ The wizards (every *Create application* flow and the page wizards) make pages th
|
|
|
371
371
|
forms and dialog titles the singular (*Employee*).
|
|
372
372
|
- **Reports** show a foreign key's parent by its display column (a name, title or label, else its first
|
|
373
373
|
text column): *Manager* shows KING, *Department* shows SALES. The key itself stays in the query,
|
|
374
|
-
hidden, so filters can use it
|
|
374
|
+
hidden, so filters can use it; a report never has two columns with the same heading (also a faceted
|
|
375
|
+
search report, whose facets filter on the display column). A row starts with a pencil that opens the form.
|
|
376
|
+
- **Interactive grids** (the grid page and both grids of a master-detail page) use the same labels as
|
|
377
|
+
headings (*Department Number*, *Hire Date*); a foreign key is a select list of the parent's names.
|
|
375
378
|
- **Files**: a `bytea` column becomes one file upload item that also fills the columns that go with it
|
|
376
379
|
(`<col>_name`/`_filename`, `<col>_mime`/`_mime_type`, `<col>_size`, `<col>_updated`; or `filename`
|
|
377
380
|
and `mime_type` next to the only `bytea` column). Reports leave these columns out. Columns named like
|
|
378
381
|
*photo*, *image* or *logo* accept pictures only.
|
|
379
382
|
- **Numbers** get a format mask with group separators (and the scale's decimals for `numeric(p,s)`),
|
|
380
|
-
in reports and forms alike, in the application's language (*2,850.00*); keys get none.
|
|
381
|
-
- The **Dashboard** comes right after *Home* in the menu; its charts are titled *Employees by Manager
|
|
382
|
-
|
|
383
|
+
in reports, read-only grid cells and forms alike, in the application's language (*2,850.00*); keys get none.
|
|
384
|
+
- The **Dashboard** comes right after *Home* in the menu; its charts are titled *Employees by Manager*
|
|
385
|
+
(a count) or *Salary by Department* (the sum of an amount column: salary, total, price, cost, value…),
|
|
386
|
+
a donut for six groups or fewer, a line for a date, else a sorted bar; a grouping that says nothing (one
|
|
387
|
+
group, or every group the same size) is left out. Rows without a value are labelled *(none)*, and bar charts show the ten largest values (the rest is
|
|
383
388
|
one *Other* bar). Tables with row-level security are left out (the application's role may see none of
|
|
384
389
|
their rows); the result page says which.
|
|
385
390
|
- The **Home** page has a hero with the application's icon and name, and a card per page of the menu
|
|
@@ -493,7 +498,7 @@ step, which keeps the table and type.
|
|
|
493
498
|
| **Calendar** | A [calendar](04-pages-and-regions.md#calendar) region (month, week, day and list views) | Start: a date or timestamp column named like *start*, *begin* or *…_date* (not *created*/*updated*); end: one named like *end* or *until*; title: a name column, else the first foreign key's name |
|
|
494
499
|
| **Chart** | A [chart](04-pages-and-regions.md#chart) region: bar, column, line, area, donut, pie or funnel of the row count, or the sum, average, minimum or maximum of a number column, per label | Label: the first foreign key (its parent's name), else a text column that isn't unique |
|
|
495
500
|
| **Map** | A [map](04-pages-and-regions.md#map) region with popups and, if ticked, an interactive report of the same rows that the map filters (*Show this area in the list*) | Position: a PostGIS geometry or geography (markers, and lines and areas as shapes), a `point`, `lat`/`lng`-like number columns, or a *location* text column with `latitude,longitude` |
|
|
496
|
-
| **Faceted search** | An interactive report (9 columns wide) and a [facets](04-pages-and-regions.md#facets-faceted-search) panel (3 columns, collapsible) with a search field | Up to six facets: foreign keys (values of the parent's name, which the report shows
|
|
501
|
+
| **Faceted search** | An interactive report (9 columns wide) and a [facets](04-pages-and-regions.md#facets-faceted-search) panel (3 columns, collapsible) with a search field | Up to six facets: foreign keys (values of the parent's name, which the report shows instead of the key), booleans, text columns with few values, dates and numbers as ranges with *from*/*to* |
|
|
497
502
|
| **Master detail** | One page with a grid of the table and an editable grid of the selected row's details below it (the grids' [master-detail](04-pages-and-regions.md#grid-interactive-grid)): a hidden item for the selection, both grids with a grid DML process; new detail rows get the master's key | The detail table: a table with a foreign key to this table's primary key |
|
|
498
503
|
|
|
499
504
|
Cards, Calendar, Map and Faceted search take an optional **Form page** number: it adds a modal
|
|
@@ -708,11 +713,13 @@ Chapters 4–7 describe every component type and property.
|
|
|
708
713
|
|
|
709
714
|
While you are signed in to the builder, the pages of your applications show the **developer
|
|
710
715
|
toolbar** when you run them in the same browser (APEX: the Runtime Developer Toolbar): a compact bar
|
|
711
|
-
|
|
716
|
+
centred at the bottom, each tool an icon with a short text (*Home*, *App 12*, *Page 3*, *Session*,
|
|
717
|
+
*Debug off*, *Quick edit*, *Customize*, *Info*) and a tooltip; on narrow screens, docked at the left or
|
|
718
|
+
right, or with *Icons only* in Settings, just the icons. It isn't shown to anyone else, nor inside
|
|
712
719
|
dialogs. The page keeps room for it, so it never covers the menu's last entry or a report's pager.
|
|
713
720
|
After a few seconds without use it shrinks to a slim tab at the edge; point at it or Tab to it to
|
|
714
|
-
bring it back. **Settings** (the last icon) docks it at the bottom, top, left or right
|
|
715
|
-
hiding off (kept in your browser). It has:
|
|
721
|
+
bring it back. **Settings** (the last icon) docks it at the bottom, top, left or right, turns the
|
|
722
|
+
hiding off and shows *Icons only* (kept in your browser). It has:
|
|
716
723
|
|
|
717
724
|
| Entry | Does |
|
|
718
725
|
|---|---|
|
|
@@ -860,6 +867,11 @@ is the earlier page: every component in one rail, with **Access control** next t
|
|
|
860
867
|
| **Lists of values** | Reusable queries for select lists, referenced as `LOV:NAME` |
|
|
861
868
|
| **Application items** | Session variables not on any page, set only by server-side code |
|
|
862
869
|
| **Application processes** | Code that runs *after login* or *before every page* |
|
|
870
|
+
| **Component groups** | Sets of a library application's components that other applications subscribe to at once ([below](#component-groups)) |
|
|
871
|
+
| **Component settings** | Defaults of built-in component types: rows per page, list of values rows, stars ([below](#component-settings)) |
|
|
872
|
+
| **Report queries** | Named queries printed as PDF, Excel, CSV, JSON, XML or HTML with `?doc=NAME` ([chapter 16](16-files.md#report-queries)) |
|
|
873
|
+
| **Shortcuts** | Named texts: `"NAME"` in static content, help texts and confirm messages ([chapter 2](02-concepts.md#shortcuts-name)) |
|
|
874
|
+
| **Application computations** | Computations of (application) items for every page: at sign-in, before every page, after every submit ([chapter 6](06-processing.md#application-computations)) |
|
|
863
875
|
| **REST modules** | REST endpoints (method, path, SQL) served by pgkiln, with an OpenAPI description ([chapter 13](13-rest-api.md#rest-modules-in-the-builder)) |
|
|
864
876
|
| **Workflows** | Multi-step processes of tasks, SQL, web service calls (invoke API), decisions, waits and parallel branches, with versions and a diagram ([chapter 6](06-processing.md#workflows)) |
|
|
865
877
|
| **Task definitions** | Approvals and action tasks: subject, owners, administrators, due date, the SQL that runs on completion ([chapter 6](06-processing.md#approvals-and-the-task-list)) |
|
|
@@ -900,6 +912,33 @@ log (`subscription`). A subscribed list of values that uses a REST data source n
|
|
|
900
912
|
with the same name in the subscribing application. Subscriptions are links between the
|
|
901
913
|
applications of this installation and are not exported (the type is).
|
|
902
914
|
|
|
915
|
+
#### Component groups
|
|
916
|
+
|
|
917
|
+
**Shared Components → Component Groups** (APEX: Component Groups, migration 105) names a set of a
|
|
918
|
+
theme or library application's components, as `kind:name`: `lov:JOBS, authz_scheme:MANAGER,
|
|
919
|
+
build_option:FEATURE_EXPORT, template_component:status_badge, list:SHORTCUTS`. Saving checks that
|
|
920
|
+
the application has each one (and keeps its name as the application spells it). Another
|
|
921
|
+
application of the same workspace subscribes to the whole group on its Subscriptions page
|
|
922
|
+
(*Subscribe to a component group*): each component is copied and subscribed as above, and a member
|
|
923
|
+
the library no longer has is skipped and named in the message. Groups are exported with the
|
|
924
|
+
application (`shared/component-groups/`).
|
|
925
|
+
|
|
926
|
+
### Component settings
|
|
927
|
+
|
|
928
|
+
**Shared Components → Component Settings** (APEX: Component Settings, migration 105) are the
|
|
929
|
+
application's defaults for its built-in component types, used where a component sets nothing itself
|
|
930
|
+
(`meta.app.component_settings`, exported with the application):
|
|
931
|
+
|
|
932
|
+
| Setting | pgkiln's default | Bounds |
|
|
933
|
+
|---|---|---|
|
|
934
|
+
| Reports: rows per page (`report_rows`) | 15 | 1–1,000 |
|
|
935
|
+
| Interactive grids: rows per page (`grid_rows`) | 25 | 1–200 |
|
|
936
|
+
| Popup lists of values: rows per page (`popup_lov_rows`) | 25 | 1–100 |
|
|
937
|
+
| Lists of values: maximum rows (`lov_max_rows`) | 5,000 | 1–50,000 |
|
|
938
|
+
| Star ratings: stars (`rating_max`) | 5 | 3–10 |
|
|
939
|
+
|
|
940
|
+
A value out of its bounds (an edited export, say) is ignored: the default applies.
|
|
941
|
+
|
|
903
942
|
### Supporting objects
|
|
904
943
|
|
|
905
944
|
APEX's *Supporting Objects*: SQL scripts that create, upgrade or remove what the application needs
|
|
@@ -1318,7 +1357,7 @@ imports as empty. Import refuses other formats.
|
|
|
1318
1357
|
| Section | Contents |
|
|
1319
1358
|
|---|---|
|
|
1320
1359
|
| `app` | the application's settings (name, alias, home page, authentication, theme, languages, …) |
|
|
1321
|
-
| `authz_schemes`, `app_items`, `app_processes`, `lovs` | shared components |
|
|
1360
|
+
| `authz_schemes`, `app_items`, `app_processes`, `app_computations`, `shortcuts`, `report_queries`, `component_groups`, `lovs` | shared components |
|
|
1322
1361
|
| `group_roles` | identity-provider group → role mappings |
|
|
1323
1362
|
| `text_messages`, `translations` | globalization |
|
|
1324
1363
|
| `report_layouts` | report layouts; the logo as base64 |
|
|
@@ -222,7 +222,8 @@ select e.empno, e.ename, e.job, d.dname as department, e.sal
|
|
|
222
222
|
|
|
223
223
|
Features for end users:
|
|
224
224
|
|
|
225
|
-
- **Search** across all columns, or in one column chosen
|
|
225
|
+
- **Search** across all columns, or in one column chosen under the magnifier at the start of the search field
|
|
226
|
+
(it shows the column's name once one is chosen); Enter searches (without JavaScript, a *Go* button)
|
|
226
227
|
(Enter searches; *Go* too).
|
|
227
228
|
- **Column heading menu** (sprint 76; with JavaScript): clicking a heading opens a menu with *Sort ascending*,
|
|
228
229
|
*Sort descending*, *Hide column*, *Control break* and the column's **values** with their counts (with the current
|
|
@@ -455,7 +456,9 @@ page). Regions with `"master": {"item": …}` are its details: they use the item
|
|
|
455
456
|
select empno, ename, job, sal from hr.emp where deptno = :P27_DEPTNO::int
|
|
456
457
|
```
|
|
457
458
|
|
|
458
|
-
and show *"Select a row above"* until a row is selected.
|
|
459
|
+
and show *"Select a row above"* until a row is selected. As in APEX, the page opens with the
|
|
460
|
+
master's first row selected, and a search or filter that leaves the selected row out selects the
|
|
461
|
+
first row it shows (a region placed above its master still waits for a selection). With JavaScript the details are
|
|
459
462
|
refreshed in place (`GET …/region/:id`, no page reload); without, the link reloads the page.
|
|
460
463
|
The selection is a signed link (bound to the application, page, user, region and value), so a
|
|
461
464
|
user can only select a row the server showed them; the item is not taken from the URL or a
|
|
@@ -479,6 +482,13 @@ on its new rows (and never lets users edit it); saving new rows without a select
|
|
|
479
482
|
The developer's `hidden` columns can't be grouped, charted or shown in the single row view.
|
|
480
483
|
After **Save** the grid comes back as it was: its search, page, sort and view.
|
|
481
484
|
|
|
485
|
+
**Navigation and edit mode** (APEX). With JavaScript an editable cell reads as text, with its
|
|
486
|
+
column's format mask (*2,850.00*) or the list's display value, until it is edited: click it, or
|
|
487
|
+
press Enter or F2 on it, or start typing (which replaces the value). On a cell the arrow keys move
|
|
488
|
+
to the next cell; while editing, Escape goes back to the cell and Enter moves to the cell below.
|
|
489
|
+
The control keeps the raw value; Tab moves through the controls as before. On a phone, where the
|
|
490
|
+
rows are cards, the fields show as fields.
|
|
491
|
+
|
|
482
492
|
**Copy and paste.** Click a cell and Shift+click another to select a range; Ctrl+C copies it
|
|
483
493
|
as tab-separated text (which spreadsheets paste as cells). Ctrl+V pastes such text (from a
|
|
484
494
|
spreadsheet or another grid) from the focused cell on, adding rows when the grid allows inserts;
|
|
@@ -807,7 +817,9 @@ link is the way to change its dates.
|
|
|
807
817
|
A panel of filters, each with a live count, that filters a **report region on the same page**.
|
|
808
818
|
Counts take the search and all *other* facets into account. Three kinds of facet:
|
|
809
819
|
|
|
810
|
-
- **checkbox** (the default): the most frequent values of a column, each with a checkbox
|
|
820
|
+
- **checkbox** (the default): the most frequent values of a column, each with a checkbox; after the seventh
|
|
821
|
+
the rest sit under *Show all (n)* (open when one of them is chosen), and a boolean's values read *Yes* and
|
|
822
|
+
*No*. With
|
|
811
823
|
`"exclude": true` the facet gets an *Exclude the selected values* switch: the chosen values are
|
|
812
824
|
then left out instead (rows where the column is empty stay).
|
|
813
825
|
- **range**: a number or date column in ranges, as radio buttons ("Any", then each range). A
|
|
@@ -817,7 +829,11 @@ Counts take the search and all *other* facets into account. Three kinds of facet
|
|
|
817
829
|
- **star**: a rating column as "5 stars and up", "4 stars and up", … down to 1 (`max`, default 5).
|
|
818
830
|
|
|
819
831
|
With `"search": true` the panel starts with a search field: the same search as the report's own
|
|
820
|
-
(`r<id>_q`, the row as text contains the term).
|
|
832
|
+
(`r<id>_q`, the row as text contains the term). The report then shows no search field of its own (its
|
|
833
|
+
*Actions* menu stays), so the page has one.
|
|
834
|
+
|
|
835
|
+
The panel starts with the number of rows the search and the facets leave (*Total row count 14*,
|
|
836
|
+
APEX's *Total Row Count*).
|
|
821
837
|
|
|
822
838
|
**Facet charts** (APEX: facet charts): with `"chart": "bar"` or `"chart": "pie"` a facet shows its counts as a chart,
|
|
823
839
|
collapsed under its heading (*Chart*). Each bar or slice, and each row of the chart's data table, links to the page
|
|
@@ -503,6 +503,28 @@ savepoint. A failing computation before the page is shown leaves the item as it
|
|
|
503
503
|
error on the page; after a submit it stops the submit (everything is rolled back) and shows the
|
|
504
504
|
error, like a failing process.
|
|
505
505
|
|
|
506
|
+
### Application computations
|
|
507
|
+
|
|
508
|
+
**Shared Components → Application Computations** (APEX: Application Computations) are
|
|
509
|
+
computations for every page of the application, with the same types, conditions, authorization
|
|
510
|
+
and build option as a page's (migration 102, `meta.app_computation`). Their `point`:
|
|
511
|
+
|
|
512
|
+
| `point` | When |
|
|
513
|
+
|---|---|
|
|
514
|
+
| `after_authentication` | Once, when a user signs in (after the application processes `after_login`) |
|
|
515
|
+
| `before_header` | Every page shown, before the page's own `before_header` computations |
|
|
516
|
+
| `after_submit` | Every page submitted, before the page's own `after_submit` computations (and the validations) |
|
|
517
|
+
|
|
518
|
+
They usually set **application items**, e.g. a user's sales region once at sign-in:
|
|
519
|
+
|
|
520
|
+
```sql
|
|
521
|
+
select region from sales.rep where lower(username) = lower(:APP_USER) -- AI_REGION, sql_query, after_authentication
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
An application computation of a page item runs on that item's page only (elsewhere it is skipped,
|
|
525
|
+
without the *unknown item* error a page's computation gives). A failing one behaves like a page
|
|
526
|
+
computation at the same point.
|
|
527
|
+
|
|
506
528
|
## Branches
|
|
507
529
|
|
|
508
530
|
After a successful submit pgkiln redirects (POST-redirect-GET). **Branches** decide where to: the
|
|
@@ -281,6 +281,7 @@ Run these as the owner (in the SQL Workshop, `psql` or migrations):
|
|
|
281
281
|
| `meta.wizard_catalog(table)` | The columns as the wizards see them: kind (`text`, `number`, `date`, `timestamp`, `boolean`, `point`, `geometry`, `binary`, `other`), key, unique, foreign key and the parent's display column |
|
|
282
282
|
| `meta.wizard_label(name)`, `meta.wizard_table_label(table, plural default true)`, `meta.wizard_column_label(table, column)` | (097) The labels of the wizards: `hiredate` → *Hire Date*, `hr.emp` → *Employees* / *Employee*, `deptno` → *Department*; short comments win ([what the generated pages look like](03-builder.md#what-the-generated-pages-look-like)) |
|
|
283
283
|
| `meta.wizard_report(table, columns default null)` | (097) A report on a table as the wizards make it: `{"source", "hidden", "headings", "formats"}` (foreign keys joined to their parent's display column) |
|
|
284
|
+
| `meta.wizard_grid_labels(table)` | (100) An interactive grid's `{"headings", "formats"}` as the wizards make them: the labels of `meta.wizard_column_label` and the number format masks (keys get none) |
|
|
284
285
|
| `meta.app_activity(days default 30)` | (098) The current application's page views, sign-ins and errors of the last days (1 to 366, at most 10,000 rows, no IP addresses), for a signed-in user with the `admin` role of that application; others get an error. The Activity page of the Create Application wizard reads it |
|
|
285
286
|
| `meta.wizard_files(table)` | (097) The `bytea` columns and the columns holding their file name, mime type, size and change time |
|
|
286
287
|
| `meta.export_app(alias)` | The application as JSON (`pgkiln/2` format) |
|
|
@@ -386,6 +387,19 @@ runtime role, never exported) ([chapter 19](19-rest-data-sources.md#web-credenti
|
|
|
386
387
|
|
|
387
388
|
**`app_process`**: `app_id`, `seq`, `name`, `point` (`after_login` / `before_page`), `code`, `authz`.
|
|
388
389
|
|
|
390
|
+
**`app_computation`** (102): `app_id`, `seq`, `item_name`, `point` (`after_authentication` / `before_header` /
|
|
391
|
+
`after_submit`), `type`, `expression`, `condition_type`, `condition_expr`, `condition_value`, `authz`, `build_option`,
|
|
392
|
+
`comments` ([chapter 6](06-processing.md#application-computations)).
|
|
393
|
+
|
|
394
|
+
**`shortcut`** (103): `app_id`, `name` (uppercase, unique per app), `type` (`text` / `html` / `function_body`), `value`,
|
|
395
|
+
`comments` ([chapter 2](02-concepts.md#shortcuts-name)).
|
|
396
|
+
|
|
397
|
+
**`report_query`** (104): `app_id`, `name` (uppercase, unique per app), `description`, `query`, `format` (`pdf` / `xlsx` /
|
|
398
|
+
`csv` / `json` / `xml` / `html`), `layout`, `file_name`, `title`, `authz`, `comments` ([chapter 16](16-files.md#report-queries)).
|
|
399
|
+
|
|
400
|
+
**`component_group`** (105): `app_id`, `name`, `description`, `members` (`text[]` of `kind:name`) ([chapter 3](03-builder.md#component-groups)).
|
|
401
|
+
`meta.app.component_settings` (105): `report_rows`, `grid_rows`, `popup_lov_rows`, `lov_max_rows`, `rating_max` ([chapter 3](03-builder.md#component-settings)).
|
|
402
|
+
|
|
389
403
|
**`build_option`**: `app_id`, `name` (uppercase, unique per app), `status` (`include` / `exclude`),
|
|
390
404
|
`description` ([chapter 6](06-processing.md#build-options)). `meta.build_option_on(app_id, ref)`
|
|
391
405
|
tells whether a component with that `build_option` value is part of the application.
|
|
@@ -114,7 +114,8 @@ src/
|
|
|
114
114
|
authz.ts authorization schemes, conditions, visibility (menu requests count as buttons)
|
|
115
115
|
engine.ts form fetch, validations, processes (conditions, execution chains, queueing background chains; web requests queued by an sql process are made right after it), application processes
|
|
116
116
|
processes.ts download (file or zip from a query, safe headers), workflow processes, configuration checks of chains
|
|
117
|
-
logic.ts computations, branches (page, URL, function returning a URL, another application) and their conditions
|
|
117
|
+
logic.ts computations (the page's and (102) the application's), branches (page, URL, function returning a URL, another application) and their conditions
|
|
118
|
+
shortcuts.ts (103) "NAME" shortcuts in static content, help texts and confirm messages
|
|
118
119
|
render.ts page chrome (nav, breadcrumb), dynamic action JSON, theme (the page's nonce'd <style>, light/dark and style switches)
|
|
119
120
|
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
|
|
120
121
|
style (user choice, default), themeCss() (only hex values and constants reach the CSS)
|
|
@@ -210,7 +210,11 @@ Under **Settings → Theme**, *Theme style* is **Automatic** (follow the device,
|
|
|
210
210
|
**Light** or **Dark**. With **Users may choose light or dark** (on by default, like APEX's
|
|
211
211
|
*Enable End Users to Choose Theme Style*), the user menu has an *Appearance* switch and My account
|
|
212
212
|
has the same choice. The choice is saved on the account, so it applies in every application that
|
|
213
|
-
allows it.
|
|
213
|
+
allows it. Until a user chooses, an application opens in its own *Theme style* (a new Iris application
|
|
214
|
+
light, also when the device is in dark mode); *Automatic* follows the device only when the application
|
|
215
|
+
or the user chose it. A user who isn't signed in has the *Appearance* switch in the user menu (with
|
|
216
|
+
*Sign in*), kept in a cookie of that application only (`pgkiln_mode`, path `/a/<alias>`), so a choice in
|
|
217
|
+
one application doesn't change another's.
|
|
214
218
|
|
|
215
219
|
## Base styles: Iris, Lavender and Standard
|
|
216
220
|
|
package/docs/guide/16-files.md
CHANGED
|
@@ -604,3 +604,26 @@ name.
|
|
|
604
604
|
The standard PDF fonts cover Western European text; set `PDF_FONT` (and `PDF_FONT_BOLD`) for other
|
|
605
605
|
scripts, as for report PDFs. APEX's Word and Excel templates and outputs have no equivalent: the
|
|
606
606
|
templates here are HTML and the output is PDF.
|
|
607
|
+
|
|
608
|
+
### Report queries
|
|
609
|
+
|
|
610
|
+
**Shared Components → Report Queries** (APEX: Report Queries, migration 104) are named queries an
|
|
611
|
+
application prints, without a template: the query's rows as a table.
|
|
612
|
+
|
|
613
|
+
| Property | Meaning |
|
|
614
|
+
|---|---|
|
|
615
|
+
| `name` | Pages download it with `?doc=NAME` (a document template of that name wins); a button with the action *document* names it |
|
|
616
|
+
| `query` | A SELECT with `:ITEM` binds (the page's session state), run as the application's role; its columns' names are the headings |
|
|
617
|
+
| `format` | `pdf` (with the report layout), `xlsx`, `csv`, `json`, `xml` or `html` |
|
|
618
|
+
| `layout` | A report layout for the PDF; empty: the default layout |
|
|
619
|
+
| `file_name`, `title` | The file's name without extension and the PDF's or HTML file's title (`&ITEM.` allowed); empty: the name, and the description or the name |
|
|
620
|
+
| `authz` | Who may download it, besides access to the page |
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
/a/sales/3?doc=ORDERS_BY_REGION the file, with page 3's session state
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
The files are made like a download process's [data export](06-processing.md#data-export) (Excel and
|
|
627
|
+
PDF hold at most 100,000 rows) and are sent as attachments that a browser never shows as a page
|
|
628
|
+
(`Content-Security-Policy: sandbox`). Signed out, on a public page of an application with sign-in,
|
|
629
|
+
`?doc=` serves only the report queries and document templates of the page's own buttons.
|
package/docs/guide/18-cli.md
CHANGED
|
@@ -65,6 +65,10 @@ hr/
|
|
|
65
65
|
app-items/ai_ename.json
|
|
66
66
|
app-processes/0010-link-user-to-employee.json
|
|
67
67
|
app-processes/0010-link-user-to-employee.code.sql
|
|
68
|
+
app-computations/0010-ai_region-after_authentication.json (by sequence, item and point)
|
|
69
|
+
component-groups/sales_lookups.json
|
|
70
|
+
report-queries/orders_by_region.json (the query in .query.sql)
|
|
71
|
+
shortcuts/delete_confirm_msg.json (a long value in .value.txt, .html or .sql by its type)
|
|
68
72
|
lovs/jobs.json
|
|
69
73
|
lovs/jobs.query.sql
|
|
70
74
|
report-layouts/hr_directory.json (a logo as hr_directory.logo.png)
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -478,6 +478,8 @@ details.btn-menu[open] > summary .btn-caret { transform: rotate(-90deg); }
|
|
|
478
478
|
.report-toolbar { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; align-items: flex-end; margin-bottom: 0.75rem; }
|
|
479
479
|
.report-toolbar .field { flex: 0 1 240px; }
|
|
480
480
|
.search { display: flex; gap: 0.4rem; flex: 1 1 320px; max-width: 560px; margin-left: auto; }
|
|
481
|
+
/* (sprint 78) a report whose search is the facets': the Actions menu at the start of the toolbar */
|
|
482
|
+
.report-toolbar .search:not(:has(input[type=search])) { flex: 0 1 auto; min-width: 0; margin-left: 0; }
|
|
481
483
|
.chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.75rem; }
|
|
482
484
|
.chip { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.83rem; padding: 0.15rem 0.3rem 0.15rem 0.7rem; background: var(--accent-soft); border-radius: 99px; }
|
|
483
485
|
.chip a { text-decoration: none; font-size: 1rem; line-height: 1; padding: 0 0.3rem; border-radius: 99px; }
|
|
@@ -1054,6 +1056,20 @@ fieldset.prop-group > legend { font-size: 0.75rem; text-transform: uppercase; le
|
|
|
1054
1056
|
.grid-table th.grid-drop-after { box-shadow: inset -3px 0 0 var(--accent); }
|
|
1055
1057
|
/* copy and paste: a range of selected cells */
|
|
1056
1058
|
.grid-table td.grid-cell-selected { background: color-mix(in srgb, var(--accent) 16%, var(--surface)); box-shadow: inset 0 0 0 1px var(--accent); }
|
|
1059
|
+
/* cell navigation mode (APEX): with JavaScript an editable cell reads as text (its formatted value, .grid-text)
|
|
1060
|
+
until it is clicked or Enter / F2 edits it; arrow keys move between cells (app.js) */
|
|
1061
|
+
.grid-text { display: none; }
|
|
1062
|
+
.js .grid-table td[data-col]:not(.grid-frozen) { position: relative; }
|
|
1063
|
+
.js .grid-table tbody td[data-col]:focus { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
1064
|
+
.js .grid-table tbody td[data-col] :is(input:not([type=checkbox]), select) { border-color: transparent; background: transparent; box-shadow: none; }
|
|
1065
|
+
.js .grid-table tbody td[data-col] select { appearance: none; }
|
|
1066
|
+
.js .grid-table tbody td[data-col]:focus-within :is(input:not([type=checkbox]), select) { border-color: var(--border); background: var(--surface); appearance: auto; }
|
|
1067
|
+
.js .grid-table td[data-col] > .grid-text { display: flex; align-items: center; position: absolute; inset: 0; padding: 0 calc(0.85rem + 1px);
|
|
1068
|
+
overflow: hidden; white-space: nowrap; pointer-events: none; }
|
|
1069
|
+
.js .grid-table td[data-col] > .grid-text.num { justify-content: flex-end; font-variant-numeric: tabular-nums; }
|
|
1070
|
+
.js .grid-table td[data-col]:has(> .grid-text) :is(input, select) { color: transparent; }
|
|
1071
|
+
.js .grid-table td[data-col]:focus-within > .grid-text { display: none; }
|
|
1072
|
+
.js .grid-table td[data-col]:focus-within :is(input, select) { color: inherit; }
|
|
1057
1073
|
.grid-columns-form { border-collapse: collapse; font-size: 0.85rem; }
|
|
1058
1074
|
.grid-columns-form th, .grid-columns-form td { padding: 0.2rem 0.35rem; text-align: left; white-space: nowrap; }
|
|
1059
1075
|
.grid-columns-form input[type=number] { width: 5.5rem; }
|
|
@@ -1144,6 +1160,10 @@ td[data-add]:hover > .cal-add, .cal-add:focus-visible { opacity: 1; }
|
|
|
1144
1160
|
.facet-chart .donut-figure { max-width: 9rem; margin: 0 auto; }
|
|
1145
1161
|
.facet-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
1146
1162
|
.facet-count { font-size: 0.75rem; color: var(--muted); background: var(--surface-3); border-radius: 99px; padding: 0 0.5rem; font-variant-numeric: tabular-nums; }
|
|
1163
|
+
/* (sprint 78) values after the seventh, and the rows the filters leave */
|
|
1164
|
+
.facet-more > summary { cursor: pointer; font-size: 0.85rem; color: var(--accent, var(--link)); padding: 0.15rem 0; }
|
|
1165
|
+
.facet-more[open] > summary { margin-bottom: 0.2rem; }
|
|
1166
|
+
.facet-total { margin: 0 0 0.6rem; font-size: 0.85rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
1147
1167
|
.js .facet-actions .btn-hot { display: none; }
|
|
1148
1168
|
.facet-exclude { font-size: 0.85rem; color: var(--muted); margin-top: 0.15rem; }
|
|
1149
1169
|
.facet-excluded .facet-value .facet-label { text-decoration: line-through; }
|
|
@@ -1210,6 +1230,9 @@ input[type=color] { width: 4rem; min-height: 2.4rem; padding: 0.15rem; }
|
|
|
1210
1230
|
.report-reflow.grid-table .agg-row td.grid-lead { display: none; }
|
|
1211
1231
|
.report-reflow.grid-table td.grid-lead:empty { display: none; }
|
|
1212
1232
|
.grid-resize { display: none !important; }
|
|
1233
|
+
/* a card shows its fields as fields: no navigation mode */
|
|
1234
|
+
.js .report-reflow.grid-table td[data-col] > .grid-text { display: none; }
|
|
1235
|
+
.js .report-reflow.grid-table tbody td[data-col] :is(input, select) { color: inherit; border-color: var(--border); background: var(--surface); appearance: auto; }
|
|
1213
1236
|
}
|
|
1214
1237
|
|
|
1215
1238
|
/* two-thirds / one-third columns on desktops (builder Users page) */
|
|
@@ -1811,6 +1834,14 @@ dialog.crop-dialog::backdrop { background: rgb(15 20 30 / 55%); }
|
|
|
1811
1834
|
.devbar-tool::-webkit-details-marker { display: none; }
|
|
1812
1835
|
.devbar .devbar-tool:hover, .devbar-menu[open] > .devbar-tool, .devbar-quick[aria-pressed="true"] { background: #2f3b4f; color: #fff; }
|
|
1813
1836
|
.devbar-debug-menu.is-on > .devbar-tool { color: #ffd166; }
|
|
1837
|
+
/* (sprint 79, UI-4) a text next to each icon (App 12, Page 3, Debug off); icons only when chosen or docked at a side */
|
|
1838
|
+
.devbar .devbar-tool:has(.devbar-text) { width: auto; min-width: 2.25rem; padding: 0 0.55rem; grid-auto-flow: column; gap: 0.35rem; white-space: nowrap; }
|
|
1839
|
+
.devbar[data-compact] .devbar-text, .devbar[data-dock="left"] .devbar-text, .devbar[data-dock="right"] .devbar-text { display: none; }
|
|
1840
|
+
.devbar[data-compact] .devbar-tool:has(.devbar-text), .devbar[data-dock="left"] .devbar-tool:has(.devbar-text), .devbar[data-dock="right"] .devbar-tool:has(.devbar-text) { width: 2.25rem; padding: 0; }
|
|
1841
|
+
@media (max-width: 900px) {
|
|
1842
|
+
.devbar .devbar-text { display: none; }
|
|
1843
|
+
.devbar .devbar-tool:has(.devbar-text) { width: 2.25rem; padding: 0; }
|
|
1844
|
+
}
|
|
1814
1845
|
.devbar a, .devbar-pop a { color: #9cc8ff; }
|
|
1815
1846
|
.devbar a:focus-visible, .devbar button:focus-visible, .devbar select:focus-visible, .devbar summary:focus-visible, .devbar input:focus-visible { outline: 2px solid #9cc8ff; outline-offset: 2px; }
|
|
1816
1847
|
.devbar-menu { position: relative; }
|
|
@@ -1887,8 +1918,9 @@ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft);
|
|
|
1887
1918
|
.devbar-quick[aria-pressed="true"] { background: var(--accent); color: var(--accent-text); }
|
|
1888
1919
|
|
|
1889
1920
|
/* (sprint 76) the pager: rows per page; a Go button only without JavaScript */
|
|
1890
|
-
.pager-size { display: inline-flex; align-items: center; gap: 0.4rem; margin-left: auto; font-size: 0.85rem; }
|
|
1921
|
+
.pager .pager-size { margin-right: 0; display: inline-flex; align-items: center; gap: 0.4rem; margin-left: auto; font-size: 0.85rem; }
|
|
1891
1922
|
.pager-size select { min-height: 0; padding: 0.15rem 0.4rem; }
|
|
1923
|
+
.pager-size label { white-space: nowrap; }
|
|
1892
1924
|
.js .no-js-only { display: none; }
|
|
1893
1925
|
|
|
1894
1926
|
/* (sprint 76) the interactive report's Actions menu: one line per entry, submenus, forms in dialogs */
|
|
@@ -1914,6 +1946,13 @@ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft);
|
|
|
1914
1946
|
.search-field { display: flex; flex: 1; min-width: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
|
|
1915
1947
|
.search-field:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1916
1948
|
.search-field .search-scope { border: 0; border-right: 1px solid var(--border); border-radius: var(--radius) 0 0 var(--radius); max-width: 9rem; background: var(--surface-2); }
|
|
1949
|
+
/* (sprint 79, IR-3; APEX: the column scope in the magnifier of the search field) all columns: only the magnifier;
|
|
1950
|
+
a column: the magnifier and its name */
|
|
1951
|
+
.search-scope-wrap { position: relative; display: flex; flex: none; }
|
|
1952
|
+
.search-scope-wrap > .icon { position: absolute; left: 0.6rem; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--muted); }
|
|
1953
|
+
.search-field .search-scope-wrap .search-scope { padding-left: 2rem; }
|
|
1954
|
+
.search-field .search-scope-wrap .search-scope:has(option[value=""]:checked) { width: 2.75rem; padding-right: 0.5rem; color: transparent; }
|
|
1955
|
+
.search-field .search-scope option { color: var(--text); }
|
|
1917
1956
|
.search-field input[type=search] { border: 0; flex: 1; min-width: 0; background: transparent; }
|
|
1918
1957
|
.search-field input[type=search]:focus { outline: none; }
|
|
1919
1958
|
/* a column heading's menu */
|
package/public/app.js
CHANGED
|
@@ -1666,6 +1666,69 @@ const pgkilnUnsaved = (() => {
|
|
|
1666
1666
|
data[i].forEach((v, j) => cells[c0 + j] && setCell(cells[c0 + j], v));
|
|
1667
1667
|
}
|
|
1668
1668
|
});
|
|
1669
|
+
|
|
1670
|
+
// ------------------------------------------------------------ cell navigation and edit mode (APEX)
|
|
1671
|
+
// An editable cell reads as text (app.css) until it is edited: a click in it, Enter or F2 or typing
|
|
1672
|
+
// edits it. On a cell the arrow keys move to the next one; while editing, Escape goes back to the
|
|
1673
|
+
// cell and Enter to the cell below. Tab moves through the cells' controls as usual.
|
|
1674
|
+
const cellSel = '[data-grid] table.grid-table > tbody:not(.grid-template) > tr > td[data-col]';
|
|
1675
|
+
function focusCell(td, edit) {
|
|
1676
|
+
if (!td) return;
|
|
1677
|
+
const el = control(td);
|
|
1678
|
+
if (edit && el && !el.disabled) {
|
|
1679
|
+
el.focus();
|
|
1680
|
+
if (el.type !== 'checkbox' && el.tagName !== 'SELECT') el.select?.();
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (!td.hasAttribute('tabindex')) td.tabIndex = -1;
|
|
1684
|
+
td.focus();
|
|
1685
|
+
}
|
|
1686
|
+
function neighbour(td, dr, dc) {
|
|
1687
|
+
const rows = rowsOf(td.closest('[data-grid]'));
|
|
1688
|
+
const { r, c } = at(td);
|
|
1689
|
+
return rows[r + dr] ? shownCells(rows[r + dr])[c + dc] : undefined;
|
|
1690
|
+
}
|
|
1691
|
+
const MOVES = { ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1] };
|
|
1692
|
+
document.addEventListener('keydown', (e) => {
|
|
1693
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
1694
|
+
const t = e.target;
|
|
1695
|
+
if (t.matches?.(cellSel)) {
|
|
1696
|
+
const el = control(t);
|
|
1697
|
+
if (MOVES[e.key]) {
|
|
1698
|
+
e.preventDefault();
|
|
1699
|
+
focusCell(neighbour(t, ...MOVES[e.key]), false);
|
|
1700
|
+
} else if (el && (e.key === 'Enter' || e.key === 'F2')) {
|
|
1701
|
+
e.preventDefault();
|
|
1702
|
+
focusCell(t, true);
|
|
1703
|
+
} else if (el?.type === 'checkbox' && e.key === ' ') {
|
|
1704
|
+
e.preventDefault();
|
|
1705
|
+
el.click();
|
|
1706
|
+
} else if (el && e.key.length === 1) focusCell(t, true); // the key types into the control
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const td = t.closest?.(cellSel);
|
|
1710
|
+
if (!td || !t.matches('input, select')) return;
|
|
1711
|
+
if (e.key === 'Escape') focusCell(td, false);
|
|
1712
|
+
else if (e.key === 'Enter' && !e.shiftKey) {
|
|
1713
|
+
e.preventDefault();
|
|
1714
|
+
const below = neighbour(td, 1, 0);
|
|
1715
|
+
focusCell(below ?? td, !!below);
|
|
1716
|
+
} else if (e.key === 'F2') focusCell(td, false);
|
|
1717
|
+
});
|
|
1718
|
+
// a click in a cell beside its control edits it
|
|
1719
|
+
document.addEventListener('click', (e) => {
|
|
1720
|
+
if (!e.shiftKey && e.target.matches?.(cellSel)) focusCell(e.target, true);
|
|
1721
|
+
});
|
|
1722
|
+
// an edited value shows as itself (a list's display value)
|
|
1723
|
+
const syncText = (e) => {
|
|
1724
|
+
const span = e.target.closest?.('td[data-col]')?.querySelector(':scope > .grid-text');
|
|
1725
|
+
if (!span) return;
|
|
1726
|
+
const el = e.target;
|
|
1727
|
+
if (el.tagName === 'SELECT') span.textContent = el.selectedIndex > 0 ? el.options[el.selectedIndex].text : '';
|
|
1728
|
+
else span.remove();
|
|
1729
|
+
};
|
|
1730
|
+
document.addEventListener('input', syncText);
|
|
1731
|
+
document.addEventListener('change', syncText);
|
|
1669
1732
|
})();
|
|
1670
1733
|
|
|
1671
1734
|
// Faceted search: apply on change. A range facet's own from/to: typing picks
|
|
@@ -3642,7 +3705,7 @@ function areaFilter(map, f) {
|
|
|
3642
3705
|
const bar = document.querySelector('.devbar');
|
|
3643
3706
|
if (!bar) return;
|
|
3644
3707
|
const KEY = 'pgkiln.devbar';
|
|
3645
|
-
let prefs = { dock: 'bottom', autohide: true };
|
|
3708
|
+
let prefs = { dock: 'bottom', autohide: true, compact: false };
|
|
3646
3709
|
try { prefs = { ...prefs, ...JSON.parse(localStorage.getItem(KEY) || '{}') }; } catch {}
|
|
3647
3710
|
if (!['bottom', 'top', 'left', 'right'].includes(prefs.dock)) prefs.dock = 'bottom';
|
|
3648
3711
|
const save = () => { try { localStorage.setItem(KEY, JSON.stringify(prefs)); } catch {} };
|
|
@@ -3650,6 +3713,10 @@ function areaFilter(map, f) {
|
|
|
3650
3713
|
settings?.closest('li')?.removeAttribute('hidden');
|
|
3651
3714
|
const apply = () => {
|
|
3652
3715
|
bar.dataset.dock = prefs.dock;
|
|
3716
|
+
// (sprint 79) "Icons only": the tools without their texts
|
|
3717
|
+
bar.toggleAttribute('data-compact', !!prefs.compact);
|
|
3718
|
+
const compact = bar.querySelector('input[name="devbar-compact"]');
|
|
3719
|
+
if (compact) compact.checked = !!prefs.compact;
|
|
3653
3720
|
bar.querySelectorAll('input[name="devbar-dock"]').forEach((r) => { r.checked = r.value === prefs.dock; });
|
|
3654
3721
|
const auto = bar.querySelector('input[name="devbar-autohide"]');
|
|
3655
3722
|
if (auto) auto.checked = !!prefs.autohide;
|
|
@@ -3658,6 +3725,7 @@ function areaFilter(map, f) {
|
|
|
3658
3725
|
bar.addEventListener('change', (e) => {
|
|
3659
3726
|
if (e.target.name === 'devbar-dock') prefs.dock = e.target.value;
|
|
3660
3727
|
else if (e.target.name === 'devbar-autohide') prefs.autohide = e.target.checked;
|
|
3728
|
+
else if (e.target.name === 'devbar-compact') prefs.compact = e.target.checked;
|
|
3661
3729
|
else return;
|
|
3662
3730
|
save();
|
|
3663
3731
|
apply();
|
package/src/appfiles.ts
CHANGED
|
@@ -116,6 +116,10 @@ const CODE: Record<string, Record<string, string>> = {
|
|
|
116
116
|
computation: { expression: 'sql', condition_expr: 'sql' },
|
|
117
117
|
branch: { condition_expr: 'sql' },
|
|
118
118
|
app_process: { code: 'sql' },
|
|
119
|
+
app_computation: { expression: 'sql', condition_expr: 'sql' },
|
|
120
|
+
// (103) a shortcut's value: text, HTML or PL/pgSQL (see codeExt)
|
|
121
|
+
shortcut: { value: 'txt' },
|
|
122
|
+
report_query: { query: 'sql' },
|
|
119
123
|
authz_scheme: { value: 'sql' },
|
|
120
124
|
lov: { query: 'sql' },
|
|
121
125
|
automation: { query: 'sql', code: 'sql' },
|
|
@@ -134,7 +138,9 @@ const CODE: Record<string, Record<string, string>> = {
|
|
|
134
138
|
};
|
|
135
139
|
const isLong = (v: unknown): v is string => typeof v === 'string' && (v.includes('\n') || v.length > 60);
|
|
136
140
|
const codeExt = (table: string, column: string, row: any) =>
|
|
137
|
-
table === 'region' && column === 'source' && row.type === 'static' ? 'html'
|
|
141
|
+
table === 'region' && column === 'source' && row.type === 'static' ? 'html'
|
|
142
|
+
: table === 'shortcut' ? (row.type === 'html' ? 'html' : row.type === 'function_body' ? 'sql' : 'txt')
|
|
143
|
+
: CODE[table][column];
|
|
138
144
|
|
|
139
145
|
export type Style = 'json' | 'text';
|
|
140
146
|
|
|
@@ -191,6 +197,11 @@ const NAMED: [section: string, dir: string, table: string][] = [
|
|
|
191
197
|
// (086) application settings
|
|
192
198
|
['app_settings', 'shared/app-settings', 'app_setting'],
|
|
193
199
|
['page_groups', 'shared/page-groups', 'page_group'],
|
|
200
|
+
// (103) shortcuts, (104) report queries
|
|
201
|
+
['shortcuts', 'shared/shortcuts', 'shortcut'],
|
|
202
|
+
['report_queries', 'shared/report-queries', 'report_query'],
|
|
203
|
+
// (105) component groups
|
|
204
|
+
['component_groups', 'shared/component-groups', 'component_group'],
|
|
194
205
|
];
|
|
195
206
|
|
|
196
207
|
/** Components of a page: [array in the document, directory, table, key source]. */
|
|
@@ -214,10 +225,11 @@ const SINGLE: [section: string, path: string, sort: string[]][] = [
|
|
|
214
225
|
];
|
|
215
226
|
|
|
216
227
|
const ACTIONS_DIR = 'shared/automation-actions';
|
|
228
|
+
const APP_COMPUTATIONS_DIR = 'shared/app-computations';
|
|
217
229
|
const STATIC_DIR = 'static';
|
|
218
230
|
const STATIC_INDEX = `${STATIC_DIR}/files.json`;
|
|
219
231
|
|
|
220
|
-
const KNOWN = new Set(['format', 'app', 'static_files', 'app_processes', 'translations', 'nav', 'list_entries', 'automation_actions', 'pages', ...NAMED.map((n) => n[0]), ...SINGLE.map((s) => s[0])]);
|
|
232
|
+
const KNOWN = new Set(['format', 'app', 'static_files', 'app_processes', 'app_computations', 'translations', 'nav', 'list_entries', 'automation_actions', 'pages', ...NAMED.map((n) => n[0]), ...SINGLE.map((s) => s[0])]);
|
|
221
233
|
|
|
222
234
|
const byColumns = (cols: string[]) => (a: any, b: any) => {
|
|
223
235
|
for (const c of cols) {
|
|
@@ -258,6 +270,9 @@ export function docToFiles(doc: Doc, style: Style = 'json'): FileMap {
|
|
|
258
270
|
|
|
259
271
|
const procs: any[] = doc.app_processes ?? [];
|
|
260
272
|
uniqueKeys(procs, (r) => r.name, 'process').forEach((k, i) => w.record('shared/app-processes', `${seqPrefix(procs[i].seq)}-${k}`, 'app_process', procs[i]));
|
|
273
|
+
// (102) application computations, by sequence, item and point
|
|
274
|
+
const comps: any[] = doc.app_computations ?? [];
|
|
275
|
+
uniqueKeys(comps, (r) => `${r.item_name ?? ''} ${r.point ?? ''}`, 'computation').forEach((k, i) => w.record(APP_COMPUTATIONS_DIR, `${seqPrefix(comps[i].seq)}-${k}`, 'app_computation', comps[i]));
|
|
261
276
|
|
|
262
277
|
for (const [section, path, sort] of SINGLE) w.json(path, [...(doc[section] ?? [])].sort(byColumns(sort)));
|
|
263
278
|
|
|
@@ -453,6 +468,7 @@ export function filesToDoc(files: FileMap): Doc {
|
|
|
453
468
|
});
|
|
454
469
|
}
|
|
455
470
|
doc.app_processes = readRecords(files, 'shared/app-processes').sort(SEQ_SORT).map(mergeCode);
|
|
471
|
+
doc.app_computations = readRecords(files, APP_COMPUTATIONS_DIR).sort(SEQ_SORT).map(mergeCode);
|
|
456
472
|
for (const [section, path] of SINGLE) doc[section] = read(path);
|
|
457
473
|
doc.translations = [...files.keys()]
|
|
458
474
|
.filter((p) => /^globalization\/translations\/[^/]+\.(json|yaml)$/.test(p))
|
package/src/builder/advisor.ts
CHANGED
|
@@ -57,6 +57,7 @@ function sqlFields(kind: string, row: any): { name: string; shape: SqlShape }[]
|
|
|
57
57
|
case 'button':
|
|
58
58
|
return [{ name: 'condition', shape: 'boolean' }, { name: 'badge_query', shape: 'select' }];
|
|
59
59
|
case 'computation':
|
|
60
|
+
case 'app_computation':
|
|
60
61
|
return [
|
|
61
62
|
...(row.type === 'sql_query' ? [{ name: 'expression', shape: 'select' as const }] : []),
|
|
62
63
|
...conditionSql(row),
|
|
@@ -83,6 +84,7 @@ function sqlFields(kind: string, row: any): { name: string; shape: SqlShape }[]
|
|
|
83
84
|
case 'automation_action':
|
|
84
85
|
return [{ name: 'code', shape: 'statements' }, { name: 'condition', shape: 'boolean' }];
|
|
85
86
|
case 'document_template':
|
|
87
|
+
case 'report_query':
|
|
86
88
|
return [{ name: 'query', shape: 'select' }];
|
|
87
89
|
case 'task_definition':
|
|
88
90
|
return [{ name: 'action_code', shape: 'statements' }];
|
|
@@ -286,7 +288,8 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
|
|
|
286
288
|
const buildOptions = new Set(all.filter((x) => x.kind === 'build_option').map((x) => x.row.name));
|
|
287
289
|
const layouts = new Set(all.filter((x) => x.kind === 'report_layout').map((x) => x.row.name.toUpperCase()));
|
|
288
290
|
const regionIds = new Set(all.filter((x) => x.kind === 'region').map((x) => x.row.id));
|
|
289
|
-
|
|
291
|
+
// (104) a document button names a document template or a report query
|
|
292
|
+
const documents = new Set(all.filter((x) => x.kind === 'document_template' || x.kind === 'report_query').map((x) => x.row.name.toUpperCase()));
|
|
290
293
|
const missing = (entry: Entry | null, field: string, message: string, severity: Severity = 'error') => findings.push({ severity, entry, field, message });
|
|
291
294
|
|
|
292
295
|
for (const e of entries) {
|
|
@@ -353,7 +356,7 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
|
|
|
353
356
|
&& !(await one('select 1 as ok from meta.app a join meta.page p on p.app_id = a.id where a.alias = $1 and p.page_no = $2', [row.target_app, row.target_page])))
|
|
354
357
|
missing(e, 'Application (app)', `Application ${row.target_app} has no page ${row.target_page}.`);
|
|
355
358
|
if (kind === 'button' && row.action === 'document' && !documents.has(String(row.document ?? '').toUpperCase()))
|
|
356
|
-
missing(e, 'Document template', row.document ? `Document template ${row.document} doesn't exist.` : 'A document button needs a document template.');
|
|
359
|
+
missing(e, 'Document template', row.document ? `Document template or report query ${row.document} doesn't exist.` : 'A document button needs a document template or a report query.');
|
|
357
360
|
if (kind === 'rest_module') for (const problem of handlerProblems(row.handlers)) missing(e, 'Handlers (JSON)', problem);
|
|
358
361
|
if (kind === 'workflow_definition') {
|
|
359
362
|
const tasks = new Set(all.filter((x) => x.kind === 'task_definition').map((x) => x.row.name.toUpperCase()));
|
|
@@ -376,6 +379,8 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
|
|
|
376
379
|
for (const problem of [...devRefs.errors, ...devRefs.warnings]) missing(e, field, problem, 'warning');
|
|
377
380
|
}
|
|
378
381
|
}
|
|
382
|
+
if (kind === 'report_query' && row.layout && !layouts.has(String(row.layout).toUpperCase())) missing(e, 'Report layout', `Report layout ${row.layout} doesn't exist (the default layout is used).`, 'warning');
|
|
383
|
+
if (kind === 'app_computation' && row.type === 'item' && !items.has(String(row.expression).toUpperCase())) missing(e, 'Expression', `Item ${row.expression} doesn't exist (its value is always empty).`, 'warning');
|
|
379
384
|
if (kind === 'document_template') {
|
|
380
385
|
const problem = templateProblem(row.template ?? '');
|
|
381
386
|
if (problem) missing(e, 'Template (HTML)', problem);
|