pgkiln 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/db/migrations/155_mail_available.sql +23 -0
  2. package/docs/apex-feature-parity.md +3 -3
  3. package/docs/guide/04-pages-and-regions.md +24 -5
  4. package/docs/guide/05-items.md +20 -1
  5. package/docs/guide/06-processing.md +7 -3
  6. package/docs/guide/07-dynamic-actions.md +3 -3
  7. package/docs/guide/08-security.md +6 -2
  8. package/docs/guide/09-reference.md +1 -0
  9. package/docs/guide/11-from-apex.md +3 -0
  10. package/docs/guide/12-development.md +4 -0
  11. package/examples/gallery/README.md +23 -6
  12. package/examples/gallery/gallery.json +4 -0
  13. package/examples/gallery/samples/sample-approvals.sql +102 -11
  14. package/examples/gallery/samples/sample-calendar.sql +40 -5
  15. package/examples/gallery/samples/sample-dynamic-actions.sql +240 -9
  16. package/examples/gallery/samples/sample-grids.sql +16 -4
  17. package/examples/gallery/samples/sample-maps.sql +65 -7
  18. package/examples/gallery/samples/sample-processes.sql +466 -0
  19. package/examples/gallery/samples/sample-validations.sql +407 -0
  20. package/examples/gallery/samples/ux-patterns.sql +154 -14
  21. package/examples/hr/README.md +2 -0
  22. package/examples/hr/hr_80_notifications.sql +125 -0
  23. package/examples/hr/hr_81_vector_search_type.sql +33 -0
  24. package/package.json +1 -1
  25. package/public/app.css +10 -1
  26. package/public/app.js +30 -9
  27. package/src/builder/components.ts +1 -1
  28. package/src/builder/region-help.ts +2 -2
  29. package/src/runtime/authz.ts +7 -0
  30. package/src/runtime/engine.ts +6 -3
  31. package/src/runtime/grid.ts +69 -9
  32. package/src/runtime/items.ts +9 -1
  33. package/src/runtime/lists.ts +2 -2
  34. package/src/runtime/push.ts +7 -1
  35. package/src/runtime/regions.ts +25 -8
  36. package/src/runtime/signin.ts +5 -5
@@ -3,8 +3,8 @@
3
3
  --
4
4
  -- The interactive grid: an editable grid made by the wizard; a grid with
5
5
  -- select lists, required columns, totals, a frozen column, a row menu and a
6
- -- default column layout; a master grid with its detail grid; and a
7
- -- read-only grid with its single row, group by and chart views.
6
+ -- default column layout, new rows' default values, and dynamic actions on a
7
+ -- new row and after a save; a master grid with its detail grid; and a read-only grid with its single row, group by and chart views.
8
8
  --
9
9
  -- Installed by an administrator from the App Gallery, or with
10
10
  -- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-grids.sql
@@ -109,7 +109,7 @@ select pg_temp.page_id('sample-grids', r.page), r.seq, r.title, r.type, 12, r.te
109
109
 
110
110
  -- 3 Grid features
111
111
  (3, 5, 'About this page', 'static', 'plain',
112
- '<p class="lead">The grid''s attributes: <code>columns</code> gives the team column a list of values and makes the name required; <code>aggregates</code> adds totals over every row of the search; <code>frozen</code> keeps the name in view while the grid scrolls sideways; <code>row_actions</code> adds a menu per row (duplicate, delete); <code>layout</code> sets the default order and widths, and every user can rearrange it (Actions → Columns).</p>',
112
+ '<p class="lead">The grid''s attributes: <code>columns</code> gives the team column a list of values and makes the name required; <code>aggregates</code> adds totals over every row of the search; <code>frozen</code> keeps the name in view while the grid scrolls sideways; <code>row_actions</code> adds a menu per row (duplicate, delete); <code>layout</code> sets the default order and widths, and every user can rearrange it (Actions → Columns).</p><p>Two dynamic actions listen to the grid itself (the trigger is its static id <code>members</code>): <strong>Row initialization</strong> (<code>grid_row_init</code>) runs when <strong>Add row</strong> or Duplicate adds a row, and shows a message; the row''s role, start date and hours already have values, from the columns'' <code>default</code> (text, may use <code>&amp;ITEM.</code>) and <code>default_sql</code> (a SELECT) in <code>columns</code>. A new row left at its defaults isn''t saved; <strong>Save</strong> (<code>grid_save</code>) shows a message on the page that comes back after the grid was saved.</p>',
113
113
  null, null, '{}', null),
114
114
  (3, 10, 'Members', 'grid', 'standard',
115
115
  E'select id, name, team_id, role, starts, hours, rate, remote\n from sample_grids.member order by team_id, name',
@@ -118,7 +118,9 @@ select pg_temp.page_id('sample-grids', r.page), r.seq, r.title, r.type, 12, r.te
118
118
  "headings": {"team_id": "Team", "starts": "Started", "hours": "Hours a week", "rate": "Hourly rate"},
119
119
  "columns": {"name": {"required": true},
120
120
  "team_id": {"lov": "select name, id from sample_grids.team order by name", "required": true},
121
- "role": {"lov": "STATIC:Lead,Developer,Designer,Tester,Analyst", "required": true}},
121
+ "role": {"lov": "STATIC:Lead,Developer,Designer,Tester,Analyst", "required": true, "default": "Developer"},
122
+ "starts": {"default_sql": "select current_date"},
123
+ "hours": {"default": "40"}},
122
124
  "formats": {"rate": "999G990D00"},
123
125
  "aggregates": {"hours": ["sum", "avg"], "rate": "avg", "id": "count"},
124
126
  "frozen": 1,
@@ -149,3 +151,13 @@ select pg_temp.page_id('sample-grids', 3), 10, 'Save the members', 'grid_dml', r
149
151
  -- the detail grid of page 4: the role as a select list
150
152
  update meta.region set config = jsonb_set(config, '{columns,role}', '{"lov": "STATIC:Lead,Developer,Designer,Tester,Analyst", "required": true}')
151
153
  where page_id = pg_temp.page_id('sample-grids', 4) and seq = 20;
154
+
155
+ -- page 3: dynamic actions on the grid's own events (trigger: the grid's static id).
156
+ -- grid_row_init: a new row (Add row, Duplicate); its defaults come from the grid's columns (default, default_sql);
157
+ -- grid_save: a message on the page that comes back after a save.
158
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, code, message)
159
+ values
160
+ (pg_temp.page_id('sample-grids', 3), 10, 'A new member', 'grid_row_init', 'members', 'show_success', null,
161
+ 'A grid_row_init dynamic action: a new row. Its role, start date and hours start with the columns'' defaults; fill in the name and the team.'),
162
+ (pg_temp.page_id('sample-grids', 3), 20, 'Saved', 'grid_save', 'members', 'show_success', null,
163
+ 'A grid_save dynamic action: the members are saved, and the totals include your changes.');
@@ -4,8 +4,9 @@
4
4
  -- Map regions on Dutch museums, without PostGIS: markers with popups and
5
5
  -- clustering; several layers (markers, a route and an area as GeoJSON, a
6
6
  -- heat map that starts hidden); a map that filters the report beside it by
7
- -- distance; and a form with a location item ("Use my location") whose
8
- -- places show on a map.
7
+ -- distance; a form with a location item ("Use my location"), a geocoded
8
+ -- address and a display map, whose places show on a map; and dynamic
9
+ -- actions on a map's own events and on the device's position.
9
10
  --
10
11
  -- Installed by an administrator from the App Gallery, or with
11
12
  -- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-maps.sql
@@ -94,7 +95,7 @@ select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias =
94
95
 
95
96
  insert into meta.page (app_id, page_no, name, title, parent_page)
96
97
  select a.id, p.no, p.name, p.name, p.parent
97
- from meta.app a, (values (1, 'Sample Maps', null), (2, 'Museums', 1), (3, 'Layers', 1), (4, 'Map and list', 1)) as p (no, name, parent)
98
+ from meta.app a, (values (1, 'Sample Maps', null), (2, 'Museums', 1), (3, 'Layers', 1), (4, 'Map and list', 1), (7, 'Map events', 1)) as p (no, name, parent)
98
99
  where a.alias = 'sample-maps';
99
100
 
100
101
  -- page 5 and its form, page 6: the Report and form wizard on the places
@@ -107,7 +108,7 @@ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
107
108
  select a.id, n.seq, n.label, n.icon, n.page
108
109
  from meta.app a, (values
109
110
  (10, 'Home', 'home', 1), (20, 'Museums', 'map', 2), (30, 'Layers', 'layers', 3),
110
- (40, 'Map and list', 'list', 4), (50, 'My places', 'map', 5)
111
+ (40, 'Map and list', 'list', 4), (50, 'My places', 'map', 5), (60, 'Map events', 'bolt', 7)
111
112
  ) as n (seq, label, icon, page)
112
113
  where a.alias = 'sample-maps';
113
114
 
@@ -120,7 +121,8 @@ select a.id, 'MAP_TOPICS', e.seq, e.label, e.icon, e.page, e.description
120
121
  (10, 'Museums', 'map', 2, 'Markers from latitude and longitude columns, popups, and clusters that open as you zoom in.'),
121
122
  (20, 'Layers', 'layers', 3, 'Several queries on one map: markers, a route and an area as GeoJSON, a heat map you switch on.'),
122
123
  (30, 'Map and list', 'list', 4, 'Move the map, then show only the museums within that distance in the list beside it.'),
123
- (40, 'My places', 'map', 5, 'A form with a location item: Use my location stores latitude,longitude, the map shows them.')
124
+ (40, 'My places', 'map', 5, 'A form with a location item: Use my location stores latitude,longitude, the map shows them.'),
125
+ (50, 'Map events', 'bolt', 7, 'Dynamic actions on a marker clicked and the map moved, and the museums nearest to you.')
124
126
  ) as e (seq, label, icon, page, description)
125
127
  where a.alias = 'sample-maps';
126
128
 
@@ -164,12 +166,68 @@ select pg_temp.page_id('sample-maps', r.page), r.seq, r.title, r.type, r.cols, r
164
166
 
165
167
  -- 5 My places (the wizard's report follows at seq 10)
166
168
  (5, 5, 'About this page', 'static', 12, 'plain',
167
- '<p class="lead">The form''s <strong>Location</strong> is a location item: <em>Use my location</em> asks the browser for the device''s position and stores it as <code>latitude,longitude</code> text, which a check constraint on the column keeps in shape. The map reads the same column.</p>',
169
+ '<p class="lead">The form''s <strong>Location</strong> is a location item: <em>Use my location</em> asks the browser for the device''s position and stores it as <code>latitude,longitude</code> text, which a check constraint on the column keeps in shape. The map reads the same column.</p><p>The form also has an <strong>Address</strong> (an <code>address</code> item, not stored): its <em>Check address</em> button offers the matching addresses, and the one chosen puts its position into Location (<code>coordinates_item</code>). For a new place, naming it runs two dynamic actions: a <em>Set Value</em> copies the name into Address and <code>trigger_geocoding</code> looks it up, as if you had pressed Check address. It needs the installation''s geocoder (<code>GEOCODER_URL</code>); without one it is a plain text field. Below it, <strong>On the map</strong> is a <code>display_map</code> item: a small map of the stored location with a pin.</p>',
168
170
  '{}', null),
169
171
  (5, 20, 'On the map', 'map', 12, 'standard',
170
172
  'select id, name as title, note as body, location from sample_maps.place where location is not null',
171
- '{"link": {"page": 6, "items": {"P6_ID": "#id#"}}, "empty": "No place has a location yet."}', null)
173
+ '{"link": {"page": 6, "items": {"P6_ID": "#id#"}}, "empty": "No place has a location yet."}', null),
174
+
175
+ -- 7 Map events
176
+ (7, 5, 'About this page', 'static', 12, 'plain',
177
+ '<p class="lead">Dynamic actions can listen to the map itself (the trigger is its static id <code>events-map</code>). <strong>Map Object Clicked</strong> (<code>map_object_click</code>) sees the marker''s <code>id</code> column: a <em>Set Value</em> without SQL puts it into <code>P7_MUSEUM</code> and the region <em>Clicked museum</em> is refreshed with it. <strong>Map Changed</strong> (<code>map_change</code>) runs when you move or zoom the map and sees the zoom level, shown in <code>P7_ZOOM</code>. The button <strong>Museums near me</strong> runs <code>current_position</code>, which asks the browser for the device''s position and puts it into <code>P7_HERE</code> as <code>latitude,longitude</code>; then the nearest museums are listed.</p>',
178
+ '{}', null),
179
+ (7, 10, 'Museums', 'map', 8, 'standard',
180
+ E'select id, name as title, city || '' · '' || kind as body, lat, lng\n from sample_maps.museum',
181
+ '{"zoom": 7, "height": "large", "name": "Museums"}', 'events-map'),
182
+ (7, 20, 'Map state', 'static', 4, 'standard', null, '{}', 'map-state'),
183
+ (7, 30, 'Clicked museum', 'classic_report', 4, 'standard',
184
+ E'select name as "Museum", city as "City", kind as "Kind", to_char(visitors, ''FM9G999G999'') as "Visitors"\n from sample_maps.museum where id::text = :P7_MUSEUM',
185
+ '{"empty": "Click a marker on the map."}', 'clicked'),
186
+ (7, 40, 'Nearest museums', 'classic_report', 4, 'standard',
187
+ E'with here as (\n select substring(:P7_HERE from ''^(-?\\d+(?:\\.\\d+)?),'')::numeric as lat,\n substring(:P7_HERE from '',(-?\\d+(?:\\.\\d+)?)$'')::numeric as lng)\nselect m.name as "Museum", m.city as "City",\n round(111.2 * sqrt(power(m.lat - h.lat, 2) + power((m.lng - h.lng) * cos(radians(h.lat)), 2))) || '' km'' as "Distance"\n from sample_maps.museum m, here h\n where h.lat is not null and h.lng is not null\n order by power(m.lat - h.lat, 2) + power((m.lng - h.lng) * cos(radians(h.lat)), 2)\n limit 5',
188
+ '{"empty": "Press Museums near me."}', 'nearest')
172
189
  ) as r (page, seq, title, type, cols, template, source, config, sid);
173
190
 
174
191
  update meta.region set config = config || jsonb_build_object('report', pg_temp.region_id('sample-maps', 4, 'filter-list'))
175
192
  where id = pg_temp.region_id('sample-maps', 4, 'filter-map');
193
+
194
+ -- page 6: a geocoded address (not stored), looked up when a new place gets its name, and a display map of the location
195
+ insert into meta.item (page_id, region_id, seq, name, label, type, help, config)
196
+ select i.page_id, i.region_id, x.seq, x.name, x.label, x.type, x.help, x.config::jsonb
197
+ from meta.item i, (values
198
+ (35, 'P6_ADDRESS', 'Address', 'address', 'Check address offers the matching addresses; the one you choose fills in Location.',
199
+ '{"coordinates_item": "P6_LOCATION", "country": "nl"}'),
200
+ (50, 'P6_MAP', 'On the map', 'display_map', null, '{"coordinates_item": "P6_LOCATION", "zoom": 15}')
201
+ ) as x (seq, name, label, type, help, config)
202
+ where i.page_id = pg_temp.page_id('sample-maps', 6) and i.name = 'P6_LOCATION';
203
+
204
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, condition_type, action, affected_items, code,
205
+ items_to_submit, server_condition_type, server_condition_expr)
206
+ values
207
+ (pg_temp.page_id('sample-maps', 6), 10, 'The name as the address', 'change', 'P6_NAME', 'is_not_null', 'set_value', 'P6_ADDRESS',
208
+ 'select :P6_NAME as "P6_ADDRESS"', 'P6_NAME', 'item_null', 'P6_ID'),
209
+ (pg_temp.page_id('sample-maps', 6), 20, 'Look the address up', 'change', 'P6_NAME', 'is_not_null', 'trigger_geocoding', 'P6_ADDRESS',
210
+ null, null, 'item_null', 'P6_ID');
211
+
212
+ -- page 7: dynamic actions on the map's own events (trigger: its static id) and the device's position
213
+ insert into meta.item (page_id, region_id, seq, name, label, type, help, config)
214
+ select pg_temp.page_id('sample-maps', 7), pg_temp.region_id('sample-maps', 7, 'map-state'), x.seq, x.name, x.label, x.type, x.help, '{"warn_unsaved": false}'
215
+ from (values
216
+ (10, 'P7_MUSEUM', 'Museum clicked', 'text', 'Set by the map_object_click dynamic action: the marker''s id column.'),
217
+ (20, 'P7_ZOOM', 'Zoom level', 'display', 'Set by the map_change dynamic action when you move or zoom the map.'),
218
+ (30, 'P7_HERE', 'Your position', 'text', 'Set by the current_position action: latitude,longitude. You can also type one, e.g. 52.09,5.12.')
219
+ ) as x (seq, name, label, type, help);
220
+
221
+ insert into meta.button (page_id, region_id, seq, name, label, action)
222
+ values (pg_temp.page_id('sample-maps', 7), pg_temp.region_id('sample-maps', 7, 'map-state'), 10, 'NEAR_ME', 'Museums near me', 'da');
223
+
224
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_items, affected_region_id, items_to_submit)
225
+ select pg_temp.page_id('sample-maps', 7), d.seq, d.name, d.event, d.trig, d.action, d.items, pg_temp.region_id('sample-maps', 7, d.region), d.submit
226
+ from (values
227
+ (10, 'The museum into P7_MUSEUM', 'map_object_click', 'events-map', 'set_value', 'P7_MUSEUM', null, null),
228
+ (20, 'Show the museum', 'map_object_click', 'events-map', 'refresh_region', null, 'clicked', 'P7_MUSEUM'),
229
+ (30, 'The zoom level into P7_ZOOM', 'map_change', 'events-map', 'set_value', 'P7_ZOOM', null, null),
230
+ (40, 'Where am I', 'click', 'NEAR_ME', 'current_position', 'P7_HERE', null, null),
231
+ (50, 'The nearest museums', 'click', 'NEAR_ME', 'refresh_region', null, 'nearest', 'P7_HERE'),
232
+ (60, 'The nearest museums (typed)', 'change', 'P7_HERE', 'refresh_region', null, 'nearest', 'P7_HERE')
233
+ ) as d (seq, name, event, trig, action, items, region, submit);