pgkiln 0.84.0 → 0.86.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 (105) hide show
  1. package/db/migrations/155_mail_available.sql +23 -0
  2. package/docs/apex-feature-parity.md +2 -2
  3. package/docs/guide/03-builder.md +14 -5
  4. package/docs/guide/04-pages-and-regions.md +1 -1
  5. package/docs/guide/06-processing.md +7 -3
  6. package/docs/guide/07-dynamic-actions.md +3 -3
  7. package/docs/guide/09-reference.md +1 -0
  8. package/docs/guide/12-development.md +10 -1
  9. package/examples/gallery/README.md +23 -6
  10. package/examples/gallery/gallery.json +4 -0
  11. package/examples/gallery/samples/sample-approvals.sql +102 -11
  12. package/examples/gallery/samples/sample-calendar.sql +40 -5
  13. package/examples/gallery/samples/sample-dynamic-actions.sql +237 -9
  14. package/examples/gallery/samples/sample-grids.sql +35 -3
  15. package/examples/gallery/samples/sample-maps.sql +65 -7
  16. package/examples/gallery/samples/sample-processes.sql +466 -0
  17. package/examples/gallery/samples/sample-validations.sql +407 -0
  18. package/examples/gallery/samples/ux-patterns.sql +154 -14
  19. package/examples/hr/README.md +2 -0
  20. package/examples/hr/hr_80_notifications.sql +125 -0
  21. package/examples/hr/hr_81_vector_search_type.sql +33 -0
  22. package/package.json +1 -1
  23. package/public/app.css +8 -1
  24. package/public/app.js +28 -9
  25. package/src/app.ts +9 -0
  26. package/src/builder/ai-agents.ts +86 -85
  27. package/src/builder/ai-builder.ts +30 -30
  28. package/src/builder/ai.ts +93 -92
  29. package/src/builder/api.ts +47 -47
  30. package/src/builder/app-groups.ts +33 -32
  31. package/src/builder/appfromfile.ts +57 -58
  32. package/src/builder/appsheets.ts +33 -32
  33. package/src/builder/appwizard.ts +65 -65
  34. package/src/builder/arrange.ts +23 -22
  35. package/src/builder/assistant.ts +39 -38
  36. package/src/builder/automations.ts +32 -31
  37. package/src/builder/availability.ts +24 -24
  38. package/src/builder/backups.ts +30 -31
  39. package/src/builder/blueprints.ts +62 -61
  40. package/src/builder/create-page.ts +52 -50
  41. package/src/builder/cross-app.ts +37 -35
  42. package/src/builder/dataload.ts +2 -2
  43. package/src/builder/datasets.ts +107 -106
  44. package/src/builder/dependencies.ts +20 -19
  45. package/src/builder/designer.ts +36 -36
  46. package/src/builder/diagnostics.ts +58 -57
  47. package/src/builder/features.ts +39 -35
  48. package/src/builder/feedback.ts +23 -20
  49. package/src/builder/find-workspace.ts +28 -24
  50. package/src/builder/form-designer.ts +36 -35
  51. package/src/builder/gallery.ts +25 -29
  52. package/src/builder/globalization.ts +50 -49
  53. package/src/builder/i18n/de.ts +3453 -0
  54. package/src/builder/i18n/en.ts +3453 -0
  55. package/src/builder/i18n/es.ts +3453 -0
  56. package/src/builder/i18n/fr.ts +3453 -0
  57. package/src/builder/i18n/it.ts +3453 -0
  58. package/src/builder/i18n/ja.ts +3453 -0
  59. package/src/builder/i18n/ko.ts +3453 -0
  60. package/src/builder/i18n/nl.ts +3453 -0
  61. package/src/builder/i18n/pt.ts +3453 -0
  62. package/src/builder/i18n/zh-cn.ts +3453 -0
  63. package/src/builder/i18n/zh-tw.ts +3453 -0
  64. package/src/builder/i18n.ts +17 -3
  65. package/src/builder/json-sources.ts +63 -62
  66. package/src/builder/layouts.ts +12 -11
  67. package/src/builder/locks.ts +29 -28
  68. package/src/builder/login-page.ts +26 -25
  69. package/src/builder/mail.ts +148 -147
  70. package/src/builder/newapp.ts +15 -12
  71. package/src/builder/plugins.ts +47 -44
  72. package/src/builder/process-jobs.ts +11 -10
  73. package/src/builder/pwa.ts +22 -21
  74. package/src/builder/remote-databases.ts +55 -56
  75. package/src/builder/report-settings.ts +1 -1
  76. package/src/builder/reporter.ts +43 -42
  77. package/src/builder/rest-catalogs.ts +87 -86
  78. package/src/builder/rest.ts +9 -8
  79. package/src/builder/routes.ts +27 -27
  80. package/src/builder/search.ts +14 -13
  81. package/src/builder/static-files.ts +31 -32
  82. package/src/builder/subscriptions.ts +47 -45
  83. package/src/builder/supporting.ts +23 -18
  84. package/src/builder/team.ts +179 -179
  85. package/src/builder/telemetry.ts +59 -62
  86. package/src/builder/templates.ts +66 -65
  87. package/src/builder/themeroller.ts +60 -63
  88. package/src/builder/ui-defaults.ts +21 -20
  89. package/src/builder/ui.ts +2 -1
  90. package/src/builder/upgrade.ts +25 -24
  91. package/src/builder/utilities.ts +48 -47
  92. package/src/builder/vector-providers.ts +67 -66
  93. package/src/builder/websources.ts +43 -42
  94. package/src/builder/wizards.ts +95 -89
  95. package/src/builder/workflow-migrate.ts +30 -31
  96. package/src/builder/workflows.ts +40 -39
  97. package/src/builder/workingcopies.ts +41 -49
  98. package/src/builder/workspace-backups.ts +21 -20
  99. package/src/builder/workspace-mail.ts +17 -16
  100. package/src/builder/workspace-resources.ts +104 -112
  101. package/src/builder/workspace-transfer.ts +34 -26
  102. package/src/runtime/engine.ts +6 -3
  103. package/src/runtime/lists.ts +2 -2
  104. package/src/runtime/push.ts +7 -1
  105. package/src/runtime/regions.ts +9 -4
@@ -0,0 +1,466 @@
1
+ -- =====================================================================
2
+ -- Sample app "Sample Processes" (App Gallery → Sample Apps)
3
+ --
4
+ -- Page processing on the back office of a small pottery supplies shop:
5
+ -- page 1 application processes, application computations and
6
+ -- shortcuts fill in the home page;
7
+ -- page 2 the orders: page computations, Clear Session State and
8
+ -- User Preferences behind two buttons;
9
+ -- page 3 the order form in a modal dialog: Form – Initialization,
10
+ -- the row process and Close Dialog handing values back;
11
+ -- page 4 shipping: Server Side Geocoding, Send E-Mail and Send Push
12
+ -- Notification, Print Report, branches to a URL and to a page
13
+ -- of an application;
14
+ -- page 5 the outbox: what shipping queued;
15
+ -- page 6 report queries in six formats;
16
+ -- page 7 Ajax callbacks: a page process and an application process;
17
+ -- page 8 tracking an order from a link without a checksum
18
+ -- (page protection unrestricted);
19
+ -- page 9 an editable grid whose Save runs Reset Pagination.
20
+ -- Sign-in is custom authentication: a PL/pgSQL function checks the shop's
21
+ -- own staff table (account demo / demo); any user it accepts may use the
22
+ -- application (access control "any user").
23
+ --
24
+ -- Installed by an administrator from the App Gallery, or with
25
+ -- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-processes.sql
26
+ -- then open /a/sample-processes and sign in as demo / demo.
27
+ -- =====================================================================
28
+
29
+ create or replace function pg_temp.page_id(p_alias text, p_page int) returns int language sql stable as
30
+ $$ select p.id from meta.page p join meta.app a on a.id = p.app_id where a.alias = p_alias and p.page_no = p_page $$;
31
+ create or replace function pg_temp.region_id(p_alias text, p_page int, p_static text) returns int language sql stable as
32
+ $$ select r.id from meta.region r where r.page_id = pg_temp.page_id(p_alias, p_page) and r.static_id = p_static $$;
33
+
34
+ -- 1. Schema and data ---------------------------------------------------------
35
+ create schema sample_processes;
36
+
37
+ -- the shop's staff: the custom authentication checks this table, not pgkiln's accounts
38
+ create table sample_processes.staff (
39
+ username text primary key check (username = lower(username)),
40
+ display_name text not null,
41
+ email text not null,
42
+ password_hash text not null,
43
+ active boolean not null default true,
44
+ last_login_at timestamptz
45
+ );
46
+
47
+ create table sample_processes.product (
48
+ id int generated always as identity primary key,
49
+ name text not null unique,
50
+ price numeric(10, 2) not null check (price > 0),
51
+ stock int not null default 0 check (stock >= 0)
52
+ );
53
+
54
+ create table sample_processes.orders (
55
+ id int generated always as identity primary key,
56
+ customer text not null,
57
+ email text not null,
58
+ address text not null,
59
+ product_id int not null references sample_processes.product,
60
+ qty int not null default 1 check (qty > 0),
61
+ status text not null default 'NEW' check (status in ('NEW', 'SHIPPED')),
62
+ location text,
63
+ ordered_at timestamptz not null default now(),
64
+ shipped_at timestamptz,
65
+ shipped_by text
66
+ );
67
+
68
+ -- what shipping queued: the mail and push queues are pgkiln's, out of the application's reach
69
+ create table sample_processes.outbox (
70
+ id int generated always as identity primary key,
71
+ at timestamptz not null default now(),
72
+ order_id int not null references sample_processes.orders on delete cascade,
73
+ channel text not null,
74
+ recipient text not null,
75
+ outcome text not null
76
+ );
77
+
78
+ insert into sample_processes.staff (username, display_name, email, password_hash)
79
+ values ('demo', 'Dana Demo', 'demo@example.com', meta.hash_password('demo'));
80
+
81
+ insert into sample_processes.product (name, price, stock) values
82
+ ('Stoneware clay 10 kg', 18.50, 40), ('Porcelain clay 10 kg', 32.00, 12), ('Celadon glaze 1 l', 24.95, 8),
83
+ ('Tenmoku glaze 1 l', 22.50, 0), ('Pottery wheel bats (5)', 29.00, 15), ('Trimming tool set', 14.75, 25);
84
+
85
+ insert into sample_processes.orders (customer, email, address, product_id, qty, status, location, ordered_at, shipped_at, shipped_by)
86
+ select o.customer, o.email, o.address, p.id, o.qty, o.status, o.location, now() - o.age, case when o.status = 'SHIPPED' then now() - o.age + interval '1 day' end,
87
+ case when o.status = 'SHIPPED' then 'Dana Demo' end
88
+ from (values
89
+ ('Studio Aarde', 'orders@studio-aarde.example', 'Oudegracht 120, Utrecht', 'Stoneware clay 10 kg', 4, 'SHIPPED', '52.090100,5.119900', interval '9 days'),
90
+ ('Mira Jansen', 'mira@example.com', 'Dam 1, Amsterdam', 'Celadon glaze 1 l', 2, 'SHIPPED', '52.373100,4.893200', interval '7 days'),
91
+ ('De Draaischijf', 'info@draaischijf.example', 'Grote Markt 1, Groningen', 'Pottery wheel bats (5)', 1, 'NEW', null, interval '3 days'),
92
+ ('Tom de Vries', 'tom@example.com', 'Coolsingel 40, Rotterdam', 'Porcelain clay 10 kg', 2, 'NEW', null, interval '2 days'),
93
+ ('Keramiekhuis Zuid', 'zuid@keramiekhuis.example', 'Vrijthof 1, Maastricht', 'Trimming tool set', 3, 'NEW', null, interval '1 day'),
94
+ ('Lotte Bakker', 'lotte@example.com', 'Markt 1, Delft', 'Stoneware clay 10 kg', 1, 'NEW', null, interval '5 hours')
95
+ ) as o (customer, email, address, product, qty, status, location, age)
96
+ join sample_processes.product p on p.name = o.product
97
+ order by o.age desc;
98
+
99
+ insert into sample_processes.outbox (at, order_id, channel, recipient, outcome)
100
+ select o.shipped_at, o.id, 'e-mail', o.email, 'queued (seed data)' from sample_processes.orders o where o.status = 'SHIPPED';
101
+
102
+ -- the custom authentication: does the user name and password match an active member of the staff?
103
+ -- security definer, so the application's role never reads the password hashes; crypt() comes from
104
+ -- pgcrypto: created with the installer's search path (the body is checked then), then pinned below to
105
+ -- pgcrypto's own schema, wherever the installation put it
106
+ create function sample_processes.check_login(p_username text, p_password text) returns boolean
107
+ language sql stable security definer set search_path from current as $$
108
+ select coalesce((select s.active and s.password_hash = crypt(p_password, s.password_hash)
109
+ from sample_processes.staff s where s.username = lower(p_username)), false)
110
+ $$;
111
+ revoke execute on function sample_processes.check_login(text, text) from public;
112
+ do $$ begin
113
+ execute format('alter function sample_processes.check_login(text, text) set search_path = %I, pg_catalog',
114
+ (select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pgcrypto'));
115
+ end $$;
116
+
117
+ -- 2. The application's database role ----------------------------------------
118
+ do $$ begin
119
+ if not exists (select 1 from pg_roles where rolname = 'app_sample_processes') then create role app_sample_processes nologin; end if;
120
+ end $$;
121
+ grant app_sample_processes to pgkiln_runtime;
122
+ grant usage on schema sample_processes to app_sample_processes;
123
+ grant select (username, display_name, email, active, last_login_at), update (last_login_at) on sample_processes.staff to app_sample_processes;
124
+ grant select, insert, update, delete on sample_processes.product to app_sample_processes;
125
+ grant select, insert, update, delete on sample_processes.orders to app_sample_processes;
126
+ grant select, insert on sample_processes.outbox to app_sample_processes;
127
+ grant execute on function sample_processes.check_login(text, text) to app_sample_processes;
128
+
129
+ -- 3. The application ----------------------------------------------------------
130
+ -- custom authentication with the staff table; any user it signs in may use the application;
131
+ -- always in the primary language; push notifications on, so Send Push Notification can queue
132
+ insert into meta.app (alias, name, home_page, authentication, custom_auth_function, custom_auth_post_code,
133
+ access_control, language, language_from, db_role, pwa, pwa_push, pwa_short_name)
134
+ values ('sample-processes', 'Sample Processes', 1, 'custom', 'sample_processes.check_login',
135
+ 'update sample_processes.staff set last_login_at = now() where username = lower(p_username);',
136
+ 'any_user', 'en', 'primary', 'app_sample_processes', true, true, 'Processes');
137
+
138
+ insert into meta.page (app_id, page_no, name, title, parent_page, mode, protection)
139
+ select a.id, p.no, p.name, p.name, p.parent, p.mode, p.protection
140
+ from meta.app a, (values
141
+ (1, 'Sample Processes', null, 'normal', 'checksum'), (4, 'Ship an order', 1, 'normal', 'checksum'),
142
+ (5, 'Outbox', 1, 'normal', 'checksum'), (6, 'Downloads', 1, 'normal', 'checksum'),
143
+ (7, 'Ajax callbacks', 1, 'normal', 'checksum'), (8, 'Track an order', 1, 'normal', 'unrestricted')
144
+ ) as p (no, name, parent, mode, protection)
145
+ where a.alias = 'sample-processes';
146
+
147
+ -- page 2 and its form, page 3 (a modal dialog): the Report and form wizard on the orders
148
+ select meta.generate_page('sample-processes', 'report_form', 'sample_processes.orders', 2, '{"label": "Orders", "form_page": 3, "nav": false}');
149
+ -- page 9: an editable grid on the products (the Interactive grid wizard)
150
+ select meta.generate_page('sample-processes', 'grid', 'sample_processes.product', 9, '{"label": "Stock", "nav": false}');
151
+ update meta.page set parent_page = 1 where app_id = (select id from meta.app where alias = 'sample-processes') and page_no in (2, 9);
152
+
153
+ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
154
+ select a.id, n.seq, n.label, n.icon, n.page
155
+ from meta.app a, (values
156
+ (10, 'Home', 'home', 1), (20, 'Orders', 'list', 2), (30, 'Ship an order', 'truck', 4), (40, 'Outbox', 'send', 5),
157
+ (50, 'Downloads', 'download', 6), (60, 'Ajax callbacks', 'bolt', 7), (70, 'Track an order', 'search', 8), (80, 'Stock', 'grid', 9)
158
+ ) as n (seq, label, icon, page)
159
+ where a.alias = 'sample-processes';
160
+
161
+ -- shared components: application items, computations, processes, shortcuts, report queries
162
+ insert into meta.app_item (app_id, name, description)
163
+ select a.id, i.name, i.description
164
+ from meta.app a, (values
165
+ ('AI_DISPLAY_NAME', 'The staff member''s name, looked up once at sign-in'),
166
+ ('AI_DEFAULT_STATUS', 'The status the order list shows when you have no saved filter'),
167
+ ('AI_TODAY', 'Today, written out'),
168
+ ('AI_GREETING', 'Good morning, afternoon or evening'),
169
+ ('AI_SIGNED_IN_AS', 'A copy of APP_USER'),
170
+ ('AI_OPEN_ORDERS', 'The number of orders to ship, counted before every page')
171
+ ) as i (name, description)
172
+ where a.alias = 'sample-processes';
173
+
174
+ insert into meta.app_computation (app_id, seq, item_name, point, type, expression, comments)
175
+ select a.id, c.seq, c.item, c.point, c.type, c.expr, c.comments
176
+ from meta.app a, (values
177
+ (10, 'AI_DISPLAY_NAME', 'after_authentication', 'sql_query',
178
+ 'select display_name from sample_processes.staff where username = lower(:APP_USER)', 'Once, at sign-in: the first column of the first row'),
179
+ (20, 'AI_DEFAULT_STATUS', 'after_authentication', 'static', 'NEW', 'Once, at sign-in: a fixed value'),
180
+ (30, 'AI_TODAY', 'before_header', 'sql_expression', 'to_char(current_date, ''FMDay FMDD FMMonth YYYY'')', 'Every page: a SQL expression'),
181
+ (40, 'AI_GREETING', 'before_header', 'function_body',
182
+ E'begin\n return case when localtime < time ''12:00'' then ''Good morning''\n when localtime < time ''18:00'' then ''Good afternoon''\n else ''Good evening'' end;\nend',
183
+ 'Every page: a PL/pgSQL function body'),
184
+ (50, 'AI_SIGNED_IN_AS', 'before_header', 'item', 'APP_USER', 'Every page: the value of another item')
185
+ ) as c (seq, item, point, type, expr, comments)
186
+ where a.alias = 'sample-processes';
187
+
188
+ insert into meta.app_process (app_id, seq, name, point, code)
189
+ select a.id, p.seq, p.name, p.point, p.code
190
+ from meta.app a, (values
191
+ -- before every page view and every submit: a column named like an application item sets it
192
+ (10, 'Count the open orders', 'before_page',
193
+ 'select count(*) as ai_open_orders from sample_processes.orders where status = ''NEW'''),
194
+ -- an Ajax callback every page can call by name (pgkiln.server.process('SERVER_CLOCK'))
195
+ (20, 'SERVER_CLOCK', 'ajax',
196
+ 'select to_char(clock_timestamp(), ''HH24:MI:SS'') as now, :AI_GREETING || '', '' || coalesce(:AI_DISPLAY_NAME, :APP_USER) as greeting, :AI_OPEN_ORDERS as open_orders')
197
+ ) as p (seq, name, point, code)
198
+ where a.alias = 'sample-processes';
199
+
200
+ insert into meta.shortcut (app_id, name, type, value, comments)
201
+ select a.id, s.name, s.type, s.value, s.comments
202
+ from meta.app a, (values
203
+ ('SHOP_NAME', 'text', 'Kiln & Clay Supplies', 'Text: HTML-escaped where it is used'),
204
+ ('SUPPORT_CONTACT', 'html', 'Questions? Mail <a href="mailto:support@example.com">support@example.com</a>.', 'HTML, used as it is'),
205
+ ('OPEN_ORDERS_LINE', 'function_body',
206
+ 'return format(''%s of %s orders are waiting to be shipped.'', count(*) filter (where status = ''NEW''), count(*)) from sample_processes.orders;',
207
+ 'Computed by PL/pgSQL once per request'),
208
+ ('DELETE_ORDER', 'text', 'Delete this order? The customer gets nothing.', 'The Delete button''s confirmation')
209
+ ) as s (name, type, value, comments)
210
+ where a.alias = 'sample-processes';
211
+
212
+ insert into meta.report_query (app_id, name, description, query, format, file_name, title)
213
+ select a.id, q.name, q.description, q.query, q.format, q.file_name, q.title
214
+ from meta.app a, (values
215
+ ('ORDERS_CSV', 'The orders as CSV', 'select o.id, o.customer, o.email, p.name as product, o.qty, o.status, o.ordered_at from sample_processes.orders o join sample_processes.product p on p.id = o.product_id order by o.id', 'csv', 'orders', null),
216
+ ('ORDERS_XLSX', 'The orders with their totals, for Excel', 'select o.id, o.customer, p.name as product, o.qty, p.price, o.qty * p.price as total, o.status, o.ordered_at from sample_processes.orders o join sample_processes.product p on p.id = o.product_id order by o.id', 'xlsx', 'orders', null),
217
+ ('ORDERS_JSON', 'The orders as JSON', 'select o.id, o.customer, p.name as product, o.qty, o.status from sample_processes.orders o join sample_processes.product p on p.id = o.product_id order by o.id', 'json', 'orders', null),
218
+ ('ORDERS_XML', 'The orders as XML', 'select o.id, o.customer, p.name as product, o.qty, o.status from sample_processes.orders o join sample_processes.product p on p.id = o.product_id order by o.id', 'xml', 'orders', null),
219
+ ('OPEN_ORDERS_HTML', 'What is left to ship, as a web page', 'select o.id, o.customer, o.address, p.name as product, o.qty from sample_processes.orders o join sample_processes.product p on p.id = o.product_id where o.status = ''NEW'' order by o.id', 'html', 'open-orders', 'Orders to ship'),
220
+ ('PRICE_LIST', 'The price list', 'select name as product, price, stock from sample_processes.product order by name', 'pdf', 'price-list', 'Price list, &AI_TODAY.'),
221
+ ('PACKING_SLIP', 'The packing slip of the order chosen on page 4', 'select o.id as "Order", o.customer, o.address, p.name as product, o.qty from sample_processes.orders o join sample_processes.product p on p.id = o.product_id where o.id = nullif(:P4_ORDER, '''')::int', 'pdf', 'packing-slip-&P4_ORDER.', 'Packing slip')
222
+ ) as q (name, description, query, format, file_name, title)
223
+ where a.alias = 'sample-processes';
224
+
225
+ -- the Ajax callbacks of page 7 are called from this file, loaded by every page
226
+ insert into meta.static_file (app_id, name, mime, content)
227
+ select a.id, 'sample-processes.js', 'text/javascript', convert_to($js$// Sample Processes: functions for "Execute JavaScript" dynamic actions (page 7).
228
+ // pgkiln.server.process runs an Ajax callback by name and sets the items it returns.
229
+ pgkiln.actions.register('sp.checkStock', async () => {
230
+ const r = await pgkiln.server.process('CHECK_STOCK', { items: ['P7_PRODUCT'] });
231
+ if (r.data) pgkiln.showSuccess(r.data.message);
232
+ else pgkiln.showError('Choose a product first.', 'P7_PRODUCT');
233
+ });
234
+ pgkiln.actions.register('sp.serverClock', async () => {
235
+ const r = await pgkiln.server.process('SERVER_CLOCK');
236
+ pgkiln.showSuccess(`${r.data.greeting}: the server's clock says ${r.data.now}, and ${r.data.open_orders} orders wait to be shipped.`);
237
+ });
238
+ $js$, 'utf8')
239
+ from meta.app a where a.alias = 'sample-processes';
240
+ update meta.app set static_includes = '{sample-processes.js}' where alias = 'sample-processes';
241
+
242
+ -- 4. Pages ---------------------------------------------------------------------
243
+ -- the wizard's report: product and total instead of the product's id
244
+ update meta.region set source = 'select o.id, o.customer, p.name as product, o.qty, o.qty * p.price as total, o.status, o.ordered_at, o.shipped_by
245
+ from sample_processes.orders o join sample_processes.product p on p.id = o.product_id
246
+ where (:P2_STATUS = ''ALL'' or o.status = :P2_STATUS)
247
+ and (:P2_SEARCH is null or o.customer ilike ''%'' || :P2_SEARCH || ''%'')
248
+ order by o.id desc',
249
+ config = config || '{"formats": {"total": "999G990D00"}}'
250
+ where page_id = pg_temp.page_id('sample-processes', 2) and type = 'report';
251
+ update meta.region set static_id = 'orders' where page_id = pg_temp.page_id('sample-processes', 2) and type = 'report';
252
+
253
+ -- the form: what happened to the order is shown, not edited
254
+ update meta.item set type = 'display'
255
+ where page_id = pg_temp.page_id('sample-processes', 3) and name in ('P3_STATUS', 'P3_LOCATION', 'P3_ORDERED_AT', 'P3_SHIPPED_AT', 'P3_SHIPPED_BY');
256
+ update meta.item set type = 'select', lov = 'select name, id from sample_processes.product order by name'
257
+ where page_id = pg_temp.page_id('sample-processes', 3) and name = 'P3_PRODUCT_ID';
258
+ update meta.button set confirm = '"DELETE_ORDER"'
259
+ where page_id = pg_temp.page_id('sample-processes', 3) and name = 'DELETE';
260
+
261
+ insert into meta.list (app_id, name, type, description)
262
+ select id, 'PROCESS_TOPICS', 'static', 'The pages of Sample Processes' from meta.app where alias = 'sample-processes';
263
+
264
+ insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description)
265
+ select a.id, 'PROCESS_TOPICS', e.seq, e.label, e.icon, e.page, e.description
266
+ from meta.app a, (values
267
+ (10, 'Orders', 'list', 2, 'Computations fill in the filter; buttons reset the pagination, clear the filter and remember it for you.'),
268
+ (20, 'Ship an order', 'truck', 4, 'Geocode the address, mail the customer, push to your devices, print a packing slip, then branch on.'),
269
+ (30, 'Outbox', 'send', 5, 'What shipping queued, by e-mail and by push notification.'),
270
+ (40, 'Downloads', 'download', 6, 'Report queries: the same rows as CSV, Excel, JSON, XML, HTML and PDF.'),
271
+ (50, 'Ajax callbacks', 'bolt', 7, 'JavaScript asks a page process and an application process by name.'),
272
+ (60, 'Track an order', 'search', 8, 'A page that takes its order number from any link: protection Unrestricted.'),
273
+ (70, 'Stock', 'grid', 9, 'An editable grid whose Save starts it on its first page again: Reset Pagination.')
274
+ ) as e (seq, label, icon, page, description)
275
+ where a.alias = 'sample-processes';
276
+
277
+ insert into meta.region (page_id, seq, title, type, columns, template, source, config, static_id)
278
+ select pg_temp.page_id('sample-processes', r.page), r.seq, r.title, r.type, r.cols, r.template, r.source, r.config::jsonb, r.sid
279
+ from (values
280
+ -- 1 Home
281
+ (1, 10, 'Sample Processes', 'static', 12, 'hero',
282
+ '<p>What happens on the server when a page is shown or submitted, without code in the browser: <strong>processes</strong> (SQL, and declarative types such as Send E-Mail or Print Report), <strong>computations</strong> that set items, <strong>branches</strong> that decide where to go next, and the same for the whole application. The shop is "SHOP_NAME"; "OPEN_ORDERS_LINE"</p>',
283
+ '{"icon": "settings"}', null),
284
+ (1, 20, 'Topics', 'list', 12, 'plain', null, '{"list": "PROCESS_TOPICS", "template": "cards"}', null),
285
+ (1, 30, 'Made by the application', 'static', 6, 'standard',
286
+ '<p>&AI_GREETING., <strong>&AI_DISPLAY_NAME.</strong>. Today is &AI_TODAY.; you are signed in as <code>&AI_SIGNED_IN_AS.</code>, and &AI_OPEN_ORDERS. orders wait to be shipped.</p><ul><li><em>&AI_DISPLAY_NAME.</em> and the order list''s default status (&AI_DEFAULT_STATUS.): application computations run once at sign-in (<code>sql_query</code>, <code>static</code>)</li><li><em>&AI_TODAY.</em>, the greeting and the user name: application computations before every page (<code>sql_expression</code>, <code>function_body</code>, <code>item</code>)</li><li>the number of open orders: an application process before every page (<code>before_page</code>)</li></ul>',
287
+ '{}', 'made'),
288
+ (1, 40, 'Shortcuts and sign-in', 'static', 6, 'standard',
289
+ '<p>The quoted names in this page''s texts are <strong>shortcuts</strong>: "SHOP_NAME" is text, the next line is HTML and the open orders above come from a PL/pgSQL function body.</p><p>"SUPPORT_CONTACT"</p><p>You signed in through <strong>custom authentication</strong>: the function <code>sample_processes.check_login</code> checks the shop''s own staff table, and the post-authentication code records the time (<code>last_login_at</code>). Access control is <em>any user</em>: everyone the function accepts may use the application. Track <a href="8?P8_ID=3">order 3</a> or <a href="8?P8_ID=1">order 1</a>: plain links, no checksum.</p>',
290
+ '{}', 'shortcuts'),
291
+
292
+ -- 2 Orders
293
+ (2, 5, 'About this page', 'static', 12, 'plain',
294
+ '<p class="lead">Before the page is shown, <strong>computations</strong> fill in the status: your saved filter (a <code>sql_query</code> on <code>meta.get_preference</code>), else the application item <code>AI_DEFAULT_STATUS</code> (type <code>item</code>), and the line under it (<code>static</code>). <strong>Filter</strong> runs <em>User Preferences</em>: the status is remembered for you, also in your next session; <strong>Clear</strong> runs <em>Clear Session State</em> and forgets the preference. Editing an order opens a modal dialog whose <em>Close Dialog</em> process hands the order number back to this page.</p>',
295
+ '{}', null),
296
+ (2, 7, 'Filter', 'static', 12, 'standard', null, '{}', 'filter'),
297
+
298
+ -- 4 Ship an order
299
+ (4, 5, 'About this page', 'static', 12, 'plain',
300
+ '<p class="lead">Choose an order: <code>sql_query</code> computations fill in its address and e-mail, an <code>item</code> computation your name, and after a submit a <code>static</code> one the note when it is empty. <strong>Locate</strong> runs <em>Server Side Geocoding</em> (the installation''s geocoder, <code>GEOCODER_URL</code>; without one it says so). <strong>Ship</strong> updates the order, queues a mail to the customer (<em>Send E-Mail</em>, with the condition <code>meta.mail_available()</code>: only when the application has a mail service) and a notification to your devices (<em>Send Push Notification</em>), clears the page and branches to the outbox (a branch to a page of an application). <strong>Packing slip</strong> answers with the report query <code>PACKING_SLIP</code> as a PDF (<em>Print Report</em>). When nothing is left to ship, a branch before the page is shown goes to the order list (a URL).</p>',
301
+ '{}', null),
302
+ (4, 10, 'Order to ship', 'static', 12, 'standard', null, '{}', 'ship'),
303
+
304
+ -- 5 Outbox
305
+ (5, 5, 'About this page', 'static', 12, 'plain',
306
+ '<p class="lead">Send E-Mail and Send Push Notification <strong>queue</strong> a message in the submit''s transaction; the server sends it after the commit. The queues are pgkiln''s (an administrator sees them under Administration and the application''s Activity), so after each one the Ship button records here what it queued: a mail only when the application has a mail service, a push message for every device of yours that turned notifications on (none: nothing arrives, and nothing fails).</p>',
307
+ '{}', null),
308
+ (5, 10, 'Outbox', 'report', 12, 'standard',
309
+ 'select b.at, b.order_id as "Order", o.customer, b.channel, b.recipient, b.outcome
310
+ from sample_processes.outbox b join sample_processes.orders o on o.id = b.order_id
311
+ order by b.id desc', '{"empty": "Nothing queued yet: ship an order."}', 'outbox'),
312
+
313
+ -- 6 Downloads
314
+ (6, 5, 'About this page', 'static', 12, 'plain',
315
+ '<p class="lead"><strong>Report queries</strong> (Shared Components) are named queries printed without a template: each button below has the action <em>Download document</em> and names one. The same file is at <code>?doc=NAME</code> on any page, e.g. <a href="6?doc=ORDERS_CSV">6?doc=ORDERS_CSV</a>. The packing slip on <em>Ship an order</em> is a report query too, sent by a <em>Print Report</em> process.</p>',
316
+ '{}', null),
317
+ (6, 10, 'Download', 'static', 12, 'buttons_container', null, '{}', 'downloads'),
318
+
319
+ -- 7 Ajax callbacks
320
+ (7, 5, 'About this page', 'static', 12, 'plain',
321
+ '<p class="lead">A process with point <strong>ajax</strong> runs only when JavaScript calls it by name, <code>pgkiln.server.process(''CHECK_STOCK'', {items: [''P7_PRODUCT'']})</code>, and answers JSON; the columns named like items set them. <em>Check stock</em> calls this page''s process; <em>Ask the server</em> calls an application process with point ajax, which every page can call. The calls live in the static application file <code>sample-processes.js</code>, run by <em>Execute JavaScript</em> dynamic actions.</p>',
322
+ '{}', null),
323
+ (7, 10, 'Stock', 'static', 6, 'standard', null, '{}', 'stock'),
324
+ (7, 20, 'The server', 'static', 6, 'standard',
325
+ '<p>An application process answers with the server''s clock, your greeting and the number of open orders.</p>', '{}', 'clock'),
326
+
327
+ -- 9 Stock (the wizard's grid follows at seq 10)
328
+ (9, 5, 'About this page', 'static', 12, 'plain',
329
+ '<p class="lead">The Interactive grid wizard made this grid and its <code>grid_dml</code> process. A grid''s Save normally comes back to the page of rows you were on; here a <em>Reset Pagination</em> process runs on Save too, so the grid starts on its first page again. Go to the second page, change a stock and press <strong>Save</strong>.</p>',
330
+ '{}', null),
331
+
332
+ -- 8 Track an order
333
+ (8, 5, 'About this page', 'static', 12, 'plain',
334
+ '<p class="lead">This page''s protection is <strong>Unrestricted</strong>: a link may set <code>P8_ID</code> without a checksum, e.g. <a href="8?P8_ID=2">8?P8_ID=2</a>, as a link in a mail would. Other pages have the default, <em>Arguments must have checksum</em>: their links come from pgkiln, signed.</p>',
335
+ '{}', null),
336
+ (8, 10, 'Which order?', 'static', 12, 'standard', null, '{}', 'which'),
337
+ (8, 20, 'The order', 'report', 12, 'standard',
338
+ 'select o.id as "Order", o.customer, p.name as product, o.qty, o.status, o.ordered_at, o.shipped_at, o.location
339
+ from sample_processes.orders o join sample_processes.product p on p.id = o.product_id
340
+ where o.id::text = :P8_ID', '{"empty": "There is no order with that number."}', 'order')
341
+ ) as r (page, seq, title, type, cols, template, source, config, sid);
342
+
343
+ update meta.region set condition = ':P8_ID is not null' where id = pg_temp.region_id('sample-processes', 8, 'order');
344
+
345
+ insert into meta.item (page_id, region_id, seq, name, label, type, lov, default_value, help, config)
346
+ select pg_temp.page_id('sample-processes', i.page), pg_temp.region_id('sample-processes', i.page, i.region),
347
+ i.seq, i.name, i.label, i.type, i.lov, i.def, i.help, i.config::jsonb
348
+ from (values
349
+ (2, 'filter', 10, 'P2_STATUS', 'Status', 'select', 'STATIC:All orders;ALL,To ship;NEW,Shipped;SHIPPED', null, null, '{"null_label": false}'),
350
+ (2, 'filter', 20, 'P2_SEARCH', 'Customer', 'text', null, null, null, '{}'),
351
+ (2, 'filter', 30, 'P2_SHOWING', 'Showing', 'display', null, null, null, '{}'),
352
+ (2, 'filter', 40, 'P3_ID', 'Last order in the dialog', 'display', null, null, 'Handed back by the dialog''s Close Dialog process.', '{}'),
353
+ (4, 'ship', 10, 'P4_ORDER', 'Order', 'select',
354
+ 'select format(''%s: %s, %s × %s'', o.id, o.customer, o.qty, p.name), o.id from sample_processes.orders o join sample_processes.product p on p.id = o.product_id where o.status = ''NEW'' order by o.id',
355
+ null, null, '{"submit_on_change": true, "null_label": "- Choose an order -"}'),
356
+ (4, 'ship', 20, 'P4_ADDRESS', 'Address', 'display', null, null, null, '{}'),
357
+ (4, 'ship', 30, 'P4_EMAIL', 'E-mail', 'display', null, null, null, '{}'),
358
+ (4, 'ship', 40, 'P4_LOCATION', 'Coordinates', 'text', null, null, 'Filled in by Locate (lat,lng).', '{}'),
359
+ (4, 'ship', 50, 'P4_SHIPPED_BY', 'Shipped by', 'text', null, null, null, '{}'),
360
+ (4, 'ship', 60, 'P4_NOTE', 'Note for the customer', 'textarea', null, null, 'Empty: a computation after the submit fills in a standard note.', '{"rows": 2}'),
361
+ (7, 'stock', 10, 'P7_PRODUCT', 'Product', 'select', 'select name, id from sample_processes.product order by name', null, null, '{"warn_unsaved": false}'),
362
+ (7, 'stock', 20, 'P7_STOCK', 'In stock', 'text', null, null, 'Set by the Ajax callback.', '{"warn_unsaved": false}'),
363
+ (8, 'which', 10, 'P8_ID', 'Order number', 'text', null, null, null, '{}')
364
+ ) as i (page, region, seq, name, label, type, lov, def, help, config);
365
+
366
+ insert into meta.button (page_id, region_id, seq, name, label, action, hot, document, condition)
367
+ select pg_temp.page_id('sample-processes', b.page), pg_temp.region_id('sample-processes', b.page, b.region),
368
+ b.seq, b.name, b.label, b.action, b.hot, b.doc, b.cond
369
+ from (values
370
+ (2, 'filter', 10, 'FILTER', 'Filter', 'submit', true, null, null),
371
+ (2, 'filter', 20, 'CLEAR', 'Clear', 'submit', false, null, null),
372
+ (4, 'ship', 10, 'SHIP', 'Ship', 'submit', true, null, ':P4_ORDER is not null'),
373
+ (4, 'ship', 20, 'LOCATE', 'Locate', 'submit', false, null, ':P4_ORDER is not null'),
374
+ (4, 'ship', 30, 'PACKING_SLIP', 'Packing slip', 'submit', false, null, ':P4_ORDER is not null'),
375
+ (6, 'downloads', 10, 'CSV', 'Orders (CSV)', 'document', false, 'ORDERS_CSV', null),
376
+ (6, 'downloads', 20, 'XLSX', 'Orders (Excel)', 'document', false, 'ORDERS_XLSX', null),
377
+ (6, 'downloads', 30, 'JSON', 'Orders (JSON)', 'document', false, 'ORDERS_JSON', null),
378
+ (6, 'downloads', 40, 'XML', 'Orders (XML)', 'document', false, 'ORDERS_XML', null),
379
+ (6, 'downloads', 50, 'HTML', 'To ship (HTML)', 'document', false, 'OPEN_ORDERS_HTML', null),
380
+ (6, 'downloads', 60, 'PDF', 'Price list (PDF)', 'document', true, 'PRICE_LIST', null),
381
+ (7, 'stock', 10, 'CHECK', 'Check stock', 'da', true, null, null),
382
+ (7, 'clock', 10, 'CLOCK', 'Ask the server', 'da', false, null, null),
383
+ (8, 'which', 10, 'TRACK', 'Track', 'submit', true, null, null)
384
+ ) as b (page, region, seq, name, label, action, hot, doc, cond);
385
+
386
+ -- page computations
387
+ insert into meta.computation (page_id, seq, item_name, point, type, expression, condition_type, condition_expr)
388
+ select pg_temp.page_id('sample-processes', c.page), c.seq, c.item, c.point, c.type, c.expr, c.ctype, c.cexpr
389
+ from (values
390
+ (2, 10, 'P2_STATUS', 'before_header', 'sql_query', 'select meta.get_preference(''ORDERS_STATUS'')', 'item_null', 'P2_STATUS'),
391
+ (2, 20, 'P2_STATUS', 'before_header', 'item', 'AI_DEFAULT_STATUS', 'item_null', 'P2_STATUS'),
392
+ (2, 30, 'P2_SHOWING', 'before_header', 'static', 'Status &P2_STATUS., newest first; your saved filter is used when you come back.', null, null),
393
+ (4, 10, 'P4_ADDRESS', 'before_header', 'sql_query', 'select address from sample_processes.orders where id = :P4_ORDER::int', 'item_not_null', 'P4_ORDER'),
394
+ (4, 20, 'P4_EMAIL', 'before_header', 'sql_query', 'select email from sample_processes.orders where id = :P4_ORDER::int', 'item_not_null', 'P4_ORDER'),
395
+ (4, 30, 'P4_LOCATION', 'before_header', 'sql_query', 'select location from sample_processes.orders where id = :P4_ORDER::int', 'item_not_null', 'P4_ORDER'),
396
+ (4, 40, 'P4_SHIPPED_BY', 'before_header', 'item', 'AI_DISPLAY_NAME', 'item_null', 'P4_SHIPPED_BY'),
397
+ (4, 50, 'P4_NOTE', 'after_submit', 'static', 'Thank you for your order!', 'item_null', 'P4_NOTE')
398
+ ) as c (page, seq, item, point, type, expr, ctype, cexpr);
399
+
400
+ -- page processes
401
+ insert into meta.process (page_id, seq, name, type, point, when_button, code, config, success_message, condition_type, condition_expr)
402
+ select pg_temp.page_id('sample-processes', p.page), p.seq, p.name, p.type, p.point, p.button, p.code, p.config::jsonb, p.message, p.ctype, p.cexpr
403
+ from (values
404
+ -- 2 Orders
405
+ (2, 10, 'Remember the status', 'user_preferences', 'submit', 'FILTER', null,
406
+ '{"action": "set", "preference": "ORDERS_STATUS", "item": "P2_STATUS"}', 'Filter saved: you see these orders when you come back.', null, null),
407
+ (2, 30, 'Clear the filter', 'clear_session_state', 'submit', 'CLEAR', null, '{"items": ["P2_STATUS", "P2_SEARCH"]}', null, null, null),
408
+ (2, 40, 'Forget the status', 'user_preferences', 'submit', 'CLEAR', null,
409
+ '{"action": "reset", "preference": "ORDERS_STATUS"}', 'Filter cleared: back to the default status.', null, null),
410
+ -- 3 Order (the dialog): the wizard made the row process
411
+ (3, 1, 'Initialize form', 'form_init', 'load', null, null, '{}', null, null, null),
412
+ (3, 90, 'Close the dialog', 'close_dialog', 'submit', null, null, '{"items": ["P3_ID"]}', null, null, null),
413
+ -- 4 Ship an order
414
+ (4, 10, 'Locate the address', 'server_geocoding', 'submit', 'LOCATE', null,
415
+ '{"address_item": "P4_ADDRESS", "coordinates_item": "P4_LOCATION", "country": "nl"}', null, null, null),
416
+ (4, 20, 'Store the location', 'sql', 'submit', 'LOCATE',
417
+ 'update sample_processes.orders set location = :P4_LOCATION where id = :P4_ORDER::int', '{}', 'Address located: &P4_LOCATION..', 'item_not_null', 'P4_LOCATION'),
418
+ (4, 30, 'Ship the order', 'sql', 'submit', 'SHIP',
419
+ E'update sample_processes.orders set status = ''SHIPPED'', shipped_at = now(), shipped_by = :P4_SHIPPED_BY\n where id = :P4_ORDER::int and status = ''NEW''',
420
+ '{}', 'The order is shipped.', null, null),
421
+ (4, 40, 'Mail the customer', 'send_email', 'submit', 'SHIP', null,
422
+ '{"to": "&P4_EMAIL.", "subject": "Your order &P4_ORDER. is on its way", "body": "&P4_NOTE.\n\nYour order &P4_ORDER. left our workshop today.\n\n&P4_SHIPPED_BY., Kiln & Clay Supplies", "body_html": "<p>&P4_NOTE.</p><p>Your order <b>&P4_ORDER.</b> left our workshop today.</p><p>&P4_SHIPPED_BY., Kiln &amp; Clay Supplies</p>"}',
423
+ null, 'sql', 'meta.mail_available()'),
424
+ (4, 50, 'Note the mail', 'sql', 'submit', 'SHIP',
425
+ E'insert into sample_processes.outbox (order_id, channel, recipient, outcome)\nvalues (:P4_ORDER::int, ''e-mail'', :P4_EMAIL, case when meta.mail_available() then ''queued'' else ''not sent: the application has no mail service'' end)',
426
+ '{}', null, null, null),
427
+ (4, 60, 'Tell my devices', 'send_push', 'submit', 'SHIP', null,
428
+ '{"to": "&APP_USER.", "title": "Order &P4_ORDER. shipped", "body": "&P4_ADDRESS.", "page": 8, "items": {"P8_ID": "&P4_ORDER."}, "tag": "order-&P4_ORDER.", "urgency": "low"}',
429
+ null, null, null),
430
+ (4, 70, 'Note the push message', 'sql', 'submit', 'SHIP',
431
+ E'insert into sample_processes.outbox (order_id, channel, recipient, outcome)\nvalues (:P4_ORDER::int, ''push'', :APP_USER, case when meta.has_push_subscription() then ''queued for your devices'' else ''queued: you have no device with notifications on'' end)',
432
+ '{}', null, null, null),
433
+ (4, 80, 'Start afresh', 'clear_session_state', 'submit', 'SHIP', null, '{"pages": [4]}', null, null, null),
434
+ (4, 90, 'Print the packing slip', 'print_report', 'submit', 'PACKING_SLIP', null, '{"document": "PACKING_SLIP"}', null, null, null),
435
+ -- 7 Ajax callbacks: an Ajax callback of this page
436
+ (7, 10, 'CHECK_STOCK', 'sql', 'ajax', null,
437
+ E'select stock as p7_stock, format(''%s: %s in stock'', name, stock) as message\n from sample_processes.product where id = nullif(:P7_PRODUCT, '''')::int',
438
+ '{}', null, null, null)
439
+ ) as p (page, seq, name, type, point, button, code, config, message, ctype, cexpr);
440
+
441
+ -- 9 Stock: Reset Pagination on the grid's Save (its request is GRID_SAVE_<region id>)
442
+ insert into meta.process (page_id, seq, name, type, when_button, success_message)
443
+ select r.page_id, 20, 'Back to the first page', 'reset_pagination', 'GRID_SAVE_' || r.id, 'Saved: the grid starts on its first page again.'
444
+ from meta.region r where r.page_id = pg_temp.page_id('sample-processes', 9) and r.type = 'grid';
445
+ update meta.region set config = config || '{"page_size": 3}'
446
+ where page_id = pg_temp.page_id('sample-processes', 9) and type = 'grid';
447
+
448
+ -- branches
449
+ insert into meta.branch (page_id, seq, name, point, when_button, target_type, target_url, target_app, target_page, condition_type, condition_expr)
450
+ select pg_temp.page_id('sample-processes', b.page), b.seq, b.name, b.point, b.button, b.ttype, b.url, b.app, b.target, b.ctype, b.cexpr
451
+ from (values
452
+ -- nothing left to ship: the order list instead of this page (a URL inside the application)
453
+ (4, 10, 'Nothing to ship', 'before_header', null, 'url', '2', null, null,
454
+ 'not_exists', 'select 1 from sample_processes.orders where status = ''NEW'''),
455
+ -- after shipping: the outbox, as a page of an application (here this one, by its alias)
456
+ (4, 20, 'To the outbox', 'after_processing', 'SHIP', 'app', null, 'sample-processes', 5, null, null)
457
+ ) as b (page, seq, name, point, button, ttype, url, app, target, ctype, cexpr);
458
+
459
+ -- page 2 runs these when the dialog closes: the report is refreshed (the dialog's P3_ID is set by then)
460
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id)
461
+ select pg_temp.page_id('sample-processes', 2), 10, 'Refresh after the dialog', 'dialog_closed', '3', 'refresh_region',
462
+ pg_temp.region_id('sample-processes', 2, 'orders');
463
+
464
+ insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, code)
465
+ select pg_temp.page_id('sample-processes', 7), d.seq, d.name, 'click', d.button, 'execute_javascript', d.fn
466
+ from (values (10, 'Check the stock', 'CHECK', 'sp.checkStock'), (20, 'Ask the server', 'CLOCK', 'sp.serverClock')) as d (seq, name, button, fn);