pgkiln 0.81.0 → 0.82.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/147_condition_types.sql +80 -0
- package/db/migrations/148_validation_types.sql +44 -0
- package/db/migrations/149_dynamic_action_events.sql +45 -0
- package/db/migrations/150_process_types.sql +23 -0
- package/db/migrations/151_item_types.sql +16 -0
- package/db/migrations/152_workflow_diagram_region.sql +10 -0
- package/docs/apex-feature-parity.md +10 -8
- package/docs/guide/02-concepts.md +3 -3
- package/docs/guide/04-pages-and-regions.md +17 -3
- package/docs/guide/05-items.md +5 -0
- package/docs/guide/06-processing.md +104 -17
- package/docs/guide/07-dynamic-actions.md +16 -1
- package/docs/guide/11-from-apex.md +7 -0
- package/examples/hr/hr_79_condition_types.sql +17 -0
- package/package.json +1 -1
- package/public/app.css +12 -0
- package/public/app.js +157 -9
- package/src/appfiles.ts +4 -4
- package/src/builder/advisor.ts +43 -7
- package/src/builder/code-editor.ts +14 -10
- package/src/builder/components.ts +51 -21
- package/src/builder/conditions.ts +64 -0
- package/src/builder/designer.ts +9 -1
- package/src/builder/i18n/de.ts +35 -0
- package/src/builder/i18n/en.ts +35 -0
- package/src/builder/i18n/es.ts +35 -0
- package/src/builder/i18n/fr.ts +35 -0
- package/src/builder/i18n/it.ts +35 -0
- package/src/builder/i18n/ja.ts +35 -0
- package/src/builder/i18n/ko.ts +35 -0
- package/src/builder/i18n/nl.ts +35 -0
- package/src/builder/i18n/pt.ts +35 -0
- package/src/builder/i18n/zh-cn.ts +35 -0
- package/src/builder/i18n/zh-tw.ts +35 -0
- package/src/builder/region-help.ts +3 -1
- package/src/builder/region-settings.ts +30 -5
- package/src/i18n/ar.ts +5 -0
- package/src/i18n/cs.ts +5 -0
- package/src/i18n/da.ts +5 -0
- package/src/i18n/de.ts +5 -0
- package/src/i18n/el.ts +5 -0
- package/src/i18n/es.ts +5 -0
- package/src/i18n/fi.ts +5 -0
- package/src/i18n/fr.ts +5 -0
- package/src/i18n/he.ts +5 -0
- package/src/i18n/it.ts +5 -0
- package/src/i18n/ja.ts +5 -0
- package/src/i18n/ko.ts +5 -0
- package/src/i18n/nb.ts +5 -0
- package/src/i18n/pl.ts +5 -0
- package/src/i18n/pt.ts +5 -0
- package/src/i18n/ru.ts +5 -0
- package/src/i18n/sv.ts +5 -0
- package/src/i18n/tr.ts +5 -0
- package/src/i18n/uk.ts +5 -0
- package/src/i18n/zh.ts +5 -0
- package/src/i18n.ts +10 -0
- package/src/metadata.ts +72 -9
- package/src/runtime/authz.ts +9 -5
- package/src/runtime/context.ts +4 -0
- package/src/runtime/da-styles.ts +37 -0
- package/src/runtime/documents.ts +3 -2
- package/src/runtime/engine.ts +95 -13
- package/src/runtime/items.ts +33 -0
- package/src/runtime/logic.ts +79 -8
- package/src/runtime/maps.ts +50 -4
- package/src/runtime/process-types.ts +139 -0
- package/src/runtime/processes.ts +2 -0
- package/src/runtime/regions.ts +4 -0
- package/src/runtime/render.ts +13 -2
- package/src/runtime/routes.ts +2 -2
- package/src/runtime/workflow-diagram.ts +45 -0
- package/src/workflow.ts +10 -2
|
@@ -79,11 +79,32 @@ process fails, earlier inserts and updates are rolled back too.
|
|
|
79
79
|
|
|
80
80
|
## Validations
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
| `
|
|
85
|
-
|
|
86
|
-
| `
|
|
82
|
+
APEX's validation types (sprint 154–155; APEX: *Validation → Type*):
|
|
83
|
+
|
|
84
|
+
| `type` | APEX | Passes when |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `sql` | Expression | `expression` (a SQL boolean expression, binds allowed) is true |
|
|
87
|
+
| `exists` / `not_exists` | Rows returned / No rows returned | the query in `expression` returns a row / no row |
|
|
88
|
+
| `function_body` | Function Body (returning Boolean) | the PL/pgSQL body in `expression` returns true |
|
|
89
|
+
| `function_error` | Function Body (returning Error Text) | the body returns null or empty; otherwise its text is the error message |
|
|
90
|
+
| `not_null` | Item is NOT NULL | the associated item has a value |
|
|
91
|
+
| `not_null_or_zero` | Item is NOT NULL or zero | the item has a value that is not zero |
|
|
92
|
+
| `not_zero` | Item is NOT zero | the item is empty or not zero |
|
|
93
|
+
| `no_spaces` | Item contains no spaces | the item has no white space |
|
|
94
|
+
| `alphanumeric` | Item is alphanumeric | the item has only letters and digits (any script) |
|
|
95
|
+
| `numeric` | Item is numeric | the item is a number (a number item's format mask is read back first) |
|
|
96
|
+
| `valid_date` | Item is a valid date | the item is a date in the application's date format, or ISO |
|
|
97
|
+
| `valid_timestamp` | Item is a valid timestamp | the item is `YYYY-MM-DD HH:MI[:SS]` (or with a `T`) |
|
|
98
|
+
| `equals` / `not_equals` | Item = Value / Item != Value | the item's value is / isn't `expression` |
|
|
99
|
+
| `in_list` / `not_in_list` | Item is (NOT) contained in Value | the item's value is / isn't one of `expression`'s values, colon separated (`CLERK:ANALYST`) |
|
|
100
|
+
| `chars_only` / `chars_any` / `chars_none` | Item contains only / at least one / none of the characters in Value | checked against the characters of `expression` |
|
|
101
|
+
| `regex` | Item matches Regular Expression | the item's value matches `expression` (a POSIX regular expression, checked in Postgres) |
|
|
102
|
+
|
|
103
|
+
The format checks (`no_spaces` … `valid_timestamp`, `chars_*`, `regex`, `not_zero`, `not_in_list`,
|
|
104
|
+
`not_equals`) pass an empty item, as APEX's do: add `not_null` (or *Value required*) when it must
|
|
105
|
+
have one. The database refuses a type without the associated item or expression it needs. The SQL
|
|
106
|
+
types run as the application's role; the [Advisor](03-builder.md#advisor) names SQL validations a type
|
|
107
|
+
says exactly (`:P3_SAL is not null` → `not_null`).
|
|
87
108
|
|
|
88
109
|
Plus: every **required** editable item must have a value.
|
|
89
110
|
|
|
@@ -92,6 +113,10 @@ Plus: every **required** editable item must have a value.
|
|
|
92
113
|
| `item_name` | The message is shown next to this item (otherwise at the top of the page) |
|
|
93
114
|
| `message` | The error message |
|
|
94
115
|
| `when_button` | Only validate for this request. A `DELETE` request runs only the validations with `when_button` `DELETE` (e.g. "approved requests can't be deleted"), not the item checks |
|
|
116
|
+
| `condition_type`, `condition_expr`, `condition_value` | Only validate when the [server-side condition](#server-side-conditions) holds, e.g. `request_in` `SAVE,CREATE` (sprint 154) |
|
|
117
|
+
| `authz` | Only validate for users the authorization scheme lets through (sprint 155) |
|
|
118
|
+
| `always_execute` | Also validate for a `DELETE` request (APEX: *Always Execute*) |
|
|
119
|
+
| `display_location` | Where the error shows: `inline_and_notification` (next to the item and at the top), `inline` (empty: next to the item, with "correct the errors below" at the top) or `notification` (at the top only). Without an associated item it is at the top |
|
|
95
120
|
|
|
96
121
|
Example: commission only for salesmen.
|
|
97
122
|
|
|
@@ -122,7 +147,7 @@ two combine well; see error handling below.
|
|
|
122
147
|
|---|---|
|
|
123
148
|
| `point` | `submit` (after validations) or `load` (when the page is shown, after form fetch) |
|
|
124
149
|
| `when_button` | Only for this request (empty = every submit with a button) |
|
|
125
|
-
| `condition_type`, `condition_expr`, `condition_value` | Only when the [condition](#
|
|
150
|
+
| `condition_type`, `condition_expr`, `condition_value` | Only when the [condition](#server-side-conditions) holds (server-side condition, as for computations and branches) |
|
|
126
151
|
| `parent_process` | The name of a `chain` process on the page: this process then runs only inside that chain |
|
|
127
152
|
| `region_id` | The form or grid region, for `form_dml` / `grid_dml` |
|
|
128
153
|
| `config` | Settings of a `data_load`, `invoke_api`, `download`, `chain`, `workflow`, `send_push` or `plugin` process (JSON) |
|
|
@@ -393,6 +418,23 @@ queued in the page's transaction and sent after the submit commits; a user witho
|
|
|
393
418
|
nothing (no error). The application needs push notifications on (Settings → Progressive Web App).
|
|
394
419
|
From SQL the same is `meta.send_push(...)` ([chapter 9](09-reference.md#push-notifications-from-sql)).
|
|
395
420
|
|
|
421
|
+
### Declarative process types
|
|
422
|
+
|
|
423
|
+
Sprint 157 (APEX: page process types). Each type calls what pgkiln already has; its settings go in
|
|
424
|
+
the process's attributes (`config`), checked when it is saved and again when it runs.
|
|
425
|
+
|
|
426
|
+
| `type` | APEX | Settings | What it does |
|
|
427
|
+
|---|---|---|---|
|
|
428
|
+
| `close_dialog` | Close Dialog | `{"items": ["P5_ID"]}` | Closes the modal page after the submit (a modal page closes after a submit anyway; this one also hands the items' values to the page below, where items of the same name take them before its [Dialog Closed](07-dynamic-actions.md#dialog-closed) actions run) |
|
|
429
|
+
| `clear_session_state` | Clear Session State | `{"items": ["P3_NAME"], "pages": [3, 4], "all": false}` | Empties the items, every `P<n>_` item of the pages, or all page and application items |
|
|
430
|
+
| `reset_pagination` | Reset Pagination | – | Reports and grids start on their first page after the submit |
|
|
431
|
+
| `user_preferences` | User Preferences | `{"action": "set", "preference": "DEFAULT_DEPT", "item": "P1_DEPT"}`, or `"reset"` | Stores (or removes) a [user preference](#user-preferences); nothing for the public user |
|
|
432
|
+
| `server_geocoding` | Server Side Geocoding | `{"address_item": "P3_ADDRESS", "coordinates_item": "P3_LOCATION", "country": "nl"}` | The installation's geocoder (`GEOCODER_URL`); the first place's `lat,lng` goes into the coordinates item |
|
|
433
|
+
| `print_report` | Print Report | `{"document": "EMPLOYEE_SHEET"}` | A [document template](16-files.md#document-templates) or report query, sent as the answer to the submit |
|
|
434
|
+
| `human_task_create` | Human Task – Create | `{"definition": "LEAVE_APPROVAL", "detail_pk": "&P7_ID.", "parameters": {"DAYS": "&P7_DAYS."}, "id_item": "P7_TASK_ID"}` | Creates a task ([approvals](#approvals-and-the-task-list)) and puts its id in the item |
|
|
435
|
+
| `human_task_manage` | Human Task – Manage | `{"action": "approve", "task": "&P8_TASK_ID.", "comment": "&P8_COMMENT."}` | `approve`, `reject`, `complete` (with `"outcome"`), `claim`, `release`, `delegate` (with `"to"`) or `cancel`; the task functions decide who may |
|
|
436
|
+
| `form_init` | Form – Initialization | – | Nothing: a form region fetches its row by itself. Kept so imported pages show it |
|
|
437
|
+
|
|
396
438
|
### Generate text with AI
|
|
397
439
|
|
|
398
440
|
An `ai_generate` process (APEX: *Generate Text with AI*) sends a prompt to a large language model
|
|
@@ -489,7 +531,7 @@ looked up by SQL, a derived value.
|
|
|
489
531
|
| `point` | `before_header`: when the page is shown (after the form fetch, before the `load` processes). `after_submit`: after the posted values are stored, before the validations |
|
|
490
532
|
| `type` | How `expression` is read, see below |
|
|
491
533
|
| `expression` | The value, item, query, expression or function body |
|
|
492
|
-
| `condition_type`, `condition_expr`, `condition_value` | Only when the [condition](#
|
|
534
|
+
| `condition_type`, `condition_expr`, `condition_value` | Only when the [condition](#server-side-conditions) holds |
|
|
493
535
|
| `authz` | Only for users who pass this authorization scheme |
|
|
494
536
|
| `seq` | Order; later computations see the values of earlier ones |
|
|
495
537
|
|
|
@@ -545,7 +587,7 @@ dialog closes instead, and the calling page reloads.
|
|
|
545
587
|
| `target_url` | A path inside the application (after `/a/<alias>/`), e.g. `12?view=month` or `account`; `&ITEM.` values are URL-encoded. Other sites are refused (no scheme, `//`, `\` or `..`) |
|
|
546
588
|
| `target_function` | `function`: a PL/pgSQL function body that returns such a path (APEX: *Function returning a URL*), run as the application's role with binds; checked like `target_url` (`meta.branch_path_ok`). An empty result: the branch doesn't apply; a refused one shows a message and is logged |
|
|
547
589
|
| `target_app` | `app`: the alias of another application of this installation (APEX: *Branch to page in another application*). It must exist with that page (else the branch doesn't apply and a message is shown); the items are signed for that application, page and user. That application's own sign-in and authorization apply when the browser gets there |
|
|
548
|
-
| `condition_type`, `condition_expr`, `condition_value` | Only when the [condition](#
|
|
590
|
+
| `condition_type`, `condition_expr`, `condition_value` | Only when the [condition](#server-side-conditions) holds |
|
|
549
591
|
| `authz`, `seq`, `build_option` | As for other components |
|
|
550
592
|
|
|
551
593
|
A `before_header` branch to the page itself is skipped (it would loop).
|
|
@@ -560,18 +602,47 @@ begin
|
|
|
560
602
|
end
|
|
561
603
|
```
|
|
562
604
|
|
|
563
|
-
###
|
|
605
|
+
### Server-side conditions
|
|
606
|
+
|
|
607
|
+
Every component has the same server-side condition (sprint 154; APEX: *Server-side Condition*):
|
|
608
|
+
regions, items, buttons, dynamic actions, validations, processes, computations, branches and
|
|
609
|
+
application computations. In the Page Designer it is the **Server-side condition** group:
|
|
610
|
+
*Condition type*, *Expression / item* and *Value*.
|
|
564
611
|
|
|
565
612
|
| `condition_type` | Holds when |
|
|
566
613
|
|---|---|
|
|
567
|
-
| (empty) | always |
|
|
568
|
-
| `sql` |
|
|
569
|
-
| `exists` / `not_exists` | the query in
|
|
570
|
-
| `
|
|
571
|
-
| `
|
|
572
|
-
| `
|
|
573
|
-
|
|
574
|
-
|
|
614
|
+
| (empty) | always (a region's or button's `condition` without a type: the SQL below) |
|
|
615
|
+
| `sql` | the expression, a boolean SQL expression, is true |
|
|
616
|
+
| `exists` / `not_exists` | the query in the expression returns a row / no row |
|
|
617
|
+
| `function_body` | the PL/pgSQL body in the expression returns true (`return :P3_QTY::int > 0;`) |
|
|
618
|
+
| `request_in` / `request_not_in` | the request (the button pressed) is / is not one of the value's names, comma separated (`SAVE,CREATE`) |
|
|
619
|
+
| `item_null` / `item_not_null` | the item named in the expression is empty / has a value |
|
|
620
|
+
| `item_zero` / `item_not_zero` | that item is / is not zero (an empty item is not zero) |
|
|
621
|
+
| `item_null_or_zero` / `item_not_null_or_zero` | that item is empty or zero / neither |
|
|
622
|
+
| `item_equals` / `item_not_equals` | that item's value is / is not the value |
|
|
623
|
+
| `item_equals_item` / `item_not_equals_item` | that item's value is / is not the value of the item named in the value |
|
|
624
|
+
| `item_in_list` / `item_not_in_list` | that item's value is / is not one of the value's entries, separated by colons (`CLERK:ANALYST`) |
|
|
625
|
+
| `page_in` / `page_not_in` | the current page is / is not one of the value's page numbers (`1,3`) |
|
|
626
|
+
| `page_is_dialog` / `page_is_not_dialog` | the page is / is not a modal dialog |
|
|
627
|
+
| `user_signed_in` / `user_public` | the user is signed in / is the public user |
|
|
628
|
+
| `language_in` / `language_not_in` | the application's language for this request is / is not one of the value's codes (`de,pt-br`; `pt` also matches `pt-br`) |
|
|
629
|
+
| `always` / `never` | always / never (for a component you want to keep but switch off: a build option does it for a release) |
|
|
630
|
+
|
|
631
|
+
Only `sql`, `exists`, `not_exists` and `function_body` run SQL, as the application's database
|
|
632
|
+
role, in the request's transaction; the others are decided by the server without a query. The
|
|
633
|
+
database refuses a condition whose expression or value doesn't fit its type (`meta.condition_ok`).
|
|
634
|
+
|
|
635
|
+
A condition whose SQL fails counts as false and shows the error. Regions and buttons keep their
|
|
636
|
+
`condition` column as the expression: without a type it is SQL, as before, so older exports keep
|
|
637
|
+
working. Dynamic actions name theirs `server_condition_type`, `server_condition_expr` and
|
|
638
|
+
`server_condition_value` (their `condition_type` is the client-side condition): when it doesn't
|
|
639
|
+
hold, the dynamic action isn't on the page.
|
|
640
|
+
|
|
641
|
+
The Page Designer's tree shows the condition in words on the component's filter badge (*When
|
|
642
|
+
P3_JOB is one of CLERK, ANALYST*), in the builder's language. The
|
|
643
|
+
[Advisor](03-builder.md#advisor) names SQL conditions a type can say exactly (`:P3_ID is not null`,
|
|
644
|
+
`:REQUEST in ('SAVE', 'CREATE')`, `:P3_JOB = 'CLERK'`) as information; `<>` and `not in` stay SQL,
|
|
645
|
+
because in SQL an empty item makes them false.
|
|
575
646
|
|
|
576
647
|
**HR example, page 22 (Leave planner).** Computations fill the employee (yours, when none is
|
|
577
648
|
chosen: `sql_query` with condition `item_null`), the name (`function_body`) and the pending
|
|
@@ -1099,6 +1170,22 @@ background runs with the tenant of the session that queued it. Filter your own t
|
|
|
1099
1170
|
(`where tenant_id = meta.tenant_id()`, or a row level security policy). Use row level security in
|
|
1100
1171
|
the tenant's tables as well: the tenant is a value the application sets, not a database boundary.
|
|
1101
1172
|
|
|
1173
|
+
### Workflow diagram region
|
|
1174
|
+
|
|
1175
|
+
Sprint 160 (APEX 24.2+: Workflow Diagram region). A region of type `workflow_diagram` draws one
|
|
1176
|
+
workflow instance in a running application: its version's steps, the ones it went through (green),
|
|
1177
|
+
where it is now (blue, several with parallel branches) and a failed step (red, with the error above
|
|
1178
|
+
the diagram). Each reached step's tooltip lists when it was reached and what happened. The instance
|
|
1179
|
+
id comes from an item:
|
|
1180
|
+
|
|
1181
|
+
```json
|
|
1182
|
+
{"instance_item": "P9_WORKFLOW_ID"}
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
It is read through `meta.workflows` as the application's role, so a user sees only workflows they
|
|
1186
|
+
started or administer (the Workflow Console's rule); others see the empty text. The diagram is SVG
|
|
1187
|
+
drawn on the server: it needs no JavaScript.
|
|
1188
|
+
|
|
1102
1189
|
### Send e-mail steps
|
|
1103
1190
|
|
|
1104
1191
|
A `send_mail` step (APEX 26.1: the *Send E-Mail* activity) queues an e-mail through the application's mail service,
|
|
@@ -12,7 +12,8 @@ your own from a static application file.
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| `event` | When it runs, see [Events](#events) |
|
|
14
14
|
| `trigger_element` | What it listens to: item names (`change`, `focus`, `blur`, `key_release`), a button name (`click`), region ids or static ids (`after_refresh`, `selection_change`), dialog page numbers (`dialog_closed`; empty: any dialog) or event names (`custom`), comma separated. Empty for `load` and `before_submit` |
|
|
15
|
-
| `condition_type`, `condition_value` | Optional client-side condition on the (first) trigger item's value (for `dialog_closed`: on the dialog's page number): `equals`, `not_equals`, `in_list` (comma-separated values), `is_null`, `is_not_null` |
|
|
15
|
+
| `condition_type`, `condition_value` | Optional client-side condition on the (first) trigger item's value (for `dialog_closed`: on the dialog's page number): `equals`, `not_equals`, `greater`, `greater_equal`, `less`, `less_equal` (numbers as numbers, other values such as ISO dates as text; an empty value fails them), `in_list`, `not_in_list` (comma-separated values), `is_null`, `is_not_null`. APEX's *JavaScript expression* isn't offered: the Content-Security-Policy runs no code from strings; use `execute_javascript` with a registered function |
|
|
16
|
+
| `server_condition_type`, `server_condition_expr`, `server_condition_value` | A [server-side condition](06-processing.md#server-side-conditions) (sprint 154): when it doesn't hold for this request, the dynamic action isn't on the page at all, e.g. `user_signed_in` or `page_is_dialog` |
|
|
16
17
|
| `action` | What to do, see below |
|
|
17
18
|
| `affected_items`, `affected_region_id` | What it acts on |
|
|
18
19
|
| `code` | SQL for server-side actions; the function's name for `execute_javascript` |
|
|
@@ -38,6 +39,14 @@ your own from a static application file.
|
|
|
38
39
|
| `before_submit` | The page is about to be submitted; a declined `confirm` keeps the page | – |
|
|
39
40
|
| `custom` | JavaScript raised the event: `pgkiln.trigger('ORDER_SAVED', value)` (APEX: custom events, `apex.event.trigger`) | Event names (case-insensitive) |
|
|
40
41
|
| `menu_entry` | An entry `{"label": …, "action": "NAME"}` of a [menu button](06-processing.md#menu-buttons-and-badges) was chosen (APEX: Menu entry chosen); the condition sees the entry's name ([below](#menu-entry-chosen)) | Entry names, or menu button names (any of their entries) |
|
|
42
|
+
| `double_click` | An item, button or region was double-clicked (sprint 156) | Item names, button names, region ids or static ids |
|
|
43
|
+
| `key_down` / `key_press` | A key goes down in an item / a key that types a character (APEX: Key Down, Key Press); a `cancel_event` keeps the key out of the field | Item names |
|
|
44
|
+
| `mouse_enter` / `mouse_leave` | The pointer enters / leaves an item, button or region (once per element, not while moving inside it) | Item names, button names, region ids or static ids |
|
|
45
|
+
| `select` | Text is selected in an item | Item names |
|
|
46
|
+
| `resize` / `scroll` | The window is resized / scrolled (at most once per frame) | – |
|
|
47
|
+
| `page_unload` | The page is left (APEX: Page Unload); only quick actions in the browser finish | – |
|
|
48
|
+
| `before_refresh` | A region is about to be refreshed by a `refresh_region` action (APEX: Before Refresh); a `cancel_event` or declined `confirm` keeps it as it is | Region ids or static ids |
|
|
49
|
+
| `facets_change` | A faceted search or smart filters region's facets were applied (APEX: Facets Change); runs on the page that comes back with the new filters | The facets region's id or static id |
|
|
41
50
|
|
|
42
51
|
**The actions of one event and trigger run one after the other, in sequence order** (APEX: the true
|
|
43
52
|
actions of a dynamic action), each waiting for the one before (a server action for its answer). A
|
|
@@ -82,6 +91,12 @@ actions of a dynamic action), each waiting for the one before (a server action f
|
|
|
82
91
|
| `share` | browser | The device's share sheet with the page's title, `message` and address; where there is none, the address is copied |
|
|
83
92
|
| `wait_popup` | browser | Shows a modal "please wait" popup with `message` (default *Please wait…*) until `remove_wait_popup` or the next page (APEX: Show Wait Popup); [below](#wait-popup) |
|
|
84
93
|
| `remove_wait_popup` | browser | Removes the wait popup (APEX: Remove Wait Popup) |
|
|
94
|
+
| `cancel_event` | browser | Stops the actions after it, and the event's own effect: a button doesn't submit or follow its link, a typed key (`key_down`, `key_press`) stays out of the field, a refresh doesn't happen (APEX: Cancel Event). Sprint 156 |
|
|
95
|
+
| `set_style` | browser | Sets CSS properties on the affected items or region: `{"styles": {"color": "#b42318", "font-weight": "bold"}}` in the attributes (APEX: Set Style). Properties from a fixed list (colours, font, text, border, opacity, visibility, padding, margin, width); values without `url()` or `var()`, checked when saved and again before they reach the page |
|
|
96
|
+
| `collapse_tree` / `expand_tree` | browser | Closes / opens every node of the affected tree region (APEX: Collapse Tree, Expand Tree) |
|
|
97
|
+
| `show_ai_assistant` | browser | Opens the affected AI assistant region (or the inline dialog it sits in) and puts the cursor in its question (APEX: Show AI Assistant) |
|
|
98
|
+
| `trigger_geocoding` | browser | The affected geocoded address item looks its text up, as its *Check address* button does (APEX: Trigger Geocoding) |
|
|
99
|
+
| `open_report_dialog` | browser | Opens a dialog of the affected interactive report: `code` is `filter`, `columns`, `sort`, `highlight`, `compute`, `aggregate`, `chart`, `group`, `pivot`, `break`, `rows` or `saved` (APEX: Invoke Interactive Report Dialog) |
|
|
85
100
|
|
|
86
101
|
Server-side actions first store `items_to_submit` in session state, run as the application's
|
|
87
102
|
database role like everything else, and check the page's and the dynamic action's authorization.
|
|
@@ -38,6 +38,13 @@ What's missing is listed in the [feature parity matrix](../apex-feature-parity.m
|
|
|
38
38
|
| Before-header processes | Process `point = 'load'` |
|
|
39
39
|
| Page computations (static, item, SQL query, SQL expression, PL/SQL function body) | Computations `before_header` / `after_submit`; function bodies are PL/pgSQL ([chapter 6](06-processing.md#computations)) |
|
|
40
40
|
| Branches (page or URL, *When button pressed*, server-side condition), before header and after processing | Branches with `when_button` and a condition; URLs stay inside the application ([chapter 6](06-processing.md#branches)) |
|
|
41
|
+
| Validation types (Rows returned, Expression, Function Body returning Boolean / Error Text, Item is NOT NULL / numeric / a valid date …, Item = Value, Item in Value, character checks, Regular Expression), Always Execute, Display Location | The same types as `type` (`exists`, `sql`, `function_body`, `function_error`, `not_null`, `numeric`, `valid_date`, `equals`, `in_list`, `chars_only`, `regex` …), `always_execute`, `display_location` ([chapter 6](06-processing.md#validations)). Interactive grid column validations aren't there yet: use a validation on the grid's process or a check constraint |
|
|
42
|
+
| Dynamic action events (Double Click, Key Down / Press, Mouse Enter / Leave, Select, Resize, Scroll, Page Unload, Before Refresh, Facets Change), conditions (greater / less than, not in list) and actions (Cancel Event, Set Style, Collapse / Expand Tree, Show AI Assistant, Trigger Geocoding, Invoke Interactive Report Dialog) | The same events, conditions and actions ([chapter 7](07-dynamic-actions.md)). Not yet: grid, calendar, map and tree component events; JavaScript expression conditions (use a registered function) |
|
|
43
|
+
| Workflow Diagram region | `workflow_diagram` with `{"instance_item": …}` ([chapter 6](06-processing.md#workflow-diagram-region)) |
|
|
44
|
+
| Map layers Points, Lines, Polygons, Heat Map, Extruded Polygons | `"layer"`: `markers`, `lines`, `polygons`, `heat`, `extruded` (drawn flat, shaded by height), with fill and line colours, width and opacity per layer or per row ([chapter 4](04-pages-and-regions.md#map)) |
|
|
45
|
+
| Items Image Upload, Text Field with autocomplete, Percent Graph, Display Map | `image_upload`, `autocomplete`, `percent_graph`, `display_map` ([chapter 5](05-items.md#item-types)) |
|
|
46
|
+
| Process types Close Dialog, Clear Session State, Reset Pagination, User Preferences, Server Side Geocoding, Print Report, Human Task – Create / – Manage, Form – Initialization | The same as `type` ([chapter 6](06-processing.md#declarative-process-types)), settings in the attributes |
|
|
47
|
+
| Server-side Condition (Rows returned, Expression, Function Body, Request = / != / is contained in, Item is NULL / NOT NULL / zero / = Value / in colon list, Current page in, Current page is a modal dialog, User is authenticated / public user, Current language in, Always / Never …) | The same list as `condition_type`: `exists`, `sql`, `function_body`, `request_in` / `request_not_in`, `item_null` / `item_not_null` / `item_zero` / `item_equals` / `item_in_list`, `page_in`, `page_is_dialog`, `user_signed_in` / `user_public`, `language_in`, `always` / `never` ([chapter 6](06-processing.md#server-side-conditions)). APEX's colon lists stay colon lists; *Request is contained in* takes commas |
|
|
41
48
|
| Build options (include / exclude) | Build options; `!NAME` for APEX's "exclude when the option is included" ([chapter 6](06-processing.md#build-options)) |
|
|
42
49
|
| Menu button, button badge | Button action `menu`; `badge` / `badge_query` |
|
|
43
50
|
| Dynamic actions *Set Focus*, *Add / Remove Class*, *Show Success / Error Message*, *Clear Errors* | `set_focus`, `add_class` / `remove_class`, `show_success` / `show_error`, `clear_errors` |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 79: declarative server-side conditions (sprint 154, CD-2)
|
|
3
|
+
--
|
|
4
|
+
-- The Employee page's buttons showed with SQL conditions such as
|
|
5
|
+
-- ":P3_EMPNO is not null". The same tests as condition types: no query,
|
|
6
|
+
-- and the Page Designer's tree says "When P3_EMPNO is not empty".
|
|
7
|
+
-- Department (page 5) gets the same; the leave request's Approve and
|
|
8
|
+
-- Reject (page 7) stay SQL: they call hr.current_empno().
|
|
9
|
+
-- =====================================================================
|
|
10
|
+
|
|
11
|
+
update meta.button b
|
|
12
|
+
set condition_type = case when b.condition ~* ' is not null$' then 'item_not_null' else 'item_null' end,
|
|
13
|
+
condition = substring(b.condition from '^:([A-Z][A-Z0-9_]*) ')
|
|
14
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
15
|
+
where b.page_id = p.id and a.alias = 'hr' and p.page_no in (3, 5)
|
|
16
|
+
and b.condition_type is null
|
|
17
|
+
and b.condition ~ '^:P[35]_(EMPNO|DEPTNO) is (not )?null$';
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -1497,6 +1497,8 @@ path.map-near { stroke: #1f2733; stroke-dasharray: 6 4; fill: #1f2733; fill-opac
|
|
|
1497
1497
|
.wf-invoke_api rect { stroke: #1f7a8c; stroke-width: 2; stroke-dasharray: 2 2; }
|
|
1498
1498
|
.wf-active rect { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 3; }
|
|
1499
1499
|
.wf-active .wf-name { fill: var(--text); }
|
|
1500
|
+
.wf-done rect { fill: var(--success-soft, #e7f5ed); stroke: var(--success, #17794a); }
|
|
1501
|
+
.wf-failed rect { fill: var(--danger-soft, #fde7e4); stroke: var(--danger, #b42318); stroke-width: 3; }
|
|
1500
1502
|
.wf-warnings { background: var(--warning-soft); color: var(--text); border-color: var(--border); }
|
|
1501
1503
|
.wf-version-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0; }
|
|
1502
1504
|
.wf-version-diagram summary { cursor: pointer; }
|
|
@@ -2251,3 +2253,13 @@ body.nav-pillar-dark .t-nav details:has(> summary.in-trail) > ul { border-inline
|
|
|
2251
2253
|
.mfa-factors { list-style: none; padding: 0; margin: 0 0 .75rem; }
|
|
2252
2254
|
.mfa-factors li { padding: .4rem 0; border-bottom: 1px solid var(--border); }
|
|
2253
2255
|
@media (max-width: 480px) { .mfa-codes { columns: 1; } }
|
|
2256
|
+
|
|
2257
|
+
/* (sprint 158) Percent Graph and Display Map items */
|
|
2258
|
+
.percent-graph { display: flex; align-items: center; gap: .5rem; }
|
|
2259
|
+
.percent-graph progress { flex: 1; max-width: 16rem; height: .75rem; accent-color: var(--t-accent, #2563eb); }
|
|
2260
|
+
.percent-graph-value { font-variant-numeric: tabular-nums; min-width: 3.5ch; text-align: right; }
|
|
2261
|
+
.display-map { margin: 0; display: inline-block; max-width: 100%; }
|
|
2262
|
+
.display-map-tile { position: relative; width: 256px; max-width: 100%; aspect-ratio: 1; overflow: hidden; border-radius: 6px; border: 1px solid var(--t-border, #d0d5dd); }
|
|
2263
|
+
.display-map-tile img { display: block; width: 100%; height: 100%; }
|
|
2264
|
+
.display-map-pin { position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%; background: #d92d20; border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.35); }
|
|
2265
|
+
.display-map figcaption { font-size: .8rem; margin-top: .25rem; }
|
package/public/app.js
CHANGED
|
@@ -341,9 +341,17 @@ const pgkilnUnsaved = (() => {
|
|
|
341
341
|
case 'in_list': return da.cond.value.split(',').map((s) => s.trim()).includes(v);
|
|
342
342
|
case 'is_null': return v === '';
|
|
343
343
|
case 'is_not_null': return v !== '';
|
|
344
|
+
// (156) numbers compared as numbers, other values (ISO dates) as text
|
|
345
|
+
case 'greater': return v !== '' && compare(v, da.cond.value) > 0;
|
|
346
|
+
case 'greater_equal': return v !== '' && compare(v, da.cond.value) >= 0;
|
|
347
|
+
case 'less': return v !== '' && compare(v, da.cond.value) < 0;
|
|
348
|
+
case 'less_equal': return v !== '' && compare(v, da.cond.value) <= 0;
|
|
349
|
+
case 'not_in_list': return !da.cond.value.split(',').map((s) => s.trim()).includes(v);
|
|
344
350
|
default: return true;
|
|
345
351
|
}
|
|
346
352
|
}
|
|
353
|
+
const NUM = /^\s*-?\d+(\.\d+)?\s*$/;
|
|
354
|
+
const compare = (a, b) => (NUM.test(a) && NUM.test(b) ? Number(a) - Number(b) : a < b ? -1 : a > b ? 1 : 0);
|
|
347
355
|
|
|
348
356
|
// ------------------------------------------------------------ messages and errors (dynamic actions)
|
|
349
357
|
// class names a dynamic action may add or remove (the database checks the same pattern)
|
|
@@ -499,6 +507,8 @@ const pgkilnUnsaved = (() => {
|
|
|
499
507
|
|
|
500
508
|
/** (085) what runDa returns to stop the actions after it (a declined confirm) */
|
|
501
509
|
const STOP = Symbol('stop');
|
|
510
|
+
/** (156) does a Cancel Event in these actions hold its condition (decided at once, so the event's effect can be stopped)? */
|
|
511
|
+
const cancels = (list) => list.some((da) => da.action === 'cancel_event' && conditionHolds(da));
|
|
502
512
|
const text = (k) => (meta.texts && meta.texts[k]) || k;
|
|
503
513
|
|
|
504
514
|
/** The actions of one event and trigger, one after the other in sequence order; false when one stopped them. */
|
|
@@ -580,6 +590,37 @@ const pgkilnUnsaved = (() => {
|
|
|
580
590
|
}
|
|
581
591
|
if (initial || !holds) return;
|
|
582
592
|
switch (da.action) {
|
|
593
|
+
// (156) stops the actions after it; the triggering event's own effect is stopped by its handler (cancels())
|
|
594
|
+
case 'cancel_event':
|
|
595
|
+
return STOP;
|
|
596
|
+
case 'set_style':
|
|
597
|
+
for (const el of targets(da)) for (const [k, v] of Object.entries(da.styles || {})) el.style.setProperty(k, v);
|
|
598
|
+
return;
|
|
599
|
+
case 'collapse_tree':
|
|
600
|
+
case 'expand_tree':
|
|
601
|
+
for (const el of targets(da)) el.querySelectorAll('.tree-view details').forEach((d) => (d.open = da.action === 'expand_tree'));
|
|
602
|
+
return;
|
|
603
|
+
case 'show_ai_assistant':
|
|
604
|
+
for (const el of targets(da)) {
|
|
605
|
+
const dlg = el.closest('dialog[data-inline-region]') || (el.matches('dialog[data-inline-region]') ? el : null);
|
|
606
|
+
if (dlg && !dlg.open) dlg.showModal();
|
|
607
|
+
el.hidden = false;
|
|
608
|
+
const details = el.querySelector(':scope > details');
|
|
609
|
+
if (details) details.open = true;
|
|
610
|
+
const input = el.querySelector('textarea, input[type=text]') || (el.id && document.querySelector(`form[id^="${el.id}"] textarea`));
|
|
611
|
+
if (input) input.focus();
|
|
612
|
+
else el.scrollIntoView({ block: 'nearest' });
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
case 'trigger_geocoding':
|
|
616
|
+
for (const n of da.items) document.querySelector(`[data-geocode="${CSS.escape(n)}"]`)?.click();
|
|
617
|
+
return;
|
|
618
|
+
case 'open_report_dialog':
|
|
619
|
+
for (const el of targets(da)) {
|
|
620
|
+
const item = da.dialog && el.querySelector(`.ir-item[data-ir-dialog="${CSS.escape(da.dialog)}"]`);
|
|
621
|
+
if (item) item.open = true;
|
|
622
|
+
}
|
|
623
|
+
return;
|
|
583
624
|
case 'alert':
|
|
584
625
|
window.alert(da.message || '');
|
|
585
626
|
return;
|
|
@@ -698,6 +739,12 @@ const pgkilnUnsaved = (() => {
|
|
|
698
739
|
return;
|
|
699
740
|
}
|
|
700
741
|
}
|
|
742
|
+
// (156) before_refresh: the region's actions run before its refresh is asked for
|
|
743
|
+
if (da.action === 'refresh_region' && da.region) {
|
|
744
|
+
const r = document.getElementById(`R${da.region}`);
|
|
745
|
+
const before = regionRun('before_refresh', r);
|
|
746
|
+
if (before.length && !(await runChain(before))) return;
|
|
747
|
+
}
|
|
701
748
|
const data = { ...(extra || {}) };
|
|
702
749
|
for (const n of da.submit) data[n] = itemValue(n);
|
|
703
750
|
const busy = targets(da);
|
|
@@ -727,6 +774,12 @@ const pgkilnUnsaved = (() => {
|
|
|
727
774
|
}
|
|
728
775
|
|
|
729
776
|
const das = meta.das || [];
|
|
777
|
+
// region events (trigger: region ids or static ids, case-insensitive)
|
|
778
|
+
const regionRun = (event, region) => {
|
|
779
|
+
if (!region || !/^R\d+$/.test(region.id || '')) return [];
|
|
780
|
+
const keys = [region.id.slice(1), (region.dataset.staticId || '').toLowerCase()].filter(Boolean);
|
|
781
|
+
return das.filter((da) => da.event === event && da.trigger.some((t) => keys.includes(String(t).toLowerCase())));
|
|
782
|
+
};
|
|
730
783
|
// (121) an AI agent's javascript tools: after the answer, the registered function of the action
|
|
731
784
|
// (pgkiln.actions.register) and the dynamic actions of the custom event of that name, with the arguments
|
|
732
785
|
scriptsLoaded.then(async () => {
|
|
@@ -760,6 +813,8 @@ const pgkilnUnsaved = (() => {
|
|
|
760
813
|
const btn = e.target.closest('[data-button]');
|
|
761
814
|
if (!btn || e.target.closest('[data-menu-entry]')) return;
|
|
762
815
|
const run = das.filter((da) => da.event === 'click' && da.trigger.includes(btn.dataset.button));
|
|
816
|
+
// (156) a Cancel Event whose condition holds: the button doesn't submit or follow its link
|
|
817
|
+
if (cancels(run)) e.preventDefault();
|
|
763
818
|
// "Generate text with AI" on a submit button: the AJAX call replaces the submit
|
|
764
819
|
// (without JavaScript the button submits and the page process runs instead)
|
|
765
820
|
const ai = run.some((da) => da.action === 'ai_generate');
|
|
@@ -803,12 +858,68 @@ const pgkilnUnsaved = (() => {
|
|
|
803
858
|
document.addEventListener('focusin', itemEvent('focus'));
|
|
804
859
|
document.addEventListener('focusout', itemEvent('blur'));
|
|
805
860
|
document.addEventListener('keyup', itemEvent('key_release'));
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
861
|
+
// (156) key_down, key_press (a key that types a character), select (text selected in an item)
|
|
862
|
+
document.addEventListener('keydown', (e) => {
|
|
863
|
+
const name = e.target && e.target.name;
|
|
864
|
+
if (!name) return;
|
|
865
|
+
const down = das.filter((da) => da.event === 'key_down' && da.trigger.includes(name));
|
|
866
|
+
const press = e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey ? das.filter((da) => da.event === 'key_press' && da.trigger.includes(name)) : [];
|
|
867
|
+
// a Cancel Event on key_down / key_press keeps the key out of the field
|
|
868
|
+
if (cancels([...down, ...press])) e.preventDefault();
|
|
869
|
+
if (down.length) runChain(down);
|
|
870
|
+
if (press.length) runChain(press);
|
|
871
|
+
});
|
|
872
|
+
document.addEventListener('select', itemEvent('select'), true);
|
|
873
|
+
// double_click, mouse_enter, mouse_leave: an item (its name), a button (its name) or a region (id or static id)
|
|
874
|
+
const elementRun = (event, target) => {
|
|
875
|
+
if (!target || !target.closest) return [];
|
|
876
|
+
const item = target.closest('[name]');
|
|
877
|
+
const btn = target.closest('[data-button]');
|
|
878
|
+
const region = target.closest('section[id^="R"], div[id^="R"], dialog[id^="R"]');
|
|
879
|
+
const names = [item && item.name, btn && btn.dataset.button].filter(Boolean);
|
|
880
|
+
return [...new Set([...das.filter((da) => da.event === event && da.trigger.some((t) => names.includes(t))), ...regionRun(event, region)])];
|
|
811
881
|
};
|
|
882
|
+
document.addEventListener('dblclick', (e) => {
|
|
883
|
+
const run = elementRun('double_click', e.target);
|
|
884
|
+
if (run.length) runChain(run);
|
|
885
|
+
});
|
|
886
|
+
// enter / leave: once per element, not again while moving inside it
|
|
887
|
+
const hovered = new Set();
|
|
888
|
+
document.addEventListener('mouseover', (e) => {
|
|
889
|
+
for (const da of elementRun('mouse_enter', e.target)) if (!hovered.has(da)) { hovered.add(da); runChain([da]); }
|
|
890
|
+
});
|
|
891
|
+
document.addEventListener('mouseout', (e) => {
|
|
892
|
+
const leaving = elementRun('mouse_enter', e.target).concat(elementRun('mouse_leave', e.target));
|
|
893
|
+
const still = new Set([...elementRun('mouse_enter', e.relatedTarget), ...elementRun('mouse_leave', e.relatedTarget)]);
|
|
894
|
+
for (const da of leaving) {
|
|
895
|
+
if (still.has(da)) continue;
|
|
896
|
+
hovered.delete(da);
|
|
897
|
+
if (da.event === 'mouse_leave') runChain([da]);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
// resize, scroll: the window (at most one run per animation frame)
|
|
901
|
+
for (const [event, dom] of [['resize', 'resize'], ['scroll', 'scroll']]) {
|
|
902
|
+
const run = das.filter((da) => da.event === event);
|
|
903
|
+
if (!run.length) continue;
|
|
904
|
+
let queued = false;
|
|
905
|
+
window.addEventListener(dom, () => {
|
|
906
|
+
if (queued) return;
|
|
907
|
+
queued = true;
|
|
908
|
+
requestAnimationFrame(() => { queued = false; runChain(run); });
|
|
909
|
+
}, { passive: true });
|
|
910
|
+
}
|
|
911
|
+
// page_unload: the page is left (only quick, local actions finish)
|
|
912
|
+
window.addEventListener('pagehide', () => runChain(das.filter((da) => da.event === 'page_unload')));
|
|
913
|
+
// facets_change: a faceted search region's facets were applied (the page came back from its form)
|
|
914
|
+
{
|
|
915
|
+
const params = new URLSearchParams(location.search);
|
|
916
|
+
const changed = params.get('facets_changed');
|
|
917
|
+
if (changed) {
|
|
918
|
+
params.delete('facets_changed');
|
|
919
|
+
history.replaceState(history.state, '', `${location.pathname}${params.size ? `?${params}` : ''}${location.hash}`);
|
|
920
|
+
runChain(regionRun('facets_change', document.getElementById(`R${changed.replace(/\D/g, '')}`)));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
812
923
|
// after_refresh: a region replaced by a refresh, a lazy load or a master row
|
|
813
924
|
document.addEventListener('pgkiln:replaced', (e) => {
|
|
814
925
|
const run = regionRun('after_refresh', e.detail);
|
|
@@ -1011,6 +1122,10 @@ const pgkilnUnsaved = (() => {
|
|
|
1011
1122
|
const dlg = document.getElementById('t-dialog');
|
|
1012
1123
|
if (dlg) dlg.close();
|
|
1013
1124
|
if (!e.data.reload) return;
|
|
1125
|
+
// (157) items a close_dialog process returned: items of the same name on this page take their values
|
|
1126
|
+
if (e.data.items && typeof e.data.items === 'object')
|
|
1127
|
+
for (const [name, value] of Object.entries(e.data.items))
|
|
1128
|
+
if (/^[A-Z][A-Z0-9_]{0,99}$/.test(name) && typeof value === 'string' && wrapperOf(name)) setItemValue(name, value);
|
|
1014
1129
|
const page = /^\d{1,9}$/.test(String(e.data.page || '')) ? String(e.data.page) : '';
|
|
1015
1130
|
const handlers = das.filter((da) => da.event === 'dialog_closed' && (!da.trigger.length || da.trigger.includes(page)));
|
|
1016
1131
|
if (!handlers.length) return location.reload();
|
|
@@ -1248,7 +1363,10 @@ const pgkilnUnsaved = (() => {
|
|
|
1248
1363
|
|
|
1249
1364
|
// Inside a dialog: close on success or cancel.
|
|
1250
1365
|
if (window.parent !== window) {
|
|
1251
|
-
|
|
1366
|
+
// (157) a close_dialog process's items go along to the page below
|
|
1367
|
+
let items = null;
|
|
1368
|
+
try { items = body.dataset.dialogItems ? JSON.parse(body.dataset.dialogItems) : null; } catch { items = null; }
|
|
1369
|
+
const tell = (reload) => window.parent.postMessage({ type: 'pgkiln:close', reload, page: body.dataset.dialogPage || '', items }, location.origin);
|
|
1252
1370
|
if (body.dataset.dialogClose) tell(true);
|
|
1253
1371
|
document.addEventListener('click', (e) => {
|
|
1254
1372
|
if (!e.target.closest('[data-dialog-cancel]')) return;
|
|
@@ -1841,7 +1959,20 @@ document.addEventListener('change', (e) => {
|
|
|
1841
1959
|
const custom = e.target.closest('fieldset')?.querySelector('[data-facet-range]');
|
|
1842
1960
|
custom?.querySelectorAll('input').forEach((i) => (i.value = ''));
|
|
1843
1961
|
}
|
|
1844
|
-
|
|
1962
|
+
// (156) the page that comes back runs the region's facets_change dynamic actions
|
|
1963
|
+
const f = e.target.form;
|
|
1964
|
+
const region = e.target.closest('[id^="R"]');
|
|
1965
|
+
if (f && region && /^R\d+$/.test(region.id)) {
|
|
1966
|
+
let mark = f.querySelector('input[name="facets_changed"]');
|
|
1967
|
+
if (!mark) {
|
|
1968
|
+
mark = document.createElement('input');
|
|
1969
|
+
mark.type = 'hidden';
|
|
1970
|
+
mark.name = 'facets_changed';
|
|
1971
|
+
f.appendChild(mark);
|
|
1972
|
+
}
|
|
1973
|
+
mark.value = region.id.slice(1);
|
|
1974
|
+
}
|
|
1975
|
+
f?.requestSubmit();
|
|
1845
1976
|
});
|
|
1846
1977
|
document.addEventListener('input', (e) => {
|
|
1847
1978
|
const box = e.target.closest?.('[data-facet-range]');
|
|
@@ -3162,6 +3293,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3162
3293
|
}
|
|
3163
3294
|
});
|
|
3164
3295
|
|
|
3296
|
+
// (159) Leaflet path options from a shape layer's checked style
|
|
3297
|
+
function shapeStyle(st, kind) {
|
|
3298
|
+
if (!st) return {};
|
|
3299
|
+
const o = {};
|
|
3300
|
+
if (st.stroke_color) o.color = st.stroke_color;
|
|
3301
|
+
if (typeof st.stroke_width === 'number') o.weight = st.stroke_width;
|
|
3302
|
+
if (st.fill_color) { o.fillColor = st.fill_color; o.fill = kind !== 'lines'; }
|
|
3303
|
+
if (typeof st.opacity === 'number') { if (kind === 'lines') o.opacity = st.opacity; else o.fillOpacity = st.opacity; }
|
|
3304
|
+
return o;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3165
3307
|
// One layer's Leaflet parts: markers (clustered or not) or a heat map, with its lines and areas.
|
|
3166
3308
|
function layerParts(l, popup, data, several) {
|
|
3167
3309
|
const parts = [];
|
|
@@ -3170,8 +3312,14 @@ function layerParts(l, popup, data, several) {
|
|
|
3170
3312
|
else for (const p of l.points) parts.push(placeMarker(p, l, popup, several));
|
|
3171
3313
|
if (l.shapes.length)
|
|
3172
3314
|
parts.push(window.L.geoJSON({ type: 'FeatureCollection', features: l.shapes }, {
|
|
3173
|
-
|
|
3174
|
-
|
|
3315
|
+
// (159) a lines / polygons / extruded layer's colours, width and opacity (checked on the server: #rrggbb, numbers)
|
|
3316
|
+
style: (f) => ({ ...(several ? { className: `map-c${l.color}` } : {}), ...shapeStyle(f.properties && f.properties.style, l.kind) }),
|
|
3317
|
+
onEachFeature: (f, layer) => {
|
|
3318
|
+
layer.bindPopup(() => popup(f.properties));
|
|
3319
|
+
// a shape's title as its tooltip (and its height on an extruded layer)
|
|
3320
|
+
const tip = [f.properties && f.properties.title, f.properties && typeof f.properties.height === 'number' ? String(f.properties.height) : ''].filter(Boolean).join(': ');
|
|
3321
|
+
if (tip && layer.bindTooltip) layer.bindTooltip(tip, { sticky: true });
|
|
3322
|
+
},
|
|
3175
3323
|
}));
|
|
3176
3324
|
return parts;
|
|
3177
3325
|
}
|
package/src/appfiles.ts
CHANGED
|
@@ -108,11 +108,11 @@ function sniff(b: Buffer) {
|
|
|
108
108
|
// Code columns that move to a sibling file when they are long or multi-line.
|
|
109
109
|
const CODE: Record<string, Record<string, string>> = {
|
|
110
110
|
region: { source: 'sql', condition: 'sql' },
|
|
111
|
-
item: { default_value: 'sql', readonly_condition: 'sql' },
|
|
111
|
+
item: { default_value: 'sql', readonly_condition: 'sql', condition_expr: 'sql' },
|
|
112
112
|
button: { condition: 'sql', badge_query: 'sql' },
|
|
113
|
-
dynamic_action: { code: 'sql' },
|
|
114
|
-
validation: { expression: 'sql' },
|
|
115
|
-
process: { code: 'sql' },
|
|
113
|
+
dynamic_action: { code: 'sql', server_condition_expr: 'sql' },
|
|
114
|
+
validation: { expression: 'sql', condition_expr: 'sql' },
|
|
115
|
+
process: { code: 'sql', condition_expr: 'sql' },
|
|
116
116
|
computation: { expression: 'sql', condition_expr: 'sql' },
|
|
117
117
|
branch: { condition_expr: 'sql' },
|
|
118
118
|
app_process: { code: 'sql' },
|