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
@@ -7,7 +7,18 @@
7
7
  -- * from 1,000 it asks for approval: a PURCHASE_APPROVAL task for the
8
8
  -- users with the role approver, decided under My tasks; the task's
9
9
  -- action records the decision on the request
10
- -- * the console lists the workflows with their diagram and history
10
+ -- * the console lists the workflows with their diagram and history; the
11
+ -- request's form draws its workflow (a workflow_diagram region)
12
+ -- * on the request's form the approver claims or releases the task, and
13
+ -- who asked can ask again after a rejection (the declarative process
14
+ -- types human_task_manage and human_task_create)
15
+ -- * the notification bell: a request of 1,000 or more notifies the
16
+ -- approver (a send_notification process), a decision notifies who asked
17
+ -- (meta.notify in the decision); opening one goes to the request
18
+ -- * the navigation menu is a list whose entries have badges from SQL
19
+ -- (your pending requests, the tasks waiting for you), refreshed every
20
+ -- 15 seconds by the live badges; a timer dynamic action refreshes
21
+ -- "Waiting for me"
11
22
  --
12
23
  -- Two accounts: demo / demo asks, approver / approver decides (nobody
13
24
  -- approves their own request).
@@ -50,11 +61,25 @@ create trigger purchase_defaults before insert on sample_approvals.purchase
50
61
  -- the decision, from the approval task's action (by the approver) or the workflow's SQL step
51
62
  -- (which runs as the user who started the workflow, so it says who decided: 'workflow')
52
63
  create function sample_approvals.decide(p_id int, p_outcome text, p_note text, p_by text default null) returns void language plpgsql as $$
64
+ declare
65
+ p sample_approvals.purchase;
53
66
  begin
54
67
  update sample_approvals.purchase
55
68
  set status = case lower(p_outcome) when 'approved' then 'APPROVED' when 'rejected' then 'REJECTED' else status end,
56
69
  decided_by = coalesce(p_by, meta.app_user()), note = nullif(p_note, '')
57
- where id = p_id and status = 'PENDING';
70
+ where id = p_id and status = 'PENDING'
71
+ returning * into p;
72
+ -- the bell of who asked: the decision, opening the request (page 3); a notification
73
+ -- that can't be added (notifications turned off) never stops the decision
74
+ if p.status in ('APPROVED', 'REJECTED') and p.requested_by is not null and meta.app_id() is not null then
75
+ begin
76
+ perform meta.notify(p.requested_by, format('Purchase %s: %s', lower(p.status), p.title),
77
+ coalesce(p.note, format('Decided by %s', p.decided_by)),
78
+ 3, jsonb_build_object('P3_ID', p.id), case p.status when 'APPROVED' then 'check' else 'x' end);
79
+ exception when others then
80
+ raise warning 'sample_approvals.decide: no notification: %', sqlerrm;
81
+ end;
82
+ end if;
58
83
  end
59
84
  $$;
60
85
 
@@ -73,8 +98,8 @@ grant select, insert, update, delete on sample_approvals.purchase to app_sample_
73
98
  grant execute on function sample_approvals.decide(int, text, text, text) to app_sample_approvals;
74
99
 
75
100
  -- 3. The application ----------------------------------------------------------
76
- insert into meta.app (alias, name, home_page, authentication, db_role)
77
- values ('sample-approvals', 'Sample Approvals', 1, 'app_users', 'app_sample_approvals');
101
+ insert into meta.app (alias, name, home_page, authentication, db_role, notifications, live_badges_s, nav_list)
102
+ values ('sample-approvals', 'Sample Approvals', 1, 'app_users', 'app_sample_approvals', true, 15, 'APPROVAL_MENU');
78
103
 
79
104
  insert into meta.app_user (app_id, username, password_hash, roles)
80
105
  select a.id, u.name, meta.hash_password(u.name), u.roles
@@ -116,13 +141,29 @@ insert into meta.process (page_id, seq, name, type, code, when_button)
116
141
  select pg_temp.page_id('sample-approvals', 3), 15, 'Start the approval', 'sql',
117
142
  $c$select meta.start_workflow('PURCHASE', :P3_ID, jsonb_build_object('TITLE', :P3_TITLE, 'AMOUNT', :P3_AMOUNT))$c$, 'CREATE';
118
143
 
119
- insert into meta.nav_entry (app_id, seq, label, icon, target_page)
120
- select a.id, n.seq, n.label, n.icon, n.page
121
- from meta.app a, (values (10, 'Home', 'home', 1), (20, 'Purchases', 'list', 2), (30, 'My tasks', 'check', 4), (40, 'Workflows', 'route', 5)) as n (seq, label, icon, page)
122
- where a.alias = 'sample-approvals';
144
+ -- from 1,000 the approver gets a notification on the bell (the declarative process type
145
+ -- send_notification; an application with several approvers would call meta.notify for each)
146
+ insert into meta.process (page_id, seq, name, type, when_button, condition_type, condition_expr, config)
147
+ select pg_temp.page_id('sample-approvals', 3), 16, 'Tell the approver', 'send_notification', 'CREATE',
148
+ 'sql', ':P3_AMOUNT::numeric >= 1000',
149
+ '{"to": "approver", "title": "Purchase to approve: &P3_TITLE.", "body": "&P3_AMOUNT. from &P3_SUPPLIER., asked by &APP_USER.", "page": 4, "icon": "check"}'::jsonb;
123
150
 
124
151
  insert into meta.list (app_id, name, type, description)
125
- select id, 'APPROVAL_TOPICS', 'static', 'The pages of Sample Approvals' from meta.app where alias = 'sample-approvals';
152
+ select id, l.name, 'static', l.description from meta.app, (values
153
+ ('APPROVAL_TOPICS', 'The pages of Sample Approvals'),
154
+ ('APPROVAL_MENU', 'The navigation menu, with badges from SQL')) as l (name, description)
155
+ where alias = 'sample-approvals';
156
+
157
+ -- the navigation menu (app.nav_list): badges from SQL, refreshed by the live badges
158
+ insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, badge_query)
159
+ select a.id, 'APPROVAL_MENU', e.seq, e.label, e.icon, e.page, e.badge_query
160
+ from meta.app a, (values
161
+ (10, 'Home', 'home', 1, null),
162
+ (20, 'Purchases', 'list', 2, 'select nullif(count(*), 0) from sample_approvals.purchase where status = ''PENDING'' and requested_by = meta.app_user()'),
163
+ (30, 'My tasks', 'check', 4, 'select nullif(count(*), 0) from meta.tasks where app_id = meta.app_id() and may_act and state in (''unassigned'', ''assigned'')'),
164
+ (40, 'Workflows', 'route', 5, null)
165
+ ) as e (seq, label, icon, page, badge_query)
166
+ where a.alias = 'sample-approvals';
126
167
 
127
168
  insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description)
128
169
  select a.id, 'APPROVAL_TOPICS', e.seq, e.label, e.icon, e.page, e.description
@@ -137,11 +178,11 @@ insert into meta.region (page_id, seq, title, type, columns, template, source, c
137
178
  select pg_temp.page_id('sample-approvals', r.page), r.seq, r.title, r.type, 12, r.template, r.source, r.config::jsonb
138
179
  from (values
139
180
  (1, 10, 'Sample Approvals', 'static', 'hero',
140
- '<p>Signed in as <strong>demo</strong>, ask for a purchase under <em>Purchases</em>. Below 1,000 the workflow approves it at once; from 1,000 it creates an approval task for the role <code>approver</code>. Sign in as <strong>approver</strong> / approver and decide under <em>My tasks</em>: the task''s action writes the decision on the request, and the workflow ends.</p>',
181
+ '<p>Signed in as <strong>demo</strong>, ask for a purchase under <em>Purchases</em>. Below 1,000 the workflow approves it at once; from 1,000 it creates an approval task for the role <code>approver</code>. Sign in as <strong>approver</strong> / approver and decide under <em>My tasks</em>: the task''s action writes the decision on the request, and the workflow ends.</p><p>The bell in the header shows notifications: the approver gets one for each request of 1,000 or more (a <code>send_notification</code> process), and who asked gets the decision (<code>meta.notify</code>). The menu''s badges come from SQL and refresh every 15 seconds without a reload: keep both accounts open in two browsers and watch them change.</p>',
141
182
  '{"icon": "check"}'),
142
183
  (1, 20, 'Topics', 'list', 'plain', null, '{"list": "APPROVAL_TOPICS", "template": "cards"}'),
143
184
  (2, 5, 'About this page', 'static', 'plain',
144
- '<p class="lead">The form''s Create button runs the wizard''s row process, then <code>select meta.start_workflow(''PURCHASE'', :P3_ID, …)</code>. A trigger fills in who asked and the status from the session, so the form can''t set them.</p>', '{}'),
185
+ '<p class="lead">The form''s Create button runs the wizard''s row process, then <code>select meta.start_workflow(''PURCHASE'', :P3_ID, …)</code>. A trigger fills in who asked and the status from the session, so the form can''t set them.</p><p>Open a request: below the form a <code>workflow_diagram</code> region draws its workflow, the steps it went through and where it is now. While its approval task is open, the approver gets <strong>Claim task</strong> (and then <strong>Release task</strong>): processes of type <code>human_task_manage</code>. After a rejection, who asked gets <strong>Ask again</strong>: the request, changed or not, is pending again and a process of type <code>human_task_create</code> creates a new <code>PURCHASE_APPROVAL</code> task, outside the workflow, which has ended.</p>', '{}'),
145
186
  (4, 5, 'About this page', 'static', 'plain',
146
187
  '<p class="lead">Regions of type <code>tasks</code>: what waits for the signed-in user (as owner, here the role <code>approver</code>), and what they asked for. Approve and Reject run the task definition''s action with <code>:DETAIL_PK</code>, <code>:OUTCOME</code> and <code>:COMMENT</code>.</p>', '{}'),
147
188
  (4, 10, 'Waiting for me', 'tasks', 'standard', null, '{"empty": "Nothing waits for you. Sign in as approver to decide on requests of 1,000 or more."}'),
@@ -151,3 +192,53 @@ select pg_temp.page_id('sample-approvals', r.page), r.seq, r.title, r.type, 12,
151
192
  (5, 10, 'Workflows I started', 'workflows', 'standard', null, '{"completed": true}'),
152
193
  (5, 20, 'All purchase workflows (approvers)', 'workflows', 'collapsible', null, '{"context": "admin", "completed": true}')
153
194
  ) as r (page, seq, title, type, template, source, config);
195
+
196
+ -- a timer dynamic action: "Waiting for me" refreshes every 30 seconds while the page is visible
197
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id)
198
+ select pg_temp.page_id('sample-approvals', 4), 10, 'Refresh what waits', 'timer', '30', 'refresh_region', r.id
199
+ from meta.region r where r.page_id = pg_temp.page_id('sample-approvals', 4) and r.title = 'Waiting for me';
200
+
201
+ -- page 3, the request's form (also the task's details page): its open approval task and its workflow,
202
+ -- computed when the page is shown
203
+ insert into meta.item (page_id, region_id, seq, name, type)
204
+ select r.page_id, r.id, i.seq, i.name, 'hidden'
205
+ from meta.region r, (values (200, 'P3_TASK_ID'), (210, 'P3_WORKFLOW_ID')) as i (seq, name)
206
+ where r.page_id = pg_temp.page_id('sample-approvals', 3) and r.type = 'form';
207
+
208
+ insert into meta.computation (page_id, seq, item_name, point, type, expression)
209
+ values
210
+ (pg_temp.page_id('sample-approvals', 3), 10, 'P3_TASK_ID', 'before_header', 'sql_query',
211
+ $q$select id::text from meta.tasks where app_id = meta.app_id() and definition = 'PURCHASE_APPROVAL' and detail_pk = :P3_ID and state in ('unassigned', 'assigned') order by id desc limit 1$q$),
212
+ (pg_temp.page_id('sample-approvals', 3), 20, 'P3_WORKFLOW_ID', 'before_header', 'sql_query',
213
+ $q$select id::text from meta.workflows where app_id = meta.app_id() and name = 'PURCHASE' and detail_pk = :P3_ID order by id desc limit 1$q$);
214
+
215
+ -- the workflow of the request: a Workflow Diagram region (shown to who started it and its administrators)
216
+ insert into meta.region (page_id, seq, title, type, columns, template, config, condition_type, condition)
217
+ values (pg_temp.page_id('sample-approvals', 3), 20, 'Its workflow', 'workflow_diagram', 12, 'standard',
218
+ '{"instance_item": "P3_WORKFLOW_ID", "empty": "You can''t see this request''s workflow: only who asked and the approvers can."}',
219
+ 'item_not_null', 'P3_WORKFLOW_ID');
220
+
221
+ -- the open task: Claim (a potential owner, while nobody has it) and Release (its owner);
222
+ -- after a rejection who asked can Ask again: the request (as changed on the form) pending again, and a new approval task
223
+ insert into meta.button (page_id, region_id, seq, name, label, action, condition_type, condition)
224
+ select r.page_id, r.id, b.seq, b.name, b.label, 'submit', 'exists', b.cond
225
+ from meta.region r, (values
226
+ (50, 'CLAIM', 'Claim task', $c$select 1 from meta.tasks where id = nullif(:P3_TASK_ID, '')::bigint and may_claim$c$),
227
+ (60, 'RELEASE', 'Release task', $c$select 1 from meta.tasks where id = nullif(:P3_TASK_ID, '')::bigint and may_release$c$),
228
+ (70, 'ASK_AGAIN', 'Ask again', $c$select 1 from sample_approvals.purchase where id = :P3_ID::int and status = 'REJECTED' and requested_by = meta.app_user()$c$)
229
+ ) as b (seq, name, label, cond)
230
+ where r.page_id = pg_temp.page_id('sample-approvals', 3) and r.type = 'form';
231
+
232
+ insert into meta.process (page_id, seq, name, type, code, when_button, config, success_message)
233
+ values
234
+ (pg_temp.page_id('sample-approvals', 3), 30, 'Claim the task', 'human_task_manage', null, 'CLAIM',
235
+ '{"action": "claim", "task": "&P3_TASK_ID."}', 'The task is yours: approve or reject it under My tasks.'),
236
+ (pg_temp.page_id('sample-approvals', 3), 40, 'Release the task', 'human_task_manage', null, 'RELEASE',
237
+ '{"action": "release", "task": "&P3_TASK_ID.", "comment": "Released from the request''s page"}', 'The task is back with every approver.'),
238
+ (pg_temp.page_id('sample-approvals', 3), 50, 'Pending again', 'sql',
239
+ $c$update sample_approvals.purchase
240
+ set title = :P3_TITLE, supplier = :P3_SUPPLIER, amount = :P3_AMOUNT::numeric, status = 'PENDING', decided_by = null, note = null
241
+ where id = :P3_ID::int and status = 'REJECTED' and requested_by = meta.app_user()$c$, 'ASK_AGAIN', '{}', null),
242
+ (pg_temp.page_id('sample-approvals', 3), 51, 'Ask for approval again', 'human_task_create', null, 'ASK_AGAIN',
243
+ '{"definition": "PURCHASE_APPROVAL", "detail_pk": "&P3_ID.", "parameters": {"TITLE": "&P3_TITLE.", "AMOUNT": "&P3_AMOUNT."}, "id_item": "P3_TASK_ID"}',
244
+ 'Asked again: the approvers have a new task.');
@@ -3,7 +3,8 @@
3
3
  --
4
4
  -- The calendar region: month, week, day and list views, all-day and timed
5
5
  -- events, events over several days, create on click, drag and drop through
6
- -- a PL/pgSQL function with its own rule, and a calendar filtered by an item.
6
+ -- a PL/pgSQL function with its own rule, a calendar filtered by an item, and
7
+ -- dynamic actions on a day chosen and on another view.
7
8
  -- The events are placed around today, so there is always something to see.
8
9
  --
9
10
  -- Installed by an administrator from the App Gallery, or with
@@ -79,7 +80,7 @@ select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias =
79
80
  insert into meta.page (app_id, page_no, name, title, parent_page)
80
81
  select a.id, p.no, p.name, p.name, p.parent
81
82
  from meta.app a, (values
82
- (1, 'Sample Calendar', null), (2, 'Month', 1), (4, 'Week planner', 1), (5, 'Agenda', 1), (6, 'By category', 1)
83
+ (1, 'Sample Calendar', null), (2, 'Month', 1), (4, 'Week planner', 1), (5, 'Agenda', 1), (6, 'By category', 1), (7, 'Calendar events', 1)
83
84
  ) as p (no, name, parent)
84
85
  where a.alias = 'sample-calendar';
85
86
 
@@ -96,7 +97,7 @@ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
96
97
  select a.id, n.seq, n.label, n.icon, n.page
97
98
  from meta.app a, (values
98
99
  (10, 'Home', 'home', 1), (20, 'Month', 'calendar', 2), (30, 'Week planner', 'clock', 4),
99
- (40, 'Agenda', 'list', 5), (50, 'By category', 'filter', 6)
100
+ (40, 'Agenda', 'list', 5), (50, 'By category', 'filter', 6), (60, 'Calendar events', 'bolt', 7)
100
101
  ) as n (seq, label, icon, page)
101
102
  where a.alias = 'sample-calendar';
102
103
 
@@ -109,7 +110,8 @@ select a.id, 'CALENDAR_TOPICS', e.seq, e.label, e.icon, e.page, e.description
109
110
  (10, 'Month', 'calendar', 2, 'All four views; click a day to add an event, drag an event to move it.'),
110
111
  (20, 'Week planner', 'clock', 4, 'Week and day views from 7:00 to 19:00, opened on the week.'),
111
112
  (30, 'Agenda', 'list', 5, 'The list view only: a month of events per day.'),
112
- (40, 'By category', 'filter', 6, 'A calendar whose query uses a page item, refreshed when it changes.')
113
+ (40, 'By category', 'filter', 6, 'A calendar whose query uses a page item, refreshed when it changes.'),
114
+ (50, 'Calendar events', 'bolt', 7, 'Dynamic actions on the calendar''s own events: a day chosen, another view or period.')
113
115
  ) as e (seq, label, icon, page, description)
114
116
  where a.alias = 'sample-calendar';
115
117
 
@@ -159,7 +161,19 @@ select pg_temp.page_id('sample-calendar', r.page), r.seq, r.title, r.type, r.col
159
161
  (6, 10, 'Filter', 'static', 12, 'standard', null, '{}', 'filter'),
160
162
  (6, 20, 'Events of the category', 'calendar', 12, 'standard',
161
163
  E'select starts as start_date, ends as end_date, title, id\n from sample_calendar.event\n where :P6_CATEGORY is null or category = :P6_CATEGORY',
162
- '{"views": ["month", "list"], "link": {"page": 3, "items": {"P3_ID": "#id#"}}}', 'category-calendar')
164
+ '{"views": ["month", "list"], "link": {"page": 3, "items": {"P3_ID": "#id#"}}}', 'category-calendar'),
165
+
166
+ -- 7 Calendar events
167
+ (7, 5, 'About this page', 'static', 12, 'plain',
168
+ '<p class="lead">Dynamic actions can listen to the calendar itself (the trigger is its static id <code>planner</code>). <strong>Date Select</strong> (<code>calendar_date_select</code>) runs when you click an empty day or hour: a <em>Set Value</em> without SQL puts the chosen date into <code>P7_DAY</code> and the report beside it is refreshed with that day''s events. This calendar has no <code>create</code> link, so a click only chooses (with one, a <em>Cancel Event</em> on this event would keep the form closed). <strong>View Change</strong> (<code>calendar_view_change</code>) runs on the page that comes back from another view or period: the view (<code>month</code>, <code>week</code>, <code>day</code> or <code>list</code>) goes into <code>P7_VIEW</code>, and the day view adds a hint.</p>',
169
+ '{}', null),
170
+ (7, 10, 'Planner', 'calendar', 8, 'standard',
171
+ E'select starts as start_date, ends as end_date, title, id\n from sample_calendar.event',
172
+ '{"views": ["month", "week", "day", "list"], "link": {"page": 3, "items": {"P3_ID": "#id#"}}}', 'planner'),
173
+ (7, 20, 'Chosen day', 'static', 4, 'standard', null, '{}', 'chosen'),
174
+ (7, 30, 'Events of the day', 'classic_report', 4, 'standard',
175
+ E'select to_char(starts, ''HH24:MI'') as "Time", title as "Event", category as "Category"\n from sample_calendar.event\n where :P7_DAY is not null\n and starts::date <= left(:P7_DAY, 10)::date and coalesce(ends, starts)::date >= left(:P7_DAY, 10)::date\n order by starts',
176
+ '{"empty": "Click a day in the calendar."}', 'day-events')
163
177
  ) as r (page, seq, title, type, cols, template, source, config, sid);
164
178
 
165
179
  insert into meta.item (page_id, region_id, seq, name, label, type, lov, config)
@@ -170,3 +184,24 @@ select pg_temp.page_id('sample-calendar', 6), r.id, 10, 'P6_CATEGORY', 'Category
170
184
  insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id, items_to_submit)
171
185
  select pg_temp.page_id('sample-calendar', 6), 10, 'Refresh the calendar', 'change', 'P6_CATEGORY', 'refresh_region', r.id, 'P6_CATEGORY'
172
186
  from meta.region r where r.page_id = pg_temp.page_id('sample-calendar', 6) and r.static_id = 'category-calendar';
187
+
188
+ -- page 7: dynamic actions on the calendar's own events (trigger: its static id)
189
+ insert into meta.item (page_id, region_id, seq, name, label, type, help, config)
190
+ select pg_temp.page_id('sample-calendar', 7), r.id, i.seq, i.name, i.label, i.type, i.help, '{"warn_unsaved": false}'
191
+ from meta.region r, (values
192
+ (10, 'P7_DAY', 'Chosen day', 'text', 'Set by the calendar_date_select dynamic action: YYYY-MM-DD, or YYYY-MM-DDTHH:MM for an hour.'),
193
+ (20, 'P7_VIEW', 'Calendar view', 'display', 'Set by the calendar_view_change dynamic action.')
194
+ ) as i (seq, name, label, type, help)
195
+ where r.page_id = pg_temp.page_id('sample-calendar', 7) and r.static_id = 'chosen';
196
+
197
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, condition_type, condition_value, action,
198
+ affected_items, affected_region_id, items_to_submit, message)
199
+ select pg_temp.page_id('sample-calendar', 7), d.seq, d.name, d.event, 'planner', d.ctype, d.cvalue, d.action, d.items,
200
+ (select id from meta.region where page_id = pg_temp.page_id('sample-calendar', 7) and static_id = d.region), d.submit, d.message
201
+ from (values
202
+ (10, 'The chosen day into P7_DAY', 'calendar_date_select', null, null, 'set_value', 'P7_DAY', null, null, null),
203
+ (20, 'Its events', 'calendar_date_select', null, null, 'refresh_region', null, 'day-events', 'P7_DAY', null),
204
+ (40, 'The view into P7_VIEW', 'calendar_view_change', null, null, 'set_value', 'P7_VIEW', null, null, null),
205
+ (50, 'A hint for the day view', 'calendar_view_change', 'equals', 'day', 'show_success', null, null, null,
206
+ 'The day view: click an hour to see the events of that day.')
207
+ ) as d (seq, name, event, ctype, cvalue, action, items, region, submit, message);
@@ -6,7 +6,21 @@
6
6
  -- page 3 values from the server (set value, execute SQL) and a report
7
7
  -- refreshed when an item changes;
8
8
  -- page 4 messages, confirmations, a wait popup, classes, item errors,
9
- -- an inline dialog and actions on page load and key release.
9
+ -- an inline dialog and actions on page load and key release;
10
+ -- page 5 keyboard and mouse: key down / key press (with Cancel Event),
11
+ -- focus, blur, select, double click, mouse enter / leave and
12
+ -- Set Style with greater / less conditions;
13
+ -- page 6 a form with rules: Before Submit, Submit, Refresh Item, Disable,
14
+ -- Hide, list and empty conditions, Page Unload, a dialog page
15
+ -- (page 10) closed or cancelled by a dynamic action;
16
+ -- page 7 regions and the window: before / after refresh, resize, scroll,
17
+ -- Close Region, Print, Share, Download, Push Subscribe;
18
+ -- page 8 a tree: Selection Change, Expand Tree, Collapse Tree; an item in the
19
+ -- tree region and a hidden item a dynamic action sets in the browser
20
+ -- (value_protected false);
21
+ -- page 9 faceted search: Facets Change and the report's dialogs;
22
+ -- page 11 Show AI Assistant (asking needs an AI service set up by an
23
+ -- administrator; the page works without one).
10
24
  --
11
25
  -- Installed by an administrator from the App Gallery, or with
12
26
  -- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-dynamic-actions.sql
@@ -21,6 +35,17 @@ create or replace function pg_temp.region_id(p_alias text, p_page int, p_static
21
35
  -- 1. Schema and data ---------------------------------------------------------
22
36
  create schema sample_da;
23
37
 
38
+ create table sample_da.city (
39
+ country text not null,
40
+ name text not null,
41
+ primary key (country, name)
42
+ );
43
+
44
+ insert into sample_da.city (country, name) values
45
+ ('NL', 'Amsterdam'), ('NL', 'Rotterdam'), ('NL', 'Utrecht'), ('BE', 'Antwerp'), ('BE', 'Brussels'), ('BE', 'Ghent'),
46
+ ('DE', 'Berlin'), ('DE', 'Hamburg'), ('DE', 'Munich'), ('FR', 'Lyon'), ('FR', 'Paris'), ('FR', 'Toulouse'),
47
+ ('US', 'Boston'), ('US', 'Chicago'), ('US', 'Seattle'), ('JP', 'Osaka'), ('JP', 'Sapporo'), ('JP', 'Tokyo');
48
+
24
49
  create table sample_da.product (
25
50
  id int generated always as identity primary key,
26
51
  name text not null unique,
@@ -48,17 +73,25 @@ values ('sample-dynamic-actions', 'Sample Dynamic Actions', 1, 'app_users', 'app
48
73
  insert into meta.app_user (app_id, username, password_hash, roles)
49
74
  select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias = 'sample-dynamic-actions';
50
75
 
51
- insert into meta.page (app_id, page_no, name, title, parent_page)
52
- select a.id, p.no, p.name, p.name, p.parent
76
+ -- push notifications on, for the dynamic action push_subscribe (page 7)
77
+ update meta.app set pwa = true, pwa_push = true where alias = 'sample-dynamic-actions';
78
+
79
+ insert into meta.page (app_id, page_no, name, title, parent_page, mode)
80
+ select a.id, p.no, p.name, p.name, p.parent, p.mode
53
81
  from meta.app a, (values
54
- (1, 'Sample Dynamic Actions', null), (2, 'Show, hide, enable', 1), (3, 'Values from the server', 1), (4, 'Messages and effects', 1)
55
- ) as p (no, name, parent)
82
+ (1, 'Sample Dynamic Actions', null, 'normal'), (2, 'Show, hide, enable', 1, 'normal'), (3, 'Values from the server', 1, 'normal'),
83
+ (4, 'Messages and effects', 1, 'normal'), (5, 'Keyboard and mouse', 1, 'normal'), (6, 'A form with rules', 1, 'normal'),
84
+ (7, 'Regions and the window', 1, 'normal'), (8, 'A tree', 1, 'normal'), (9, 'Faceted search', 1, 'normal'),
85
+ (10, 'A dialog page', 6, 'modal'), (11, 'AI assistant', 1, 'normal')
86
+ ) as p (no, name, parent, mode)
56
87
  where a.alias = 'sample-dynamic-actions';
57
88
 
58
89
  insert into meta.nav_entry (app_id, seq, label, icon, target_page)
59
90
  select a.id, n.seq, n.label, n.icon, n.page
60
91
  from meta.app a, (values
61
- (10, 'Home', 'home', 1), (20, 'Show, hide, enable', 'eye', 2), (30, 'Values from the server', 'refresh', 3), (40, 'Messages and effects', 'bell', 4)
92
+ (10, 'Home', 'home', 1), (20, 'Show, hide, enable', 'eye', 2), (30, 'Values from the server', 'refresh', 3), (40, 'Messages and effects', 'bell', 4),
93
+ (50, 'Keyboard and mouse', 'keyboard', 5), (60, 'A form with rules', 'clipboard', 6), (70, 'Regions and the window', 'layers', 7),
94
+ (80, 'A tree', 'folder', 8), (90, 'Faceted search', 'filter', 9), (100, 'AI assistant', 'chat', 11)
62
95
  ) as n (seq, label, icon, page)
63
96
  where a.alias = 'sample-dynamic-actions';
64
97
 
@@ -70,7 +103,13 @@ select a.id, 'DA_TOPICS', e.seq, e.label, e.icon, e.page, e.description
70
103
  from meta.app a, (values
71
104
  (10, 'Show, hide, enable', 'eye', 2, 'Items that appear, disappear or switch on depending on another item.'),
72
105
  (20, 'Values from the server', 'refresh', 3, 'SQL fills in items and refreshes a report, without reloading the page.'),
73
- (30, 'Messages and effects', 'bell', 4, 'Alerts, confirmations, a wait popup, classes, item errors and an inline dialog.')
106
+ (30, 'Messages and effects', 'bell', 4, 'Alerts, confirmations, a wait popup, classes, item errors and an inline dialog.'),
107
+ (40, 'Keyboard and mouse', 'keyboard', 5, 'Keys counted or refused, focus, selected text, double clicks, hovering and Set Style.'),
108
+ (50, 'A form with rules', 'clipboard', 6, 'Before submit, submit, a list refreshed by another item, disable and hide, a dialog page.'),
109
+ (60, 'Regions and the window', 'layers', 7, 'Before and after a refresh, resize and scroll, print, share, download, notifications.'),
110
+ (70, 'A tree', 'folder', 8, 'A node chosen in a tree shows its products; expand and collapse every node.'),
111
+ (80, 'Faceted search', 'filter', 9, 'Facets that changed, and the interactive report''s dialogs opened by buttons.'),
112
+ (90, 'AI assistant', 'chat', 11, 'A button that opens an AI assistant and puts the cursor in its question.')
74
113
  ) as e (seq, label, icon, page, description)
75
114
  where a.alias = 'sample-dynamic-actions';
76
115
 
@@ -111,9 +150,88 @@ select pg_temp.page_id('sample-dynamic-actions', r.page), r.seq, r.title, r.type
111
150
  '{}', 'target'),
112
151
  (4, 40, 'An inline dialog', 'static', 12, 'inline_dialog',
113
152
  '<p>This region has the template <em>Inline dialog</em>: hidden until a dynamic action (or a link to its id) opens it. Escape, the close button or a click beside it closes it.</p>',
114
- '{}', 'dialog')
153
+ '{}', 'dialog'),
154
+
155
+ -- 5 Keyboard and mouse
156
+ (5, 5, 'About this page', 'static', 12, 'plain',
157
+ '<p class="lead">Events of the keyboard and the mouse. <strong>Key down</strong> counts every key you press in <em>Type here</em> (a <em>Set value</em> on the server). <strong>Key press</strong> with the condition <em>greater or equal</em> 100 and <strong>Cancel Event</strong> keeps a fourth digit out of <em>Quantity</em>. <strong>Focus</strong> and <strong>Blur</strong> highlight the note''s region while you are in it, <strong>Select</strong> notices selected text, a <strong>Double click</strong> runs <em>Clear</em>. <strong>Mouse enter</strong> and <strong>Mouse leave</strong> highlight a region under the pointer, and <strong>Set Style</strong> marks the score with a coloured bar by conditions <em>greater</em>, <em>greater or equal</em> and <em>less</em>.</p>',
158
+ '{}', null),
159
+ (5, 10, 'Keys', 'static', 6, 'standard', null, '{}', 'keys'),
160
+ (5, 20, 'A note', 'static', 6, 'standard', null, '{}', 'note-box'),
161
+ (5, 30, 'Score', 'static', 6, 'standard', null, '{}', 'score'),
162
+ (5, 40, 'Point at this region', 'static', 6, 'standard',
163
+ '<p>Move the pointer onto this region: <em>Mouse enter</em> adds the class <code>is-highlight</code>, <em>Mouse leave</em> removes it again. Each runs once per visit, not on every movement inside it.</p>',
164
+ '{}', 'hover'),
165
+
166
+ -- 6 A form with rules
167
+ (6, 5, 'About this page', 'static', 12, 'plain',
168
+ '<p class="lead"><strong>Disable</strong> with the condition <em>is empty</em> keeps the buttons off until you have typed a name and left the field. A city list is drawn again with <strong>Refresh item</strong> when the country changes (its SQL uses the country). <strong>Hide</strong> with <em>not in list</em> hides the VAT number outside the EU, <strong>Show</strong> with <em>in list</em> shows the free delivery note and the customs number; <em>less or equal</em> 0 copies shows an error. Every submit runs <strong>Before submit</strong> first: a confirmation that can stop it. <em>Send</em> is a submit button (its process shows a message); <em>Keep as a draft</em> runs the action <strong>Submit</strong>, a submit without a button: the values are kept in session state and no process runs. Leaving the page (<strong>Page unload</strong>) shows a wait popup until the next page is there. <em>Open the dialog page</em> opens page 10, whose buttons run <strong>Close dialog</strong> and <strong>Cancel dialog</strong>.</p>',
169
+ '{}', null),
170
+ (6, 10, 'Order', 'static', 6, 'standard', null, '{}', 'order'),
171
+ (6, 20, 'Delivery', 'static', 6, 'standard', null, '{}', 'delivery'),
172
+ (6, 30, 'Free delivery', 'static', 6, 'standard',
173
+ '<p>Orders to the Netherlands and Belgium are delivered for free. This region is shown by <em>Show</em> with the condition <em>in list</em> <code>NL,BE</code>.</p>',
174
+ '{}', 'free-delivery'),
175
+ (6, 40, 'Send', 'static', 12, 'buttons_container', null, '{}', 'send'),
176
+
177
+ -- 7 Regions and the window
178
+ (7, 5, 'About this page', 'static', 12, 'plain',
179
+ '<p class="lead"><em>Shuffle</em> refreshes the report: <strong>Before refresh</strong> dims it, <strong>After refresh</strong> notes the time. Changing the window''s size runs <strong>Resize</strong>; scrolling runs <strong>Scroll</strong>, which closes the collapsible <em>Tips</em> with <strong>Close region</strong>. The other buttons run <strong>Print</strong>, <strong>Share</strong> (the device''s share sheet, else the address is copied), <strong>Download</strong> (the file of a download process of the page) and <strong>Push subscribe</strong> (turns on notifications on this device; the server needs <code>PGKILN_SECRET_KEY</code> for them).</p>',
180
+ '{}', null),
181
+ (7, 10, 'Tips', 'static', 12, 'collapsible',
182
+ '<p>This region is collapsible. Scroll the page and a <em>Scroll</em> dynamic action closes it with <em>Close region</em>; open it again with its heading.</p><p>A dynamic action on <em>Resize</em> or <em>Scroll</em> runs at most once per frame, so it can''t slow the page down.</p>',
183
+ '{}', 'tips'),
184
+ (7, 20, 'Try the actions', 'static', 12, 'buttons_container', null, '{}', 'tools'),
185
+ (7, 30, 'Products in random order', 'report', 12, 'standard',
186
+ 'select name, category, price from sample_da.product order by random()',
187
+ '{"formats": {"price": "999G990D00"}, "sortable": false, "page_size": 50}', 'shuffled'),
188
+ (7, 40, 'Pick products', 'report', 12, 'standard',
189
+ 'select name, category, price from sample_da.product order by name',
190
+ '{"formats": {"price": "999G990D00"}, "page_size": 10, "selection": {"column": "name", "item": "P7_PICKED"}}', 'picks'),
191
+
192
+ -- 8 A tree
193
+ (8, 5, 'About this page', 'static', 12, 'plain',
194
+ '<p class="lead">Clicking a node of the tree runs <strong>Selection change [Tree]</strong>: a <em>Set value</em> without SQL puts the node''s id into a hidden item (<code>{"value_protected": false}</code>: a hidden input the browser may set) and into the display item under the tree (items show in any region type), and <em>Refresh region</em> draws the report with it. <strong>Expand tree</strong> and <strong>Collapse tree</strong> open or close every node.</p>',
195
+ '{}', null),
196
+ (8, 10, 'Categories and products', 'tree', 5, 'standard',
197
+ E'select ''c-'' || lower(category) as id, null::text as parent_id, category as label from sample_da.product group by category\nunion all\nselect id::text, ''c-'' || lower(category), name from sample_da.product',
198
+ '{"expanded": 1}', 'catalogue-tree'),
199
+ (8, 20, 'Chosen', 'report', 7, 'standard',
200
+ E'select name, category, price from sample_da.product\n where :P8_NODE is not null and (id::text = :P8_NODE or ''c-'' || lower(category) = :P8_NODE)\n order by name',
201
+ '{"formats": {"price": "999G990D00"}, "searchable": false, "empty": "Choose a category or a product in the tree."}', 'chosen'),
202
+
203
+ -- 9 Faceted search
204
+ (9, 5, 'About this page', 'static', 12, 'plain',
205
+ '<p class="lead">Choosing a facet runs <strong>Facets change</strong> on the page that comes back with the new filters: here a success message. The buttons of the report run <strong>Open report dialog</strong> with the interactive report''s <em>Filter</em>, <em>Columns</em> and <em>Chart</em> dialogs.</p>',
206
+ '{}', null),
207
+ (9, 10, 'Filters', 'facets', 3, 'collapsible', null, '{}', 'filters'),
208
+ (9, 30, 'Catalogue', 'report', 9, 'standard',
209
+ 'select name, category, price from sample_da.product',
210
+ '{"formats": {"price": "999G990D00"}}', 'catalogue'),
211
+
212
+ -- 10 A dialog page
213
+ (10, 5, 'About this page', 'static', 12, 'plain',
214
+ '<p class="lead">This page is a <strong>modal dialog</strong>. <em>Close</em> runs <strong>Close dialog</strong>: the page below runs its <em>Dialog closed</em> actions (a message). <em>Cancel</em> runs <strong>Cancel dialog</strong>: the dialog closes and nothing else happens.</p>',
215
+ '{}', null),
216
+ (10, 10, 'Close or cancel', 'static', 12, 'buttons_container', null, '{}', 'dialog-buttons'),
217
+
218
+ -- 11 AI assistant
219
+ (11, 5, 'About this page', 'static', 12, 'plain',
220
+ '<p class="lead">The button runs <strong>Show AI Assistant</strong>: it opens the AI assistant region (here in an inline dialog) and puts the cursor in its question. Answers need an AI service that an administrator set up and allowed for this application; without one the assistant says so.</p>',
221
+ '{}', null),
222
+ (11, 10, 'Ask', 'static', 12, 'buttons_container', null, '{}', 'ask'),
223
+ (11, 20, 'Office supplies assistant', 'ai_assistant', 12, 'inline_dialog', null,
224
+ '{"service": "CLAUDE", "system": "You answer questions about the office supplies in the data, briefly.", "welcome": "Ask me about the office supplies: prices, categories, what is cheapest.", "placeholder": "Which pens are there?", "context": [{"name": "products", "sql": "select name, category, price from sample_da.product order by category, name", "max_rows": 50}], "error_message": "No AI service is set up for this application: an administrator adds one (Administration, AI services) and allows it for the application."}',
225
+ 'assistant')
115
226
  ) as r (page, seq, title, type, cols, template, source, config, sid);
116
227
 
228
+ -- the facets filter the catalogue report (its id)
229
+ update meta.region set config = jsonb_build_object(
230
+ 'report', pg_temp.region_id('sample-dynamic-actions', 9, 'catalogue'),
231
+ 'facets', '[{"column": "category", "label": "Category"},
232
+ {"column": "price", "label": "Price", "type": "range", "ranges": [{"to": 5, "label": "Below 5"}, {"from": 5, "to": 15, "label": "5 to 15"}, {"from": 15, "label": "15 and more"}]}]'::jsonb)
233
+ where id = pg_temp.region_id('sample-dynamic-actions', 9, 'filters');
234
+
117
235
  insert into meta.item (page_id, region_id, seq, name, label, type, lov, default_value, help, config)
118
236
  select pg_temp.page_id('sample-dynamic-actions', i.page), pg_temp.region_id('sample-dynamic-actions', i.page, i.region),
119
237
  i.seq, i.name, i.label, i.type, i.lov, i.def, i.help, i.config::jsonb
@@ -131,7 +249,22 @@ select pg_temp.page_id('sample-dynamic-actions', i.page), pg_temp.region_id('sam
131
249
  (3, 'filter', 10, 'P3_CATEGORY', 'Category', 'select', 'select distinct category as d, category as r from sample_da.product order by 1', null,
132
250
  'The report below is refreshed when this changes.', '{"warn_unsaved": false, "null_label": "All categories"}'),
133
251
  (4, 'greeting', 10, 'P4_NAME', 'Your name', 'text', null, null, 'Type: the greeting follows each key you release.', '{"warn_unsaved": false}'),
134
- (4, 'greeting', 20, 'P4_GREETING', 'Greeting', 'text', null, null, null, '{"warn_unsaved": false}')
252
+ (4, 'greeting', 20, 'P4_GREETING', 'Greeting', 'text', null, null, null, '{"warn_unsaved": false}'),
253
+ (5, 'keys', 10, 'P5_TYPE', 'Type here', 'text', null, null, 'Key down: every key counts, also arrows and Backspace.', '{"warn_unsaved": false}'),
254
+ (5, 'keys', 20, 'P5_KEYS', 'Keys pressed', 'display', null, '0', null, '{}'),
255
+ (5, 'keys', 30, 'P5_QTY', 'Quantity (up to 999)', 'text', null, null, 'Key press: a fourth digit is refused.', '{"warn_unsaved": false}'),
256
+ (5, 'note-box', 10, 'P5_NOTE', 'Note', 'textarea', null, 'Select a word of this text, or double-click to empty the note.', 'Focus highlights the region; a double click empties the note.', '{"warn_unsaved": false}'),
257
+ (5, 'score', 10, 'P5_SCORE', 'Score (0 to 100)', 'number', null, null, 'Set Style marks a score above 80 green, below 50 red.', '{"warn_unsaved": false}'),
258
+ (6, 'order', 10, 'P6_NAME', 'Your name', 'text', null, null, 'The buttons stay off while this is empty.', '{}'),
259
+ (6, 'order', 20, 'P6_COPIES', 'Copies', 'number', null, '1', 'Zero or fewer shows an error.', '{}'),
260
+ (6, 'delivery', 10, 'P6_COUNTRY', 'Country', 'select', 'STATIC:Netherlands;NL,Belgium;BE,Germany;DE,France;FR,United States;US,Japan;JP', null, null, '{"null_label": "Choose a country"}'),
261
+ (6, 'delivery', 20, 'P6_CITY', 'City', 'select', 'select name as d, name as r from sample_da.city where country = :P6_COUNTRY order by 1', null, 'Refresh item draws this list again when the country changes.', '{"null_label": "Choose a city"}'),
262
+ (6, 'delivery', 30, 'P6_VAT', 'VAT number', 'text', null, null, 'Only in the EU (hidden with not in list).', '{}'),
263
+ (6, 'delivery', 40, 'P6_CUSTOMS', 'Customs number', 'text', null, null, 'Only outside the EU (shown with in list).', '{}'),
264
+ (7, 'tools', 10, 'P7_REFRESHED', 'Last shuffled', 'display', null, 'not yet', null, '{}'),
265
+ (7, 'tools', 20, 'P7_PICKED', 'Picked', 'text', null, null, 'Selection change: the rows checked in Pick products, colon separated.', '{"warn_unsaved": false}'),
266
+ (8, 'catalogue-tree', 10, 'P8_SHOWN', 'Chosen node', 'display', null, 'none yet', 'Set by the tree: a category (c-…) or a product id.', '{}'),
267
+ (8, 'chosen', 10, 'P8_NODE', null, 'hidden', null, null, null, '{"value_protected": false}')
135
268
  ) as i (page, region, seq, name, label, type, lov, def, help, config);
136
269
 
137
270
  insert into meta.button (page_id, region_id, seq, name, label, action, hot)
@@ -142,6 +275,31 @@ select pg_temp.page_id('sample-dynamic-actions', 4), pg_temp.region_id('sample-d
142
275
  (70, 'ERROR', 'Show an item error', false), (80, 'CLEAR', 'Clear the errors', false), (90, 'DIALOG', 'Open the inline dialog', false)
143
276
  ) as b (seq, name, label, hot);
144
277
 
278
+ insert into meta.button (page_id, region_id, seq, name, label, action, hot, target_page)
279
+ select pg_temp.page_id('sample-dynamic-actions', b.page), pg_temp.region_id('sample-dynamic-actions', b.page, b.region), b.seq, b.name, b.label, b.action, b.hot, b.target
280
+ from (values
281
+ (6, 'send', 10, 'SEND', 'Send', 'submit', true, null::int), (6, 'send', 20, 'DRAFT', 'Keep as a draft', 'da', false, null),
282
+ (6, 'send', 30, 'OPEN_DIALOG', 'Open the dialog page', 'redirect', false, 10),
283
+ (7, 'tools', 10, 'SHUFFLE', 'Shuffle', 'da', true, null), (7, 'tools', 20, 'PRINT', 'Print', 'da', false, null),
284
+ (7, 'tools', 30, 'SHARE', 'Share', 'da', false, null), (7, 'tools', 40, 'DOWNLOAD', 'Download as CSV', 'da', false, null),
285
+ (7, 'tools', 50, 'NOTIFY', 'Turn on notifications', 'da', false, null),
286
+ (8, 'catalogue-tree', 10, 'EXPAND', 'Expand all', 'da', false, null), (8, 'catalogue-tree', 20, 'COLLAPSE', 'Collapse all', 'da', false, null),
287
+ (9, 'catalogue', 10, 'FILTER', 'Filter…', 'da', false, null), (9, 'catalogue', 20, 'COLUMNS', 'Columns…', 'da', false, null),
288
+ (9, 'catalogue', 30, 'CHART', 'Chart…', 'da', false, null),
289
+ (10, 'dialog-buttons', 10, 'CANCEL', 'Cancel', 'da', false, null), (10, 'dialog-buttons', 20, 'CLOSE', 'Close', 'da', true, null),
290
+ (11, 'ask', 10, 'ASK', 'Ask the assistant', 'da', true, null)
291
+ ) as b (page, region, seq, name, label, action, hot, target);
292
+
293
+ -- page 6: what Send submits to (the sample stores nothing); page 7: the file of the Download button
294
+ insert into meta.process (page_id, seq, name, type, code, point, when_button, success_message, config)
295
+ select pg_temp.page_id('sample-dynamic-actions', p.page), p.seq, p.name, p.type, p.code, p.point, p.button, p.message, p.config::jsonb
296
+ from (values
297
+ (6, 10, 'Send the order', 'sql', 'select null', 'submit', 'SEND', 'Sent (this sample stores nothing): Before submit asked first.', '{}'),
298
+ (7, 10, 'PRODUCTS_CSV', 'download',
299
+ E'select ''name;category;price'' || E''\\n'' || string_agg(name || '';'' || category || '';'' || price, E''\\n'' order by name) as content,\n ''products.csv'' as filename, ''text/csv'' as mime_type\n from sample_da.product',
300
+ 'submit', 'DOWNLOAD', null, '{}')
301
+ ) as p (page, seq, name, type, code, point, button, message, config);
302
+
145
303
  insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, condition_type, condition_value,
146
304
  action, affected_items, affected_region_id, code, items_to_submit, message, css_classes)
147
305
  select pg_temp.page_id('sample-dynamic-actions', d.page), d.seq, d.name, d.event, d.trig, d.ctype, d.cvalue,
@@ -175,3 +333,76 @@ select pg_temp.page_id('sample-dynamic-actions', d.page), d.seq, d.name, d.event
175
333
  (4, 130, 'Clear the errors', 'click', 'CLEAR', null, null, 'clear_errors', null, null, null, null, null, null),
176
334
  (4, 140, 'Open the dialog', 'click', 'DIALOG', null, null, 'open_region', null, 'dialog', null, null, null, null)
177
335
  ) as d (page, seq, name, event, trig, ctype, cvalue, action, items, region, code, submit, message, classes);
336
+
337
+ -- pages 5 to 11: more events, actions and client-side conditions (a region trigger is its static id)
338
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, condition_type, condition_value,
339
+ action, affected_items, affected_region_id, code, items_to_submit, message, css_classes, config)
340
+ select pg_temp.page_id('sample-dynamic-actions', d.page), d.seq, d.name, d.event, d.trig, d.ctype, d.cvalue,
341
+ d.action, d.items, pg_temp.region_id('sample-dynamic-actions', d.page, d.region), d.code, d.submit, d.message, d.classes, d.config::jsonb
342
+ from (values
343
+ -- page 5 Keyboard and mouse
344
+ (5, 10, 'Count the keys', 'key_down', 'P5_TYPE', null, null, 'set_value', 'P5_KEYS', null,
345
+ 'select coalesce(nullif(:P5_KEYS, '''')::int, 0) + 1', null, null, null, null),
346
+ (5, 20, 'Say why a key is refused', 'key_press', 'P5_QTY', 'greater_equal', '100', 'show_error', 'P5_QTY', null, null, null,
347
+ 'Three digits at most: that key was refused.', null, null),
348
+ (5, 30, 'Refuse a fourth digit', 'key_press', 'P5_QTY', 'greater_equal', '100', 'cancel_event', null, null, null, null, null, null, null),
349
+ (5, 40, 'No error below 100', 'change', 'P5_QTY', 'less', '100', 'clear_errors', 'P5_QTY', null, null, null, null, null, null),
350
+ (5, 50, 'Highlight the note while in it', 'focus', 'P5_NOTE', null, null, 'add_class', null, 'note-box', null, null, null, 'is-highlight', null),
351
+ (5, 60, 'Stop highlighting the note', 'blur', 'P5_NOTE', null, null, 'remove_class', null, 'note-box', null, null, null, 'is-highlight', null),
352
+ (5, 70, 'Text selected', 'select', 'P5_NOTE', null, null, 'show_success', null, null, null, null,
353
+ 'You selected text in the note (Select). Copy it with Ctrl+C.', null, null),
354
+ (5, 80, 'Empty the note', 'double_click', 'P5_NOTE', null, null, 'clear', 'P5_NOTE', null, null, null, null, null, null),
355
+ (5, 90, 'Highlight under the pointer', 'mouse_enter', 'hover', null, null, 'add_class', null, 'hover', null, null, null, 'is-highlight', null),
356
+ (5, 100, 'Pointer gone', 'mouse_leave', 'hover', null, null, 'remove_class', null, 'hover', null, null, null, 'is-highlight', null),
357
+ (5, 110, 'A plain score', 'change', 'P5_SCORE', 'greater_equal', '50', 'set_style', 'P5_SCORE', null, null, null, null, null,
358
+ '{"styles": {"border-width": "0", "padding": "0"}}'),
359
+ (5, 120, 'A high score in green', 'change', 'P5_SCORE', 'greater', '80', 'set_style', 'P5_SCORE', null, null, null, null, null,
360
+ '{"styles": {"border-style": "solid", "border-width": "0 0 0 4px", "border-color": "#067647", "padding": "0 0 0 8px"}}'),
361
+ (5, 130, 'A low score in red', 'change', 'P5_SCORE', 'less', '50', 'set_style', 'P5_SCORE', null, null, null, null, null,
362
+ '{"styles": {"border-style": "solid", "border-width": "0 0 0 4px", "border-color": "#b42318", "padding": "0 0 0 8px"}}'),
363
+ -- page 6 A form with rules
364
+ (6, 10, 'Send only with a name', 'change', 'P6_NAME', 'is_null', null, 'disable', null, 'send', null, null, null, null, null),
365
+ (6, 20, 'Cities of the country', 'change', 'P6_COUNTRY', null, null, 'refresh_item', 'P6_CITY', null, null, 'P6_COUNTRY', null, null, null),
366
+ (6, 30, 'No VAT number outside the EU', 'change', 'P6_COUNTRY', 'not_in_list', 'NL,BE,DE,FR', 'hide', 'P6_VAT', null, null, null, null, null, null),
367
+ (6, 40, 'Customs outside the EU', 'change', 'P6_COUNTRY', 'in_list', 'US,JP', 'show', 'P6_CUSTOMS', null, null, null, null, null, null),
368
+ (6, 50, 'Free delivery in the Benelux', 'change', 'P6_COUNTRY', 'in_list', 'NL,BE', 'show', null, 'free-delivery', null, null, null, null, null),
369
+ (6, 60, 'At least one copy', 'change', 'P6_COPIES', 'less_equal', '0', 'show_error', 'P6_COPIES', null, null, null,
370
+ 'At least one copy, please.', null, null),
371
+ (6, 70, 'Copies are fine', 'change', 'P6_COPIES', 'greater', '0', 'clear_errors', 'P6_COPIES', null, null, null, null, null, null),
372
+ (6, 80, 'A draft submits the page', 'click', 'DRAFT', null, null, 'submit', null, null, null, null, null, null, null),
373
+ (6, 90, 'Ask before sending', 'before_submit', null, null, null, 'confirm', null, null, null, null, 'Submit the order form?', null, null),
374
+ (6, 100, 'A popup while leaving', 'page_unload', null, null, null, 'wait_popup', null, null, null, null, 'Loading the next page…', null, null),
375
+ (6, 110, 'The dialog page was closed', 'dialog_closed', '10', null, null, 'show_success', null, null, null, null,
376
+ 'The dialog page ran Close dialog: this page ran its Dialog closed action instead of reloading.', null, null),
377
+ -- page 7 Regions and the window
378
+ (7, 10, 'Shuffle the report', 'click', 'SHUFFLE', null, null, 'refresh_region', null, 'shuffled', null, null, null, null, null),
379
+ (7, 20, 'Dim it while it loads', 'before_refresh', 'shuffled', null, null, 'add_class', null, 'shuffled', null, null, null, 'is-muted', null),
380
+ (7, 30, 'Note the time', 'after_refresh', 'shuffled', null, null, 'set_value', 'P7_REFRESHED', null,
381
+ 'select to_char(clock_timestamp(), ''HH24:MI:SS'')', null, null, null, null),
382
+ (7, 40, 'The window changed size', 'resize', null, null, null, 'show_success', null, null, null, null,
383
+ 'The window changed size (Resize): the regions flowed into the new width.', null, null),
384
+ (7, 50, 'Close the tips when scrolling', 'scroll', null, null, null, 'close_region', null, 'tips', null, null, null, null, null),
385
+ (7, 55, 'Note the picked rows', 'selection_change', 'picks', null, null, 'show_success', null, null, null, null,
386
+ 'Selection change: the checked rows go into Picked with the next submit.', null, null),
387
+ (7, 60, 'Print the page', 'click', 'PRINT', null, null, 'print', null, null, null, null, null, null, null),
388
+ (7, 70, 'Share the page', 'click', 'SHARE', null, null, 'share', null, null, null, null, 'Dynamic actions in pgkiln, without JavaScript of your own.', null, null),
389
+ (7, 80, 'Download the products', 'click', 'DOWNLOAD', null, null, 'download', null, null, 'PRODUCTS_CSV', null, null, null, null),
390
+ (7, 90, 'Turn on notifications', 'click', 'NOTIFY', null, null, 'push_subscribe', null, null, null, null,
391
+ 'Notifications are on for this device.', null, null),
392
+ -- page 8 A tree
393
+ (8, 10, 'Remember the node', 'tree_selection_change', 'catalogue-tree', null, null, 'set_value', 'P8_NODE,P8_SHOWN', null, null, null, null, null, null),
394
+ (8, 20, 'Show its products', 'tree_selection_change', 'catalogue-tree', null, null, 'refresh_region', null, 'chosen', null, 'P8_NODE', null, null, null),
395
+ (8, 30, 'Open every node', 'click', 'EXPAND', null, null, 'expand_tree', null, 'catalogue-tree', null, null, null, null, null),
396
+ (8, 40, 'Close every node', 'click', 'COLLAPSE', null, null, 'collapse_tree', null, 'catalogue-tree', null, null, null, null, null),
397
+ -- page 9 Faceted search
398
+ (9, 10, 'The facets changed', 'facets_change', 'filters', null, null, 'show_success', null, null, null, null,
399
+ 'The facets changed (Facets change): the catalogue shows the matching products.', null, null),
400
+ (9, 20, 'The filter dialog', 'click', 'FILTER', null, null, 'open_report_dialog', null, 'catalogue', 'filter', null, null, null, null),
401
+ (9, 30, 'The columns dialog', 'click', 'COLUMNS', null, null, 'open_report_dialog', null, 'catalogue', 'columns', null, null, null, null),
402
+ (9, 40, 'The chart dialog', 'click', 'CHART', null, null, 'open_report_dialog', null, 'catalogue', 'chart', null, null, null, null),
403
+ -- page 10 A dialog page
404
+ (10, 10, 'Cancel the dialog', 'click', 'CANCEL', null, null, 'cancel_dialog', null, null, null, null, null, null, null),
405
+ (10, 20, 'Close the dialog', 'click', 'CLOSE', null, null, 'close_dialog', null, null, null, null, null, null, null),
406
+ -- page 11 AI assistant
407
+ (11, 10, 'Open the assistant', 'click', 'ASK', null, null, 'show_ai_assistant', null, 'assistant', null, null, null, null, null)
408
+ ) as d (page, seq, name, event, trig, ctype, cvalue, action, items, region, code, submit, message, classes, config);