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.
- package/db/migrations/155_mail_available.sql +23 -0
- package/docs/apex-feature-parity.md +3 -3
- package/docs/guide/04-pages-and-regions.md +24 -5
- package/docs/guide/05-items.md +20 -1
- package/docs/guide/06-processing.md +7 -3
- package/docs/guide/07-dynamic-actions.md +3 -3
- package/docs/guide/08-security.md +6 -2
- package/docs/guide/09-reference.md +1 -0
- package/docs/guide/11-from-apex.md +3 -0
- package/docs/guide/12-development.md +4 -0
- package/examples/gallery/README.md +23 -6
- package/examples/gallery/gallery.json +4 -0
- package/examples/gallery/samples/sample-approvals.sql +102 -11
- package/examples/gallery/samples/sample-calendar.sql +40 -5
- package/examples/gallery/samples/sample-dynamic-actions.sql +240 -9
- package/examples/gallery/samples/sample-grids.sql +16 -4
- package/examples/gallery/samples/sample-maps.sql +65 -7
- package/examples/gallery/samples/sample-processes.sql +466 -0
- package/examples/gallery/samples/sample-validations.sql +407 -0
- package/examples/gallery/samples/ux-patterns.sql +154 -14
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_80_notifications.sql +125 -0
- package/examples/hr/hr_81_vector_search_type.sql +33 -0
- package/package.json +1 -1
- package/public/app.css +10 -1
- package/public/app.js +30 -9
- package/src/builder/components.ts +1 -1
- package/src/builder/region-help.ts +2 -2
- package/src/runtime/authz.ts +7 -0
- package/src/runtime/engine.ts +6 -3
- package/src/runtime/grid.ts +69 -9
- package/src/runtime/items.ts +9 -1
- package/src/runtime/lists.ts +2 -2
- package/src/runtime/push.ts +7 -1
- package/src/runtime/regions.ts +25 -8
- package/src/runtime/signin.ts +5 -5
|
@@ -11,8 +11,16 @@
|
|
|
11
11
|
-- page 6 content patterns: alerts, badges and tags, colour classes,
|
|
12
12
|
-- text styles, lists, media lists, timelines, comments, avatars,
|
|
13
13
|
-- metric cards and empty states;
|
|
14
|
-
-- page 7 page patterns: a modal dialog (page 8), a drawer (page 9)
|
|
15
|
-
-- page with a side column (page 10)
|
|
14
|
+
-- page 7 page patterns: a modal dialog (page 8), a drawer (page 9), a
|
|
15
|
+
-- page with a side column (page 10) and an embedded page (a URL
|
|
16
|
+
-- region);
|
|
17
|
+
-- page 11 more item types: autocomplete, multi-select, URL and percent
|
|
18
|
+
-- graph, and people (pages 12 and 13) with a picture uploaded by
|
|
19
|
+
-- an Image Upload item into the table;
|
|
20
|
+
-- page 14 display positions: banner, body, right side column and footer
|
|
21
|
+
-- (page template Right side column);
|
|
22
|
+
-- page 15 button positions: every slot of a region's header and bottom bar;
|
|
23
|
+
-- page 16 a sign-in region on a page with the template Minimal.
|
|
16
24
|
-- Every page says which template, option or class it uses.
|
|
17
25
|
--
|
|
18
26
|
-- Installed by an administrator from the App Gallery, or with
|
|
@@ -34,15 +42,19 @@ create table ux_patterns.person (
|
|
|
34
42
|
role text not null,
|
|
35
43
|
status text not null check (status in ('Active', 'Away', 'Left')),
|
|
36
44
|
joined date not null,
|
|
37
|
-
note text
|
|
45
|
+
note text,
|
|
46
|
+
website text check (website ~ '^https?://'),
|
|
47
|
+
photo bytea check (octet_length(photo) <= 2 * 1024 * 1024),
|
|
48
|
+
photo_name text,
|
|
49
|
+
photo_mime text
|
|
38
50
|
);
|
|
39
51
|
|
|
40
|
-
insert into ux_patterns.person (name, role, status, joined, note) values
|
|
41
|
-
('Alex Morgan', 'Product owner', 'Active', current_date - 400, 'Sets the priorities of the team.'),
|
|
42
|
-
('Sam Lee', 'Developer', 'Active', current_date - 200, 'Builds the reports.'),
|
|
43
|
-
('Robin Diaz', 'Designer', 'Away', current_date - 90, 'Back next week.'),
|
|
44
|
-
('Kim Novak', 'Tester', 'Active', current_date - 30, 'Writes the browser tests.'),
|
|
45
|
-
('Jo Park', 'Analyst', 'Left', current_date - 700, 'Moved to another team.');
|
|
52
|
+
insert into ux_patterns.person (name, role, status, joined, note, website) values
|
|
53
|
+
('Alex Morgan', 'Product owner', 'Active', current_date - 400, 'Sets the priorities of the team.', 'https://example.com/alex'),
|
|
54
|
+
('Sam Lee', 'Developer', 'Active', current_date - 200, 'Builds the reports.', 'https://example.com/sam'),
|
|
55
|
+
('Robin Diaz', 'Designer', 'Away', current_date - 90, 'Back next week.', null),
|
|
56
|
+
('Kim Novak', 'Tester', 'Active', current_date - 30, 'Writes the browser tests.', null),
|
|
57
|
+
('Jo Park', 'Analyst', 'Left', current_date - 700, 'Moved to another team.', null);
|
|
46
58
|
|
|
47
59
|
-- 2. The application's database role ----------------------------------------
|
|
48
60
|
do $$ begin
|
|
@@ -51,6 +63,8 @@ end $$;
|
|
|
51
63
|
grant app_ux_patterns to pgkiln_runtime;
|
|
52
64
|
grant usage on schema ux_patterns to app_ux_patterns;
|
|
53
65
|
grant select on all tables in schema ux_patterns to app_ux_patterns;
|
|
66
|
+
-- the people form of page 13 edits a person and uploads a picture
|
|
67
|
+
grant insert, update, delete on ux_patterns.person to app_ux_patterns;
|
|
54
68
|
|
|
55
69
|
-- 3. The application ----------------------------------------------------------
|
|
56
70
|
insert into meta.app (alias, name, home_page, authentication, db_role)
|
|
@@ -71,7 +85,11 @@ select a.id, p.no, p.name, p.name, p.parent, p.mode, p.pos, p.size, p.tpl
|
|
|
71
85
|
(7, 'Page patterns', 1, 'normal', 'center', 'medium', 'standard'),
|
|
72
86
|
(8, 'A modal dialog', 7, 'modal', 'center', 'small', 'standard'),
|
|
73
87
|
(9, 'A drawer', 7, 'modal', 'right', 'medium', 'standard'),
|
|
74
|
-
(10, 'A side column', 7, 'normal', 'center', 'medium', 'left_side_column')
|
|
88
|
+
(10, 'A side column', 7, 'normal', 'center', 'medium', 'left_side_column'),
|
|
89
|
+
(11, 'More item types', 1, 'normal', 'center', 'medium', 'standard'),
|
|
90
|
+
(14, 'Display positions', 1, 'normal', 'center', 'medium', 'right_side_column'),
|
|
91
|
+
(15, 'Button positions', 1, 'normal', 'center', 'medium', 'standard'),
|
|
92
|
+
(16, 'Sign-in region', 1, 'normal', 'center', 'medium', 'minimal')
|
|
75
93
|
) as p (no, name, parent, mode, pos, size, tpl)
|
|
76
94
|
where a.alias = 'ux-patterns';
|
|
77
95
|
|
|
@@ -79,7 +97,9 @@ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
|
79
97
|
select a.id, n.seq, n.label, n.icon, n.page
|
|
80
98
|
from meta.app a, (values
|
|
81
99
|
(10, 'Home', 'home', 1), (20, 'Region templates', 'layers', 2), (30, 'Region options', 'sliders', 3), (40, 'Buttons', 'target', 4),
|
|
82
|
-
(50, 'Items and forms', 'edit', 5), (60, 'Content patterns', 'grid', 6), (70, 'Page patterns', 'file', 7)
|
|
100
|
+
(50, 'Items and forms', 'edit', 5), (60, 'Content patterns', 'grid', 6), (70, 'Page patterns', 'file', 7),
|
|
101
|
+
(80, 'More item types', 'list-plus', 11), (90, 'Display positions', 'layout-panel-left', 14),
|
|
102
|
+
(100, 'Button positions', 'mouse-pointer-click', 15), (110, 'Sign-in region', 'log-in', 16)
|
|
83
103
|
) as n (seq, label, icon, page)
|
|
84
104
|
where a.alias = 'ux-patterns';
|
|
85
105
|
|
|
@@ -100,7 +120,11 @@ select a.id, e.list, e.seq, e.label, e.icon, e.page, e.badge, e.description
|
|
|
100
120
|
('UX_TOPICS', 30, 'Buttons', 'target', 4, null, 'Hot and normal, small and large, pill, outline, link, full width, success and danger, a menu and a badge.'),
|
|
101
121
|
('UX_TOPICS', 40, 'Items and forms', 'edit', 5, null, 'Every common item type in a form, and the item template options.'),
|
|
102
122
|
('UX_TOPICS', 50, 'Content patterns', 'grid', 6, null, 'Alerts, badges, colours, text, lists, media lists, timelines, comments, avatars, metrics and empty states.'),
|
|
103
|
-
('UX_TOPICS', 60, 'Page patterns', 'file', 7, null, 'A modal dialog, a drawer
|
|
123
|
+
('UX_TOPICS', 60, 'Page patterns', 'file', 7, null, 'A modal dialog, a drawer, a page with a side column and an embedded page.'),
|
|
124
|
+
('UX_TOPICS', 70, 'More item types', 'list-plus', 11, null, 'Autocomplete, multi-select, URL, percent graph and a picture uploaded into a table.'),
|
|
125
|
+
('UX_TOPICS', 80, 'Display positions', 'layout-panel-left', 14, null, 'Banner, body, right side column and footer.'),
|
|
126
|
+
('UX_TOPICS', 90, 'Button positions', 'mouse-pointer-click', 15, null, 'Create, edit, help and up in the header; previous, close, delete and next in the bottom bar.'),
|
|
127
|
+
('UX_TOPICS', 100, 'Sign-in region', 'log-in', 16, null, 'The Login region on a page without navigation, as on an own login page.'),
|
|
104
128
|
('UX_STEPS', 10, 'Choose', null, 2, null, null),
|
|
105
129
|
('UX_STEPS', 20, 'Details', null, 5, null, null),
|
|
106
130
|
('UX_STEPS', 30, 'Confirm', null, 7, null, null),
|
|
@@ -216,7 +240,7 @@ select pg_temp.page_id('ux-patterns', r.page), r.seq, r.title, r.type, r.cols, r
|
|
|
216
240
|
|
|
217
241
|
-- 7 Page patterns
|
|
218
242
|
(7, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
219
|
-
'<p class="lead">A page''s <strong>mode</strong> and <strong>dialog position</strong> make it a modal dialog or a drawer; its <strong>page template</strong> can add a side column. The buttons open examples of each.</p>',
|
|
243
|
+
'<p class="lead">A page''s <strong>mode</strong> and <strong>dialog position</strong> make it a modal dialog or a drawer; its <strong>page template</strong> can add a side column. The buttons open examples of each. Under them a <strong>URL region</strong> (type <code>url</code>) shows the modal dialog page in a sandboxed frame: its source is a URL, here a page of this server (<code>/a/ux-patterns/8</code>).</p>',
|
|
220
244
|
'{}', null, null),
|
|
221
245
|
(7, 10, 'Dialogs, drawers and columns', 'static', 12, 'buttons_container', '{}', null, '{}', 'p-buttons', null),
|
|
222
246
|
(8, 10, 'A modal dialog', 'static', 12, 'plain', '{}',
|
|
@@ -230,7 +254,56 @@ select pg_temp.page_id('ux-patterns', r.page), r.seq, r.title, r.type, r.cols, r
|
|
|
230
254
|
'{}', null, 'left'),
|
|
231
255
|
(10, 20, 'The body', 'static', 12, 'standard', '{}',
|
|
232
256
|
'<p>The regions of the body use the 12-column grid as usual. A faceted search beside its report is the typical use of a side column.</p>',
|
|
233
|
-
'{}', null, null)
|
|
257
|
+
'{}', null, null),
|
|
258
|
+
(7, 20, 'An embedded page (URL region)', 'url', 12, 'standard', '{}', '/a/ux-patterns/8',
|
|
259
|
+
'{"height": 240}', null, null),
|
|
260
|
+
|
|
261
|
+
-- 11 More item types
|
|
262
|
+
(11, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
263
|
+
'<p class="lead">More item types, each named in its label. <strong>Autocomplete</strong> is a text field with suggestions from its list of values (any text may be typed), a <strong>multi-select</strong> returns its values colon separated, a <strong>URL</strong> field opens the phone''s URL keyboard and a <strong>percent graph</strong> shows a number from 0 to 100 as a bar. The people of this catalog (page 12) have a picture: their form (page 13) uploads it with an <strong>Image Upload</strong> item into the table <code>ux_patterns.person</code>.</p>',
|
|
264
|
+
'{}', null, null),
|
|
265
|
+
(11, 10, 'Text with suggestions and choices', 'static', 12, 'standard', '{}', null, '{}', 'm-input', null),
|
|
266
|
+
(11, 20, 'Display only', 'static', 12, 'standard', '{}', null, '{}', 'm-display', null),
|
|
267
|
+
|
|
268
|
+
-- 14 Display positions (page template right_side_column)
|
|
269
|
+
(14, 1, 'Banner (position banner)', 'static', 12, 'standard', '{to-accent,to-compact}',
|
|
270
|
+
'<p>A region with the position <code>banner</code> shows at the top of the page content, above the body: a notice for everyone who opens the page.</p>',
|
|
271
|
+
'{}', null, 'banner'),
|
|
272
|
+
(14, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
273
|
+
'<p class="lead">A page has <strong>display positions</strong> and each region chooses one with its <strong>Position</strong> (Page Designer → region → Layout). This page has the page template <em>Right side column</em> (<code>right_side_column</code>), so the regions with the position <code>right</code> show in a column beside the body; on a narrow screen under it.</p>',
|
|
274
|
+
'{}', null, null),
|
|
275
|
+
(14, 10, 'The body (position body)', 'static', 12, 'standard', '{}',
|
|
276
|
+
'<p>Regions without a position, or with <code>body</code>, are the body: the 12-column grid of every page.</p>',
|
|
277
|
+
'{}', null, 'body'),
|
|
278
|
+
(14, 20, 'People (body)', 'report', 12, 'standard', '{}',
|
|
279
|
+
E'select name, role, status from ux_patterns.person order by name', '{"searchable": false}', null, null),
|
|
280
|
+
(14, 30, 'Right column (position right)', 'static', 12, 'standard', '{}',
|
|
281
|
+
'<p>This region has the position <code>right</code>: on this page it is the side column. On a page with another template it would show in the body, in sequence.</p>',
|
|
282
|
+
'{}', null, 'right'),
|
|
283
|
+
(14, 40, 'Topics (right)', 'list', 12, 'standard', '{}', null, '{"list": "UX_TOPICS", "template": "links"}', null, 'right'),
|
|
284
|
+
(14, 50, 'Footer (position footer)', 'static', 12, 'plain', '{}',
|
|
285
|
+
'<p class="muted">A region with the position <code>footer</code> comes after the body and the side column: small print, links, a last update.</p>',
|
|
286
|
+
'{}', null, 'footer'),
|
|
287
|
+
|
|
288
|
+
-- 15 Button positions
|
|
289
|
+
(15, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
290
|
+
'<p class="lead">A region''s button has a <strong>Button position</strong> (Page Designer → button → Layout; <code>slot</code> in the files), as APEX''s button positions. <em>Create, Edit, Change, Copy, Help</em> and <em>Up</em> go into the region''s header after the title; <em>Previous, Close</em> and <em>Delete</em> to the left end of the bottom bar and <em>Next</em> to the right end; <em>Region</em> (or none) with the region''s other buttons (in the bottom bar of a static or form region, before Next). Each button is labelled with its position. Previous and Next go to the neighbouring pages, Up and Close to the home page, Help opens an inline dialog; the others do nothing.</p>',
|
|
291
|
+
'{}', null, null),
|
|
292
|
+
(15, 10, 'Every button position', 'static', 12, 'standard', '{}',
|
|
293
|
+
'<p>The buttons of this region use every position. They apply to the <code>standard</code>, <code>collapsible</code> and <code>plain</code> templates; the other templates place every button where they always do.</p>',
|
|
294
|
+
'{}', 'slots', null),
|
|
295
|
+
(15, 20, 'In a collapsible region', 'static', 12, 'collapsible', '{}',
|
|
296
|
+
'<p>The same positions in a collapsible region: <em>Edit</em> in the header, <em>Previous</em> and <em>Next</em> in the bottom bar.</p>',
|
|
297
|
+
'{}', 'slots-collapsible', null),
|
|
298
|
+
(15, 30, 'Help', 'static', 12, 'inline_dialog', '{}',
|
|
299
|
+
'<p>A button with the position <code>help</code> sits in the header, like APEX''s Help button. This one opens an inline dialog with a dynamic action <em>Open region</em>.</p>',
|
|
300
|
+
'{}', 'slot-help', null),
|
|
301
|
+
|
|
302
|
+
-- 16 Sign-in region (page template minimal)
|
|
303
|
+
(16, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
304
|
+
'<p class="lead">This page has the page template <em>Minimal (no navigation)</em> (<code>minimal</code>): no header, navigation menu or navigation bar, the title and the regions only, centred. The region below is a <strong>Login</strong> region: the application''s sign-in form, as on an own login page (Application settings → Sign-in methods → Create a login page). It posts to the application''s sign-in, so throttling and every sign-in method work as on the built-in page. You are signed in already: signing in again as demo / demo starts a new session and opens the home page. A wrong password shows the built-in sign-in page with its message, because this page is not the application''s login page. The region has <code>{"autofocus": false}</code>: you are signed in already, so the cursor isn''t put in the user name field (on a real login page leave it out).</p>',
|
|
305
|
+
'{}', 'about-login', null),
|
|
306
|
+
(16, 10, 'Sign in', 'login', 12, 'standard', '{}', null, '{"autofocus": false}', null, null)
|
|
234
307
|
) as r (page, seq, title, type, cols, template, topts, source, config, sid, pos);
|
|
235
308
|
|
|
236
309
|
-- buttons
|
|
@@ -262,10 +335,36 @@ select pg_temp.page_id('ux-patterns', b.page), pg_temp.region_id('ux-patterns',
|
|
|
262
335
|
(7, 'p-buttons', 30, 'SIDE', 'A page with a side column', 'redirect', false, 10, '{}', null, null)
|
|
263
336
|
) as b (page, region, seq, name, label, action, hot, target, topts, menu, badge);
|
|
264
337
|
|
|
338
|
+
-- button positions (page 15) and the other new pages' buttons: harmless (navigate, open the help, or nothing)
|
|
339
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, hot, target_page, template_options, slot)
|
|
340
|
+
select pg_temp.page_id('ux-patterns', b.page), pg_temp.region_id('ux-patterns', b.page, b.region), b.seq, b.name, b.label, b.action, b.hot, b.target,
|
|
341
|
+
b.topts::text[], b.slot
|
|
342
|
+
from (values
|
|
343
|
+
(15, 'slots', 10, 'CREATE', 'Create (create)', 'da', true, null, '{}', 'create'),
|
|
344
|
+
(15, 'slots', 20, 'EDIT', 'Edit (edit)', 'da', false, null, '{}', 'edit'),
|
|
345
|
+
(15, 'slots', 30, 'CHANGE', 'Change (change)', 'da', false, null, '{}', 'change'),
|
|
346
|
+
(15, 'slots', 40, 'COPY', 'Copy (copy)', 'da', false, null, '{}', 'copy'),
|
|
347
|
+
(15, 'slots', 50, 'HELP', 'Help (help)', 'da', false, null, '{}', 'help'),
|
|
348
|
+
(15, 'slots', 60, 'UP', 'Up (up)', 'redirect', false, 1, '{}', 'up'),
|
|
349
|
+
(15, 'slots', 70, 'PREVIOUS', 'Previous (previous)', 'redirect', false, 14, '{}', 'previous'),
|
|
350
|
+
(15, 'slots', 80, 'CLOSE', 'Close (close)', 'redirect', false, 1, '{}', 'close'),
|
|
351
|
+
(15, 'slots', 90, 'DELETE', 'Delete (delete)', 'da', false, null, '{to-danger}', 'delete'),
|
|
352
|
+
(15, 'slots', 100, 'NEXT', 'Next (next)', 'redirect', true, 16, '{}', 'next'),
|
|
353
|
+
(15, 'slots', 110, 'IN_REGION', 'With the others (region)', 'da', false, null, '{}', 'region'),
|
|
354
|
+
(15, 'slots-collapsible', 10, 'C_EDIT', 'Edit (edit)', 'da', false, null, '{}', 'edit'),
|
|
355
|
+
(15, 'slots-collapsible', 20, 'C_PREVIOUS', 'Previous (previous)', 'redirect', false, 14, '{}', 'previous'),
|
|
356
|
+
(15, 'slots-collapsible', 30, 'C_NEXT', 'Next (next)', 'redirect', true, 16, '{}', 'next'),
|
|
357
|
+
(11, 'm-display', 10, 'PEOPLE', 'People and their pictures', 'redirect', true, 12, '{}', null),
|
|
358
|
+
(16, 'about-login', 10, 'HOME', 'Back to the catalog', 'redirect', false, 1, '{}', null)
|
|
359
|
+
) as b (page, region, seq, name, label, action, hot, target, topts, slot);
|
|
360
|
+
|
|
265
361
|
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id)
|
|
266
362
|
select pg_temp.page_id('ux-patterns', 2), d.seq, d.name, 'click', d.button, 'open_region', pg_temp.region_id('ux-patterns', 2, d.region)
|
|
267
363
|
from (values (10, 'Open the inline dialog', 'OPEN_DIALOG', 'dialog'), (20, 'Open the inline drawer', 'OPEN_DRAWER', 'drawer')) as d (seq, name, button, region);
|
|
268
364
|
|
|
365
|
+
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id)
|
|
366
|
+
select pg_temp.page_id('ux-patterns', 15), 10, 'Open the help', 'click', 'HELP', 'open_region', pg_temp.region_id('ux-patterns', 15, 'slot-help');
|
|
367
|
+
|
|
269
368
|
-- items
|
|
270
369
|
insert into meta.item (page_id, region_id, seq, name, label, type, lov, default_value, required, help, config, template_options)
|
|
271
370
|
select pg_temp.page_id('ux-patterns', 5), pg_temp.region_id('ux-patterns', 5, i.region), i.seq, i.name, i.label, i.type, i.lov, i.def, i.req, i.help,
|
|
@@ -293,3 +392,44 @@ select pg_temp.page_id('ux-patterns', 5), pg_temp.region_id('ux-patterns', 5, i.
|
|
|
293
392
|
('f-options', 40, 'P5_BOLD', 'Bold value (to-bold)', 'display', null, 'A bold value', false, null, '{}', '{to-bold}'),
|
|
294
393
|
('f-options', 50, 'P5_HIDDEN_LABEL', 'Hidden label (to-hide-label)', 'text', null, 'The label is only for screen readers', false, null, '{}', '{to-hide-label}')
|
|
295
394
|
) as i (region, seq, name, label, type, lov, def, req, help, config, topts);
|
|
395
|
+
|
|
396
|
+
-- page 11: more item types
|
|
397
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, lov, default_value, help, config)
|
|
398
|
+
select pg_temp.page_id('ux-patterns', 11), pg_temp.region_id('ux-patterns', 11, i.region), i.seq, i.name, i.label, i.type, i.lov, i.def, i.help, i.config::jsonb
|
|
399
|
+
from (values
|
|
400
|
+
('m-input', 10, 'P11_ROLE', 'Role (autocomplete)', 'autocomplete', 'select distinct role from ux_patterns.person order by 1', null,
|
|
401
|
+
'Suggestions from the roles of the people; type anything else as well.', '{}'),
|
|
402
|
+
('m-input', 20, 'P11_DAYS', 'Days (multiselect)', 'multiselect', 'STATIC:Monday;MON,Tuesday;TUE,Wednesday;WED,Thursday;THU,Friday;FRI', 'MON:WED',
|
|
403
|
+
'Several values: the item''s value is their return values, colon separated (MON:WED).', '{}'),
|
|
404
|
+
('m-input', 30, 'P11_WEBSITE', 'Website (url)', 'url', null, 'https://example.com', 'A URL field: the phone shows its URL keyboard.', '{}'),
|
|
405
|
+
('m-display', 10, 'P11_ACTIVE_PCT', 'People active (percent_graph)', 'percent_graph', null, null,
|
|
406
|
+
'A computation counts the active people; the bar reads out as the number.', '{}'),
|
|
407
|
+
('m-display', 20, 'P11_DONE_PCT', 'A full bar (percent_graph, 100)', 'percent_graph', null, null, null, '{}')
|
|
408
|
+
) as i (region, seq, name, label, type, lov, def, help, config);
|
|
409
|
+
|
|
410
|
+
insert into meta.computation (page_id, seq, item_name, point, type, expression)
|
|
411
|
+
values (pg_temp.page_id('ux-patterns', 11), 10, 'P11_ACTIVE_PCT', 'before_header', 'sql_query',
|
|
412
|
+
E'select round(100.0 * count(*) filter (where status = ''Active'') / greatest(count(*), 1)) from ux_patterns.person'),
|
|
413
|
+
(pg_temp.page_id('ux-patterns', 11), 20, 'P11_DONE_PCT', 'before_header', 'static', '100');
|
|
414
|
+
|
|
415
|
+
-- pages 12 and 13: the people, with a picture (Report and form wizard)
|
|
416
|
+
select meta.generate_page('ux-patterns', 'report_form', 'ux_patterns.person', 12, '{"label": "People", "form_page": 13, "nav": false}');
|
|
417
|
+
update meta.page set parent_page = 11 where id = pg_temp.page_id('ux-patterns', 12);
|
|
418
|
+
insert into meta.region (page_id, seq, title, type, columns, template, source)
|
|
419
|
+
values (pg_temp.page_id('ux-patterns', 12), 1, 'About this page', 'static', 12, 'plain',
|
|
420
|
+
'<p class="lead">Made with the <em>Report and form</em> wizard (<code>meta.generate_page</code>) on <code>ux_patterns.person</code>. Open a person: the form has a <strong>Picture</strong> item of type <code>image_upload</code> that crops the picture to a square in the browser and saves it into the column <code>photo</code> (with its name and type in <code>photo_name</code> and <code>photo_mime</code>), a <strong>Role</strong> with <code>autocomplete</code> and a <strong>Website</strong> of type <code>url</code>.</p>');
|
|
421
|
+
update meta.region set source = 'select id, name, role, status, website, photo_name from ux_patterns.person order by name',
|
|
422
|
+
config = config || '{"headings": {"photo_name": "Picture"}}'
|
|
423
|
+
where page_id = pg_temp.page_id('ux-patterns', 12) and type = 'report';
|
|
424
|
+
delete from meta.item where page_id = pg_temp.page_id('ux-patterns', 13) and name in ('P13_PHOTO_NAME', 'P13_PHOTO_MIME');
|
|
425
|
+
update meta.item set type = 'image_upload', label = 'Picture (image_upload)',
|
|
426
|
+
help = 'Image Upload: a picture only, cropped to a square and made smaller in the browser before it is saved into the column photo.',
|
|
427
|
+
config = '{"filename_column": "photo_name", "mime_column": "photo_mime", "accept": "image/png,image/jpeg,image/webp", "max_mb": 2,
|
|
428
|
+
"crop": "1:1", "max_px": 600}'
|
|
429
|
+
where page_id = pg_temp.page_id('ux-patterns', 13) and name = 'P13_PHOTO';
|
|
430
|
+
update meta.item set type = 'autocomplete', label = 'Role (autocomplete)', lov = 'select distinct role from ux_patterns.person order by 1'
|
|
431
|
+
where page_id = pg_temp.page_id('ux-patterns', 13) and name = 'P13_ROLE';
|
|
432
|
+
update meta.item set type = 'select', lov = 'STATIC:Active,Away,Left'
|
|
433
|
+
where page_id = pg_temp.page_id('ux-patterns', 13) and name = 'P13_STATUS';
|
|
434
|
+
update meta.item set type = 'url', label = 'Website (url)', help = 'Starts with https://.'
|
|
435
|
+
where page_id = pg_temp.page_id('ux-patterns', 13) and name = 'P13_WEBSITE';
|
package/examples/hr/README.md
CHANGED
|
@@ -44,6 +44,8 @@ Then open http://127.0.0.1:3100/a/hr and sign in as `king`, `blake`, `jones`, `a
|
|
|
44
44
|
| `hr_68_component_details.sql` | *Component details* (page 47): a popup LOV over 6,000 rows that loads more as the dialog scrolls, a wait popup around a two-second process (*Recalculate*) and around an Ajax call (*Check in the background*), and an *Actions* menu whose entries run dynamic actions (event `menu_entry`) |
|
|
45
45
|
| `hr_71_fulltext_search.sql` | Full-text search: `hr.policy` gets a generated `search_vector` column (title weight A, body B, english) with a GIN index, and the search configuration `POLICIES` (search type *fulltext*, *Did you mean* when `pg_trgm` is installed) joins page 42 *Search*: "works home" finds *Working from home*, the matched words highlighted |
|
|
46
46
|
| `hr_72_vector_search.sql` | Search by meaning: `hr.policy` gets *Relocation leave* and a managers-only *Salary bands* with row level security (`audience`); with pgvector, the vector provider `POLICY_EMBEDDINGS` (a stand-in function, `hr.policy_embedding`, eight topics), `hr.policy.embedding vector(8)` with an HNSW index and a trigger, the hybrid search configuration `POLICIES_MEANING` on page 42 (*time off for a family move* finds *Relocation leave*), and *Policy questions* (page 49): the agent `POLICY_AGENT` with the Retrieve tool `policy_search`. Needs an AI service named `HR_ASSISTANT` for page 49 |
|
|
47
|
+
| `hr_80_notifications.sql` | The notification bell and live badges: HR's own notifications (`hr.notification`: leave requested and decided, workflow, automation and task messages) also go on the bell through `meta.notify`, a leave notification opening the request (page 7); *Mark all read* clears the bell too; badges from SQL (`badge_query`) on the navigation bar (*Leave requests* pending, *My tasks* you can act on), the *Pending requests* shortcut and the *Mark all read* button, refreshed every 30 seconds without a reload; a `timer` dynamic action refreshes *My notifications*. Sign in as jones, and as scott in another browser: ask for leave and watch jones's bell |
|
|
48
|
+
| `hr_81_vector_search_type.sql` | *Policies by meaning only* on page 42: the search type `vector` next to part 72's hybrid one (only with pgvector) |
|
|
47
49
|
|
|
48
50
|
The files run in order, each once (recorded in `public.pgkiln_seed`), so new ones can be added
|
|
49
51
|
later. pgkiln's own tests use this application as their fixture (`npm test` installs it first).
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 80: the notification bell and live badges (migration 154)
|
|
3
|
+
--
|
|
4
|
+
-- HR has had its own notifications since hr.sql: rows in hr.notification
|
|
5
|
+
-- (leave requested and decided, the workflows' and automations' messages),
|
|
6
|
+
-- listed under "My notifications" on the home page. This part puts them on
|
|
7
|
+
-- pgkiln's notification bell as well:
|
|
8
|
+
--
|
|
9
|
+
-- * Settings → User Interface: Notifications on, Live badges every 30 s
|
|
10
|
+
-- * hr.notification gets the page a notification opens (page, items) and
|
|
11
|
+
-- an icon; a trigger hands every new row to meta.notify(), so the bell
|
|
12
|
+
-- shows it and opening it goes to that page (a leave request opens the
|
|
13
|
+
-- request on page 7, the link signed for the recipient)
|
|
14
|
+
-- * "Mark all read" on the home page also marks the bell's notifications read
|
|
15
|
+
-- * badges from SQL (badge_query), refreshed in place by the live badges:
|
|
16
|
+
-- the navigation bar's "Leave requests" (pending requests you can see),
|
|
17
|
+
-- "My tasks" (tasks you can act on), the Shortcuts list's "Pending
|
|
18
|
+
-- requests" and the "Mark all read" button (your unread notifications)
|
|
19
|
+
-- * a timer dynamic action refreshes "My notifications" every 30 seconds
|
|
20
|
+
--
|
|
21
|
+
-- Sign in as jones, open the home page, and in another browser sign in as
|
|
22
|
+
-- scott (a clerk of jones's department) and ask for leave: jones's bell and
|
|
23
|
+
-- badges change without a reload.
|
|
24
|
+
-- =====================================================================
|
|
25
|
+
|
|
26
|
+
update meta.app set notifications = true, live_badges_s = 30 where alias = 'hr';
|
|
27
|
+
|
|
28
|
+
-- the page a notification opens, and its icon
|
|
29
|
+
alter table hr.notification add column page int, add column items jsonb, add column icon text;
|
|
30
|
+
|
|
31
|
+
-- the leave notifications open the request (page 7)
|
|
32
|
+
create or replace function hr.notify_leave_requested() returns trigger
|
|
33
|
+
language plpgsql security definer set search_path = hr, pg_catalog as $$
|
|
34
|
+
begin
|
|
35
|
+
insert into hr.notification (username, message, page, items, icon)
|
|
36
|
+
select m.username, format('%s requested %s day(s) of leave from %s.',
|
|
37
|
+
e.ename, new.days, to_char(new.start_date, 'DD Mon YYYY')),
|
|
38
|
+
7, jsonb_build_object('P7_ID', new.id), 'calendar'
|
|
39
|
+
from hr.emp e join hr.emp m on m.empno = e.mgr
|
|
40
|
+
where e.empno = new.empno and m.username is not null;
|
|
41
|
+
return new;
|
|
42
|
+
end
|
|
43
|
+
$$;
|
|
44
|
+
|
|
45
|
+
create or replace function hr.notify_leave_decided() returns trigger
|
|
46
|
+
language plpgsql security definer set search_path = hr, pg_catalog as $$
|
|
47
|
+
begin
|
|
48
|
+
if new.status in ('APPROVED', 'REJECTED') then
|
|
49
|
+
insert into hr.notification (username, message, page, items, icon)
|
|
50
|
+
select e.username, format('Your leave from %s to %s was %s by %s.',
|
|
51
|
+
to_char(new.start_date, 'DD Mon'), to_char(new.end_date, 'DD Mon'),
|
|
52
|
+
lower(new.status), new.decided_by),
|
|
53
|
+
7, jsonb_build_object('P7_ID', new.id), case new.status when 'APPROVED' then 'check' else 'x' end
|
|
54
|
+
from hr.emp e
|
|
55
|
+
where e.empno = new.empno and e.username is not null;
|
|
56
|
+
end if;
|
|
57
|
+
return new;
|
|
58
|
+
end
|
|
59
|
+
$$;
|
|
60
|
+
|
|
61
|
+
-- Every new hr.notification also goes on the bell. Only from the running
|
|
62
|
+
-- application (meta.app_id() is HR, notifications on); a row added through
|
|
63
|
+
-- the API or SQL Workshop stays in hr.notification only. The bell never
|
|
64
|
+
-- stops the change that wrote the notification.
|
|
65
|
+
create function hr.notification_to_bell() returns trigger
|
|
66
|
+
language plpgsql security definer set search_path = hr, pg_catalog as $$
|
|
67
|
+
begin
|
|
68
|
+
if exists (select 1 from meta.app a where a.id = meta.app_id() and a.alias = 'hr' and a.notifications) then
|
|
69
|
+
perform meta.notify(
|
|
70
|
+
p_user => new.username,
|
|
71
|
+
p_title => left(new.message, 200),
|
|
72
|
+
p_page => new.page,
|
|
73
|
+
p_items => coalesce(new.items, '{}'::jsonb),
|
|
74
|
+
p_icon => new.icon);
|
|
75
|
+
end if;
|
|
76
|
+
return new;
|
|
77
|
+
exception when others then
|
|
78
|
+
raise warning 'hr.notification_to_bell: %', sqlerrm;
|
|
79
|
+
return new;
|
|
80
|
+
end
|
|
81
|
+
$$;
|
|
82
|
+
revoke execute on function hr.notification_to_bell() from public;
|
|
83
|
+
|
|
84
|
+
create trigger notification_to_bell after insert on hr.notification
|
|
85
|
+
for each row execute function hr.notification_to_bell();
|
|
86
|
+
|
|
87
|
+
-- "Mark all read" on the home page: the bell's notifications too
|
|
88
|
+
insert into meta.process (page_id, seq, name, type, code, when_button)
|
|
89
|
+
select p.id, 20, 'Mark the bell read', 'sql', 'select meta.mark_notifications_read()', 'MARK_READ'
|
|
90
|
+
from meta.page p join meta.app a on a.id = p.app_id where a.alias = 'hr' and p.page_no = 1;
|
|
91
|
+
|
|
92
|
+
-- the button shows how many of your notifications are unread
|
|
93
|
+
update meta.button b set badge_query = 'select nullif(count(*), 0) from hr.notification where read_at is null'
|
|
94
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
95
|
+
where b.page_id = p.id and a.alias = 'hr' and p.page_no = 1 and b.name = 'MARK_READ';
|
|
96
|
+
|
|
97
|
+
-- badges from SQL in the navigation bar and the Shortcuts list (row level
|
|
98
|
+
-- security applies: a manager counts their department's requests, an
|
|
99
|
+
-- employee their own)
|
|
100
|
+
insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description, badge_query)
|
|
101
|
+
select a.id, 'HR_NAVBAR', e.seq, e.label, e.icon, e.page, e.description, e.badge_query
|
|
102
|
+
from meta.app a, (values
|
|
103
|
+
(20, 'Leave requests', 'calendar', 6, 'Pending leave requests you can see',
|
|
104
|
+
'select nullif(count(*), 0) from hr.leave_request where status = ''PENDING'''),
|
|
105
|
+
(30, 'My tasks', 'check', 14, 'Tasks you can act on',
|
|
106
|
+
'select nullif(count(*), 0) from meta.tasks where app_id = meta.app_id() and may_act and state in (''unassigned'', ''assigned'')')
|
|
107
|
+
) as e (seq, label, icon, page, description, badge_query)
|
|
108
|
+
where a.alias = 'hr';
|
|
109
|
+
|
|
110
|
+
update meta.list_entry l set badge_query = 'select count(*) from hr.leave_request where status = ''PENDING'''
|
|
111
|
+
from meta.app a
|
|
112
|
+
where l.app_id = a.id and a.alias = 'hr' and l.list_name = 'HR_SHORTCUTS' and l.label = 'Pending requests';
|
|
113
|
+
|
|
114
|
+
-- a timer dynamic action: "My notifications" refreshes every 30 seconds while the page is visible
|
|
115
|
+
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id)
|
|
116
|
+
select p.id, 50, 'Refresh my notifications', 'timer', '30', 'refresh_region', r.id
|
|
117
|
+
from meta.page p join meta.app a on a.id = p.app_id join meta.region r on r.page_id = p.id and r.title = 'My notifications'
|
|
118
|
+
where a.alias = 'hr' and p.page_no = 1;
|
|
119
|
+
|
|
120
|
+
-- Dutch labels for the new navigation bar entries (part 4's translations)
|
|
121
|
+
insert into meta.translation (app_id, language, source, target)
|
|
122
|
+
select a.id, 'nl', t.src, t.nl
|
|
123
|
+
from meta.app a, (values ('Leave requests', 'Verlofaanvragen'), ('My tasks', 'Mijn taken')) as t (src, nl)
|
|
124
|
+
where a.alias = 'hr'
|
|
125
|
+
on conflict do nothing;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 81: search by meaning alone (search type vector)
|
|
3
|
+
--
|
|
4
|
+
-- Part 72 added POLICIES_MEANING, a hybrid search (full-text and vector
|
|
5
|
+
-- similarity combined). This part adds POLICIES_VECTOR to the same search
|
|
6
|
+
-- page (42): the vector similarity alone, so the two can be compared ("time
|
|
7
|
+
-- off for a family move" finds Relocation leave in both; a word that only
|
|
8
|
+
-- full-text matches finds nothing here). Only with pgvector, like part 72.
|
|
9
|
+
-- =====================================================================
|
|
10
|
+
|
|
11
|
+
do $$
|
|
12
|
+
begin
|
|
13
|
+
if not exists (select from information_schema.columns where table_schema = 'hr' and table_name = 'policy' and column_name = 'embedding') then
|
|
14
|
+
raise notice 'HR part 81: hr.policy has no vector column (pgvector is not installed): no vector search.';
|
|
15
|
+
return;
|
|
16
|
+
end if;
|
|
17
|
+
insert into meta.search_config (app_id, name, label, seq, source_type, source, search_type,
|
|
18
|
+
vector_provider, vector_column, key_column, distance, min_similarity,
|
|
19
|
+
title_column, description_column, icon, max_results)
|
|
20
|
+
select id, 'POLICIES_VECTOR', 'Policies by meaning only', 27, 'table', 'hr.policy', 'vector',
|
|
21
|
+
'POLICY_EMBEDDINGS', 'embedding', 'id', 'cosine', 0.6,
|
|
22
|
+
'title', 'body', 'sparkles', 3
|
|
23
|
+
from meta.app where alias = 'hr';
|
|
24
|
+
update meta.region r
|
|
25
|
+
set config = jsonb_set(r.config, '{configs}', (r.config->'configs') || '["POLICIES_VECTOR"]'::jsonb)
|
|
26
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
27
|
+
where r.page_id = p.id and a.alias = 'hr' and p.page_no = 42 and r.type = 'search';
|
|
28
|
+
end $$;
|
|
29
|
+
|
|
30
|
+
insert into meta.translation (app_id, language, source, target)
|
|
31
|
+
select a.id, 'nl', 'Policies by meaning only', 'Regelingen alleen op betekenis'
|
|
32
|
+
from meta.app a where a.alias = 'hr'
|
|
33
|
+
on conflict do nothing;
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -426,6 +426,12 @@ dialog.region-inline_drawer:target { inset: 0 0 0 auto; }
|
|
|
426
426
|
.region-collapsible summary.region-header { cursor: pointer; list-style: none; }
|
|
427
427
|
.region-collapsible summary.region-header::-webkit-details-marker { display: none; }
|
|
428
428
|
.region-collapsible details:not([open]) summary.region-header { border-bottom: 0; }
|
|
429
|
+
/* (166) header buttons of a collapsible region: over the header row, outside the summary (the toggle) */
|
|
430
|
+
.region-collapsible.has-header-buttons { position: relative; }
|
|
431
|
+
.region-collapsible > .region-header-buttons { position: absolute; top: 0; right: 1rem; z-index: 1; max-width: calc(min(50%, 22rem) - 1rem); min-height: 50px; display: flex; align-items: center; justify-content: flex-end; }
|
|
432
|
+
.region-collapsible > .region-header-buttons .buttons { margin: 0; flex-wrap: wrap; justify-content: flex-end; }
|
|
433
|
+
.region-collapsible.has-header-buttons > details > summary.region-header { padding-right: min(50%, 22rem); }
|
|
434
|
+
.region.to-compact > .region-header-buttons { min-height: 0; top: 0.35rem; }
|
|
429
435
|
.region-body { padding: 1rem; }
|
|
430
436
|
.region-footer { display: flex; padding: 0.75rem 1rem; border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--radius) var(--radius); }
|
|
431
437
|
.region-plain > .region-footer { background: none; padding: 1rem 0 0; }
|
|
@@ -448,6 +454,8 @@ dialog.region-inline_drawer:target { inset: 0 0 0 auto; }
|
|
|
448
454
|
|
|
449
455
|
/* ---------- forms ---------- */
|
|
450
456
|
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 1rem 1.25rem; }
|
|
457
|
+
/* (167) the items of a region type that shows content of its own (a tree, chart, map …): under that content */
|
|
458
|
+
.form-grid.region-items { margin-top: 1rem; }
|
|
451
459
|
.page-items { margin-bottom: 1rem; }
|
|
452
460
|
.field { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
453
461
|
.field[data-wide] { grid-column: 1 / -1; }
|
|
@@ -1815,7 +1823,8 @@ td.tc-cell { overflow-wrap: anywhere; }
|
|
|
1815
1823
|
.t-navbar a[aria-current="page"] { background: rgb(255 255 255 / 16%); }
|
|
1816
1824
|
.t-navbar-menu a { display: flex; align-items: center; gap: 0.45rem; padding: 0.4rem 0.9rem; }
|
|
1817
1825
|
@media (max-width: 1024px) {
|
|
1818
|
-
|
|
1826
|
+
/* (166) visually hidden, still the link's name: an aria-label would leave out the badge (label-content-name-mismatch) */
|
|
1827
|
+
.t-navbar .nb-icon > span:not(.badge) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
1819
1828
|
}
|
|
1820
1829
|
@media (max-width: 640px) {
|
|
1821
1830
|
.list-badges { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
package/public/app.js
CHANGED
|
@@ -595,9 +595,16 @@ const pgkilnUnsaved = (() => {
|
|
|
595
595
|
// (156) stops the actions after it; the triggering event's own effect is stopped by its handler (cancels())
|
|
596
596
|
case 'cancel_event':
|
|
597
597
|
return STOP;
|
|
598
|
-
case 'set_style':
|
|
599
|
-
|
|
598
|
+
case 'set_style': {
|
|
599
|
+
// (166) an item's own field (input, select, text area), as in APEX; its wrapper only when it has none
|
|
600
|
+
const field = (n) => {
|
|
601
|
+
const f = document.getElementById(n);
|
|
602
|
+
return f && f.matches('input, select, textarea') ? f : wrapperOf(n);
|
|
603
|
+
};
|
|
604
|
+
const els = [...da.items.map(field).filter(Boolean), ...(da.region ? [document.getElementById(`R${da.region}`)].filter(Boolean) : [])];
|
|
605
|
+
for (const el of els) for (const [k, v] of Object.entries(da.styles || {})) el.style.setProperty(k, v);
|
|
600
606
|
return;
|
|
607
|
+
}
|
|
601
608
|
case 'collapse_tree':
|
|
602
609
|
case 'expand_tree':
|
|
603
610
|
for (const el of targets(da)) el.querySelectorAll('.tree-view details').forEach((d) => (d.open = da.action === 'expand_tree'));
|
|
@@ -615,7 +622,12 @@ const pgkilnUnsaved = (() => {
|
|
|
615
622
|
}
|
|
616
623
|
return;
|
|
617
624
|
case 'trigger_geocoding':
|
|
618
|
-
for (const n of da.items)
|
|
625
|
+
for (const n of da.items) {
|
|
626
|
+
// (166) not for the address just chosen from the list (a change action on the address item itself would reopen it)
|
|
627
|
+
const field = document.getElementById(n);
|
|
628
|
+
if (field && field.dataset.geocoded !== undefined && field.value === field.dataset.geocoded) continue;
|
|
629
|
+
document.querySelector(`[data-geocode="${CSS.escape(n)}"]`)?.click();
|
|
630
|
+
}
|
|
619
631
|
return;
|
|
620
632
|
case 'open_report_dialog':
|
|
621
633
|
for (const el of targets(da)) {
|
|
@@ -1015,12 +1027,17 @@ const pgkilnUnsaved = (() => {
|
|
|
1015
1027
|
e.stopImmediatePropagation();
|
|
1016
1028
|
const submitter = e.submitter;
|
|
1017
1029
|
if (!(await runChain(run))) return;
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
submitting =
|
|
1023
|
-
|
|
1030
|
+
// (166) in a new task: for a submit the user started (a button, Enter) the browser is still firing
|
|
1031
|
+
// this submit event when the actions end, and ignores a requestSubmit made then. requestSubmit
|
|
1032
|
+
// dispatches its submit event synchronously, so the flag lets exactly that one through.
|
|
1033
|
+
setTimeout(() => {
|
|
1034
|
+
submitting = true;
|
|
1035
|
+
try {
|
|
1036
|
+
form.requestSubmit(submitter && submitter.form === form ? submitter : undefined);
|
|
1037
|
+
} finally {
|
|
1038
|
+
submitting = false;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1024
1041
|
}, true);
|
|
1025
1042
|
|
|
1026
1043
|
// ------------------------------------------------------------ cascading lists of values
|
|
@@ -1579,6 +1596,8 @@ const pgkilnUnsaved = (() => {
|
|
|
1579
1596
|
if (!row) return;
|
|
1580
1597
|
e.preventDefault();
|
|
1581
1598
|
closeMenus(dup);
|
|
1599
|
+
// (167) a copy is not the template's default values: it is saved even when it matches them
|
|
1600
|
+
row.querySelector('input[name$="_def"]')?.remove();
|
|
1582
1601
|
for (const td of row.querySelectorAll('td[data-col]')) {
|
|
1583
1602
|
const from = src.querySelector(`td[data-col="${td.dataset.col}"]`);
|
|
1584
1603
|
if (from && control(td) && control(from)) setCell(td, control(from).tagName === 'SELECT' ? control(from).value : cellText(from));
|
|
@@ -3082,6 +3101,8 @@ const liveBadges = (() => {
|
|
|
3082
3101
|
b.textContent = p.label;
|
|
3083
3102
|
b.addEventListener('click', () => {
|
|
3084
3103
|
input.value = p.label;
|
|
3104
|
+
// (166) the address chosen: trigger_geocoding on this item's change doesn't look it up again
|
|
3105
|
+
input.dataset.geocoded = p.label;
|
|
3085
3106
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3086
3107
|
const target = btn.dataset.coords && document.getElementById(btn.dataset.coords);
|
|
3087
3108
|
if (target) {
|
|
@@ -180,7 +180,7 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
|
|
|
180
180
|
{ name: 'readonly_condition', label: 'Read-only condition (SQL)', kind: 'code', group: 'Security', help: 'When true the item is shown read-only and ignored on submit.' },
|
|
181
181
|
{ name: 'authz', label: 'Authorization', kind: 'authz', group: 'Security', help: AUTHZ_HELP },
|
|
182
182
|
buildOption('Security'),
|
|
183
|
-
{ name: 'config', label: 'Attributes (JSON)', kind: 'json', group: 'Attributes', help: '{"submit_on_change":true,"null_label":"- All -","cascade_parents":"P3_DEPTNO","wide":true}. {"warn_unsaved":false}: a change of this item doesn\'t count for the page\'s "Warn on unsaved changes". Lists of values read at most {"max_rows":5000} rows. File items: {"filename_column":"photo_name","mime_column":"photo_mime","size_column":"photo_size","updated_column":"photo_updated","accept":"image/*,.pdf","max_mb":2,"capture":"environment","max_px":1600} (capture opens the camera on phones; max_px makes photos smaller before upload). {"crop":"1:1"} lets the user crop a picture before upload (free, 1:1, 4:3, 3:4, 16:9, 3:2, 2:3). Several files: {"multiple":true,"max_files":5,"table":"doc.attachment","parent_column":"ticket_id","key_column":"id"} with the content column as source (one row per file). Text items: {"scan":true} adds a barcode/QR scan button where the browser can read codes. Location items hold "lat,lng" with a "Use my location" button. Rich text: sanitised HTML; markdown: Markdown text ({"rows":10}). Rating: {"max":5} stars, stored as 1..max. Combobox: free text with list-of-values suggestions, values colon-separated ({"multiple":false} for one value). Shuttle: two lists (available, chosen) with move and order buttons; the chosen values colon-separated in their order. Select One / Select Many: a search field over the list of values (Select Many: chosen values as chips, colon-separated); {"null_label":"- None -"} or false for Select One. Date range: "from:to" (ISO dates; split_part(:P1_X, \':\', 1)). QR code: shows the value as a QR code ({"ecc":"M","size":200,"show_value":true}). Password: {"reveal":true} adds a show/hide button. Image (display image): the value is an https URL, a path or a data:image URL; or {"sql":"select photo, photo_mime from hr.emp where id = :P3_ID"} shows the first column of the first row (a BLOB); {"alt":"Photo of &P3_NAME.","width":160,"height":160}. Address: {"coordinates_item":"P3_LOCATION","country":"nl"} (a "Check address" button where the installation has a geocoder, GEOCODER_URL; the chosen address\'s position goes into the item as "lat,lng"). Any item: {"persist":"user"} keeps its value per user across sessions (a user preference named like the item). Number and display items: {"format_mask":"999G999G990D00"} shows the number in the language\'s notation (FML999G990D00 adds the currency) and reads it back on submit. · plugin: {"plugin":"name","attributes":{"NAME":"value"}} (an item plug-in: a text field its JavaScript enhances)' },
|
|
183
|
+
{ name: 'config', label: 'Attributes (JSON)', kind: 'json', group: 'Attributes', help: '{"submit_on_change":true,"null_label":"- All -","cascade_parents":"P3_DEPTNO","wide":true}. {"warn_unsaved":false}: a change of this item doesn\'t count for the page\'s "Warn on unsaved changes". Lists of values read at most {"max_rows":5000} rows. File items: {"filename_column":"photo_name","mime_column":"photo_mime","size_column":"photo_size","updated_column":"photo_updated","accept":"image/*,.pdf","max_mb":2,"capture":"environment","max_px":1600} (capture opens the camera on phones; max_px makes photos smaller before upload). {"crop":"1:1"} lets the user crop a picture before upload (free, 1:1, 4:3, 3:4, 16:9, 3:2, 2:3). Several files: {"multiple":true,"max_files":5,"table":"doc.attachment","parent_column":"ticket_id","key_column":"id"} with the content column as source (one row per file). Text items: {"scan":true} adds a barcode/QR scan button where the browser can read codes. Location items hold "lat,lng" with a "Use my location" button. Rich text: sanitised HTML; markdown: Markdown text ({"rows":10}). Rating: {"max":5} stars, stored as 1..max. Combobox: free text with list-of-values suggestions, values colon-separated ({"multiple":false} for one value). Shuttle: two lists (available, chosen) with move and order buttons; the chosen values colon-separated in their order. Select One / Select Many: a search field over the list of values (Select Many: chosen values as chips, colon-separated); {"null_label":"- None -"} or false for Select One. Date range: "from:to" (ISO dates; split_part(:P1_X, \':\', 1)). QR code: shows the value as a QR code ({"ecc":"M","size":200,"show_value":true}). Password: {"reveal":true} adds a show/hide button. Image (display image): the value is an https URL, a path or a data:image URL; or {"sql":"select photo, photo_mime from hr.emp where id = :P3_ID"} shows the first column of the first row (a BLOB); {"alt":"Photo of &P3_NAME.","width":160,"height":160}. Address: {"coordinates_item":"P3_LOCATION","country":"nl"} (a "Check address" button where the installation has a geocoder, GEOCODER_URL; the chosen address\'s position goes into the item as "lat,lng"). Hidden items: {"value_protected":false} renders the item as a hidden input that dynamic actions and JavaScript may set; its posted value is user input, like a text field\'s (protected, the default: the value stays on the server). Any item: {"persist":"user"} keeps its value per user across sessions (a user preference named like the item). Number and display items: {"format_mask":"999G999G990D00"} shows the number in the language\'s notation (FML999G990D00 adds the currency) and reads it back on submit. · plugin: {"plugin":"name","attributes":{"NAME":"value"}} (an item plug-in: a text field its JavaScript enhances)' },
|
|
184
184
|
],
|
|
185
185
|
},
|
|
186
186
|
button: {
|
|
@@ -13,7 +13,7 @@ export const REGION_CONFIG_HELP: [string[], string][] = [
|
|
|
13
13
|
[['chart'], '{"kind":"bar"|"column"|"line"|"area"|"donut","max_rows":1000,"format_mask":"FML999G990"}'],
|
|
14
14
|
[['cards'], '{"style":"metric","link":{...},"max_rows":500}'],
|
|
15
15
|
[['dynamic'], '{"max_rows":1000}'],
|
|
16
|
-
[['grid'], '{"page_size":25,"allow":{"insert":true,"update":true,"delete":true},"readonly":["col"],"columns":{"deptno":{"lov":"LOV:DEPARTMENTS","required":true}},"aggregates":{"sal":["sum","avg"]},"layout":{"order":["col"],"hidden":["col"],"widths":{"col":160},"frozen":1},"row_actions":{"edit":{"page":3,"items":{"P3_ID":"#id#"}},"duplicate":true,"delete":true,"links":[{"label":"…","page":4,"items":{}}]},"select_row":{"column":"id","item":"P1_ID"} (a master),"master":{"item":"P1_ID","column":"parent_id"} (a detail; any region type),"actions":true,"saved_reports":true,"public_reports":"SCHEME","views":["row","group","chart"] (views besides the grid; left out: all three)}'],
|
|
16
|
+
[['grid'], '{"page_size":25,"allow":{"insert":true,"update":true,"delete":true},"readonly":["col"],"columns":{"deptno":{"lov":"LOV:DEPARTMENTS","required":true,"default":"&P1_DEPTNO."},"hiredate":{"default_sql":"select current_date"}} (default / default_sql: the value a new row starts with),"aggregates":{"sal":["sum","avg"]},"layout":{"order":["col"],"hidden":["col"],"widths":{"col":160},"frozen":1},"row_actions":{"edit":{"page":3,"items":{"P3_ID":"#id#"}},"duplicate":true,"delete":true,"links":[{"label":"…","page":4,"items":{}}]},"select_row":{"column":"id","item":"P1_ID"} (a master),"master":{"item":"P1_ID","column":"parent_id"} (a detail; any region type),"actions":true,"saved_reports":true,"public_reports":"SCHEME","views":["row","group","chart"] (views besides the grid; left out: all three)}'],
|
|
17
17
|
[['calendar'], '{"link":{...}}'],
|
|
18
18
|
[['facets'], '{"report":<region id>,"search":true,"facets":[{"column":"job","label":"Job","exclude":true},{"column":"sal","type":"range","ranges":[{"to":1000},{"from":1000}],"custom":true},{"column":"rating","type":"star","max":5}]}'],
|
|
19
19
|
[['smart_filters'], '{"report":<region id>,"suggestions":3,"placeholder":"…","ask":true,"facets":[…as facets]}'],
|
|
@@ -27,7 +27,7 @@ export const REGION_CONFIG_HELP: [string[], string][] = [
|
|
|
27
27
|
[['report'], '{"ai_filter":{}} adds an "Ask in your own words" box: the question becomes the report\'s settings, shown as chips (built-in phrases; {"service":"NAME"} uses an AI service)'],
|
|
28
28
|
[['plugin'], '{"plugin":"name","attributes":{"NAME":"value"}} (a region plug-in; a Source SELECT feeds its template)'],
|
|
29
29
|
[['form'], '{"lost_update":false} saves without checking that the row is unchanged since it was shown'],
|
|
30
|
-
[['login'], '(
|
|
30
|
+
[['login'], '{"autofocus":false} (the cursor is not put in the user name field) · the sign-in form of the application\'s login page (Application settings → Login page): providers, user name and password, "Keep me signed in", "Forgot password?"'],
|
|
31
31
|
[['url'], '{"height":480,"sandbox":["allow-scripts","allow-forms","allow-popups","allow-same-origin"] (the default; false: no sandbox),"fullscreen":true}'],
|
|
32
32
|
[['classic_report'], '{"report_template":"standard"|"value_pairs"|"badges"|"media_list","pagination":"total" (rows X–Y of N, the default)|"range" (rows X–Y)|"none" (one page of at most max_rows, 500),"page_size":15,"max_rows":500,"sortable":true,"searchable":false,"mobile":"reflow"|"scroll","hidden":["col"],"headings":{"col":"Label"},"link":{"column":"id","page":3,"items":{"P3_ID":"#id#"}},"empty":"No rows"} (a report without the Actions menu; badges: label and value columns; media_list: title and description columns)'],
|
|
33
33
|
[['breadcrumb'], '{"current":false} leaves the current page out of the trail (the pages above it come from each page\'s Breadcrumb parent, else Home); put it in the Breadcrumb bar position with template plain'],
|
package/src/runtime/authz.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { savepoint } from '../db.ts';
|
|
|
5
5
|
import { bindValues, publicError, stripSemicolon, type PageContext, type Visibility } from './context.ts';
|
|
6
6
|
import { saveButton, saveRequest } from './grid.ts';
|
|
7
7
|
import { selectionOf } from './report.ts';
|
|
8
|
+
import { unprotectedHidden } from './items.ts';
|
|
8
9
|
import { conditionHolds, conditionOf, serverConditionOf } from './logic.ts';
|
|
9
10
|
|
|
10
11
|
export class Forbidden extends Error {}
|
|
@@ -99,6 +100,12 @@ export async function computeVisibility(ctx: PageContext): Promise<Visibility> {
|
|
|
99
100
|
if (!(await isAuthorized(ctx, i.authz))) continue;
|
|
100
101
|
if (i.condition_type && !(await conditionHolds(ctx, i as Condition, `condition of item ${i.name}`))) continue;
|
|
101
102
|
vis.items.add(i.name);
|
|
103
|
+
// (167) a hidden item with {"value_protected": false} is a field of the page (APEX: Value Protected = No):
|
|
104
|
+
// the browser may set it, and a submit or dynamic action takes its posted value, as any field's
|
|
105
|
+
if (i.type === 'hidden' && unprotectedHidden(i)) {
|
|
106
|
+
vis.editable.add(i.name);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
102
109
|
if (i.type === 'hidden' || i.type === 'display' || i.type === 'qrcode' || i.type === 'image' || i.type === 'percent_graph' || i.type === 'display_map') continue;
|
|
103
110
|
// fail closed: a broken read-only condition makes the item read-only
|
|
104
111
|
if (i.readonly_condition && (await sqlTrue(ctx, i.readonly_condition, `read-only condition of ${i.name}`, true))) continue;
|