pgkiln 0.50.0 → 0.52.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/SECURITY.md +7 -0
- package/db/migrations/119_workspace_home.sql +30 -0
- package/db/migrations/120_datasets.sql +87 -0
- package/db/migrations/121_ai_agents.sql +94 -0
- package/db/migrations/122_team_development.sql +186 -0
- package/db/migrations/123_workflow_outcomes.sql +407 -0
- package/db/migrations/124_component_details.sql +51 -0
- package/docs/apex-feature-parity.md +15 -12
- package/docs/guide/01-installation.md +9 -3
- package/docs/guide/03-builder.md +211 -11
- package/docs/guide/04-pages-and-regions.md +96 -22
- package/docs/guide/05-items.md +7 -1
- package/docs/guide/06-processing.md +123 -8
- package/docs/guide/07-dynamic-actions.md +34 -0
- package/docs/guide/09-reference.md +2 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +70 -11
- package/docs/guide/20-ai-agents.md +4 -2
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_63_datasets.sql +29 -0
- package/examples/hr/hr_64_ai_agent.sql +97 -0
- package/examples/hr/hr_65_ask_reports.sql +21 -0
- package/examples/hr/hr_66_team_development.sql +58 -0
- package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
- package/examples/hr/hr_68_component_details.sql +96 -0
- package/package.json +1 -1
- package/public/app.css +46 -0
- package/public/app.js +134 -20
- package/public/builder.css +116 -0
- package/public/builder.js +47 -0
- package/src/ai/chat.ts +18 -18
- package/src/ai/service.ts +35 -11
- package/src/ai/types.ts +7 -2
- package/src/app.ts +11 -1
- package/src/builder/advisor.ts +6 -3
- package/src/builder/ai-agents.ts +372 -0
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/ai.ts +13 -9
- package/src/builder/assistant.ts +22 -7
- package/src/builder/components.ts +38 -9
- package/src/builder/datasets.ts +472 -0
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +6 -1
- package/src/builder/home.ts +12 -7
- package/src/builder/instance.ts +2 -0
- package/src/builder/locks.ts +7 -1
- package/src/builder/region-help.ts +2 -2
- package/src/builder/reporter.ts +52 -3
- package/src/builder/routes.ts +7 -4
- package/src/builder/shared.ts +1 -1
- package/src/builder/subscriptions.ts +1 -0
- package/src/builder/team.ts +991 -0
- package/src/builder/ui.ts +8 -5
- package/src/builder/workflow-migrate.ts +210 -0
- package/src/builder/workflows.ts +29 -6
- package/src/builder/workspace-home.ts +285 -0
- package/src/builder/workspace-transfer.ts +1 -1
- package/src/builder/workspaces.ts +7 -4
- package/src/cli/replace.ts +7 -1
- package/src/datasets.ts +367 -0
- package/src/i18n/ar.ts +33 -0
- package/src/i18n/cs.ts +33 -0
- package/src/i18n/da.ts +33 -0
- package/src/i18n/de.ts +33 -0
- package/src/i18n/el.ts +33 -0
- package/src/i18n/es.ts +33 -0
- package/src/i18n/fi.ts +33 -0
- package/src/i18n/fr.ts +33 -0
- package/src/i18n/he.ts +33 -0
- package/src/i18n/it.ts +33 -0
- package/src/i18n/ja.ts +33 -0
- package/src/i18n/ko.ts +33 -0
- package/src/i18n/nb.ts +33 -0
- package/src/i18n/pl.ts +33 -0
- package/src/i18n/pt.ts +33 -0
- package/src/i18n/ru.ts +33 -0
- package/src/i18n/sv.ts +33 -0
- package/src/i18n/tr.ts +33 -0
- package/src/i18n/uk.ts +33 -0
- package/src/i18n/zh.ts +33 -0
- package/src/i18n.ts +66 -0
- package/src/instance.ts +3 -0
- package/src/metadata.ts +9 -3
- package/src/release-check.ts +139 -0
- package/src/reporting-app.ts +101 -0
- package/src/runtime/ai-agents.ts +365 -0
- package/src/runtime/ai-filter.ts +385 -107
- package/src/runtime/assistant.ts +64 -11
- package/src/runtime/context.ts +2 -0
- package/src/runtime/data-reporter.ts +199 -32
- package/src/runtime/ir-phrases.ts +812 -0
- package/src/runtime/ir-plan.ts +449 -0
- package/src/runtime/items.ts +1 -1
- package/src/runtime/regions.ts +6 -2
- package/src/runtime/render.ts +1 -1
- package/src/runtime/report.ts +1 -1
- package/src/runtime/tasks.ts +63 -12
- package/src/session.ts +2 -0
- package/src/subscriptions.ts +2 -1
- package/src/task-events.ts +232 -0
- package/src/team-samples.ts +74 -0
- package/src/team.ts +342 -0
- package/src/workflow.ts +135 -18
- package/src/workspace-export.ts +39 -4
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- HR example, part 68: component details of APEX 26.1 (migration 124;
|
|
3
|
+
-- docs/guide/05-items.md "Popup LOV", docs/guide/07-dynamic-actions.md
|
|
4
|
+
-- "Wait popup" and "Menu entry chosen")
|
|
5
|
+
--
|
|
6
|
+
-- Page 47 "Component details":
|
|
7
|
+
-- * P47_REF, a popup LOV over 6,000 reference numbers: the dialog loads
|
|
8
|
+
-- the next 100 as the list scrolls (no More button).
|
|
9
|
+
-- * RECALC, a submit button whose process takes two seconds: a dynamic
|
|
10
|
+
-- action shows the wait popup on its click; the next page removes it.
|
|
11
|
+
-- * CHECK ("Check in the background"): wait popup, an Ajax call (execute_sql,
|
|
12
|
+
-- a second and a half) that fills P47_RESULT, remove wait popup.
|
|
13
|
+
-- * ACTIONS, a menu button: a link, and two entries that run dynamic
|
|
14
|
+
-- actions (event menu_entry): COUNT (the same Ajax call with a wait
|
|
15
|
+
-- popup) and HELLO (a success message).
|
|
16
|
+
-- =====================================================================
|
|
17
|
+
|
|
18
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
19
|
+
select id, 47, 'Component details', 'Component details', 1 from meta.app where alias = 'hr';
|
|
20
|
+
|
|
21
|
+
insert into meta.region (page_id, seq, title, type, columns, template, source)
|
|
22
|
+
select p.id, 10, 'Component details', 'static', 12, 'standard',
|
|
23
|
+
'<p>A popup list of values that loads more rows as you scroll, a wait popup around a long process and an Ajax call, and a menu button whose entries run dynamic actions.</p>'
|
|
24
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
25
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
26
|
+
|
|
27
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, lov, config, help)
|
|
28
|
+
select p.id, r.id, i.seq, i.name, i.label, i.type, i.lov, i.config::jsonb, i.help
|
|
29
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
30
|
+
join meta.region r on r.page_id = p.id and r.title = 'Component details', (values
|
|
31
|
+
(10, 'P47_REF', 'Reference', 'popup_lov',
|
|
32
|
+
'select ''REF-'' || lpad(n::text, 5, ''0'') || '' · '' || (array[''Accounting'', ''Research'', ''Sales'', ''Operations''])[1 + n % 4] as name, n as ref
|
|
33
|
+
from generate_series(1, 6000) n
|
|
34
|
+
order by n',
|
|
35
|
+
'{"page_size": 100}', '6,000 reference numbers: scroll the dialog to load more.'),
|
|
36
|
+
(20, 'P47_RESULT', 'Result', 'text', null, '{}', 'Filled by the Ajax call behind Check in the background, or Actions → Count employees.')
|
|
37
|
+
) as i (seq, name, label, type, lov, config, help)
|
|
38
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
39
|
+
|
|
40
|
+
insert into meta.button (page_id, region_id, seq, name, label, action, hot, menu)
|
|
41
|
+
select p.id, r.id, b.seq, b.name, b.label, b.action, b.hot, b.menu::jsonb
|
|
42
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
43
|
+
join meta.region r on r.page_id = p.id and r.title = 'Component details', (values
|
|
44
|
+
(10, 'RECALC', 'Recalculate', 'submit', true, null),
|
|
45
|
+
(20, 'CHECK', 'Check in the background', 'da', false, null),
|
|
46
|
+
(30, 'ACTIONS', 'Actions', 'menu', false,
|
|
47
|
+
'[{"label": "Count employees", "action": "COUNT", "icon": "users"},
|
|
48
|
+
{"label": "Say hello", "action": "HELLO", "icon": "bell"},
|
|
49
|
+
{"label": "Pick an employee", "page": 26, "icon": "search"}]')
|
|
50
|
+
) as b (seq, name, label, action, hot, menu)
|
|
51
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
52
|
+
|
|
53
|
+
-- the long process: two seconds on RECALC
|
|
54
|
+
insert into meta.process (page_id, seq, name, type, point, code, when_button, success_message)
|
|
55
|
+
select p.id, 10, 'Recalculate (slow)', 'sql', 'submit', 'select pg_sleep(2)', 'RECALC', 'Recalculated.'
|
|
56
|
+
from meta.page p join meta.app a on a.id = p.app_id
|
|
57
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
58
|
+
|
|
59
|
+
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_items, code, message)
|
|
60
|
+
select p.id, d.seq, d.name, d.event, d.trig, d.action, d.affected, d.code, d.message
|
|
61
|
+
from meta.page p join meta.app a on a.id = p.app_id, (values
|
|
62
|
+
(10, 'Wait while recalculating', 'click', 'RECALC', 'wait_popup', null, null, 'Recalculating…'),
|
|
63
|
+
(20, 'Wait for the check', 'click', 'CHECK', 'wait_popup', null, null, 'Checking…'),
|
|
64
|
+
(30, 'The check', 'click', 'CHECK', 'execute_sql', 'P47_RESULT',
|
|
65
|
+
'select (select count(*) from hr.emp) || '' employees, checked at '' || to_char(clock_timestamp(), ''HH24:MI:SS'') as p47_result from pg_sleep(1.5)', null),
|
|
66
|
+
(40, 'Check done', 'click', 'CHECK', 'remove_wait_popup', null, null, null),
|
|
67
|
+
(50, 'Wait for the count', 'menu_entry', 'COUNT', 'wait_popup', null, null, 'Counting employees…'),
|
|
68
|
+
(60, 'The count', 'menu_entry', 'COUNT', 'execute_sql', 'P47_RESULT',
|
|
69
|
+
'select (select count(*) from hr.emp) || '' employees, counted at '' || to_char(clock_timestamp(), ''HH24:MI:SS'') as p47_result from pg_sleep(1.5)', null),
|
|
70
|
+
(70, 'Count done', 'menu_entry', 'COUNT', 'remove_wait_popup', null, null, null),
|
|
71
|
+
(80, 'Hello', 'menu_entry', 'HELLO', 'show_success', null, null, 'Hello from a menu entry.')
|
|
72
|
+
) as d (seq, name, event, trig, action, affected, code, message)
|
|
73
|
+
where a.alias = 'hr' and p.page_no = 47;
|
|
74
|
+
|
|
75
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
76
|
+
select id, 34, 'Component details', 'list', 47 from meta.app where alias = 'hr';
|
|
77
|
+
|
|
78
|
+
insert into meta.translation (app_id, language, source, target)
|
|
79
|
+
select a.id, 'nl', t.source, t.target
|
|
80
|
+
from meta.app a, (values
|
|
81
|
+
('Component details', 'Componentdetails'),
|
|
82
|
+
('Reference', 'Referentie'),
|
|
83
|
+
('Result', 'Resultaat'),
|
|
84
|
+
('Recalculate', 'Herberekenen'),
|
|
85
|
+
('Check in the background', 'Op de achtergrond controleren'),
|
|
86
|
+
('Actions', 'Acties'),
|
|
87
|
+
('Count employees', 'Medewerkers tellen'),
|
|
88
|
+
('Say hello', 'Hallo zeggen'),
|
|
89
|
+
('Recalculating…', 'Herberekenen…'),
|
|
90
|
+
('Checking…', 'Controleren…'),
|
|
91
|
+
('Counting employees…', 'Medewerkers tellen…'),
|
|
92
|
+
('Recalculated.', 'Herberekend.'),
|
|
93
|
+
('Hello from a menu entry.', 'Hallo vanuit een menu-item.')
|
|
94
|
+
) as t(source, target)
|
|
95
|
+
where a.alias = 'hr'
|
|
96
|
+
on conflict do nothing;
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -2042,3 +2042,49 @@ a.search-hit:hover, a.search-hit:focus-visible { background: var(--accent-soft);
|
|
|
2042
2042
|
.t-footer .icon { width: 1rem; height: 1rem; }
|
|
2043
2043
|
@media print { .t-footer { display: none; } }
|
|
2044
2044
|
.list-card:hover { border-top-color: var(--card-color); }
|
|
2045
|
+
|
|
2046
|
+
/* (sprint 110) Data Reporter: a saved report's link and actions, column descriptions */
|
|
2047
|
+
.reporter-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; margin: 0 0 0.75rem; min-width: 0; }
|
|
2048
|
+
.reporter-actions .buttons { margin: 0; }
|
|
2049
|
+
.reporter-link { flex: 1 1 14rem; min-width: 0; font-family: var(--mono, monospace); font-size: 0.85rem; }
|
|
2050
|
+
.reporter-coldesc { display: block; font-size: 0.78rem; }
|
|
2051
|
+
|
|
2052
|
+
/* (121) AI agents: links a navigate tool built under an answer; tool arguments in the AI usage log */
|
|
2053
|
+
.assistant-links { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.4rem; }
|
|
2054
|
+
.assistant-links .btn { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
|
2055
|
+
.ai-args { overflow-wrap: anywhere; word-break: break-word; }
|
|
2056
|
+
|
|
2057
|
+
/* (sprint 112) the report's settings as chips under "Ask in your own words" (src/runtime/ai-filter.ts): Edit unfolds a small form */
|
|
2058
|
+
.ask-chips { margin: 0.6rem 0 0; padding: 0; list-style: none; }
|
|
2059
|
+
.ask-chip { flex-wrap: wrap; max-width: 100%; min-width: 0; overflow-wrap: anywhere; padding-right: 0.2rem; }
|
|
2060
|
+
.ask-chip-text { min-width: 0; }
|
|
2061
|
+
.ask-edit > summary { cursor: pointer; font-size: 0.78rem; text-decoration: underline; list-style: none; padding: 0 0.2rem; }
|
|
2062
|
+
.ask-edit > summary::-webkit-details-marker { display: none; }
|
|
2063
|
+
.ask-edit[open] { flex-basis: 100%; }
|
|
2064
|
+
.ask-edit-body { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.4rem; padding: 0.4rem 0.2rem 0.3rem; }
|
|
2065
|
+
.ask-field { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1 1 8rem; max-width: 100%; }
|
|
2066
|
+
.ask-field label { font-size: 0.75rem; }
|
|
2067
|
+
.ask-field select, .ask-field input { min-width: 0; width: 100%; box-sizing: border-box; }
|
|
2068
|
+
.ask-chip-x { background: none; border: 0; color: inherit; font-size: 1rem; line-height: 1; padding: 0.15rem 0.4rem; border-radius: 99px; cursor: pointer; }
|
|
2069
|
+
.ask-chip-x:hover, .ask-chip-x:focus-visible { background: var(--surface); }
|
|
2070
|
+
/* (123) workflows: draft and send e-mail steps; custom task outcomes */
|
|
2071
|
+
.wf-send_mail rect { stroke: var(--success); stroke-width: 2; }
|
|
2072
|
+
.wf-draft rect { fill: var(--surface-2, var(--surface)); stroke: var(--muted); stroke-width: 1.5; stroke-dasharray: 6 4; }
|
|
2073
|
+
.wf-draft .wf-name, .wf-draft .wf-type { font-style: italic; }
|
|
2074
|
+
.btn.to-warning { background: var(--warning-soft); border-color: color-mix(in srgb, #b86e00 45%, var(--border)); color: var(--text); }
|
|
2075
|
+
.btn.to-warning:hover { background: color-mix(in srgb, var(--warning-soft) 80%, #b86e00); }
|
|
2076
|
+
.wf-export { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0 0; }
|
|
2077
|
+
.wf-migrate-map { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); gap: 0.5rem 1rem; }
|
|
2078
|
+
.wf-migrate-map label { display: flex; flex-direction: column; gap: 0.2rem; overflow-wrap: anywhere; }
|
|
2079
|
+
.wf-migrate-map select { max-width: 100%; }
|
|
2080
|
+
.wf-problem { color: var(--danger); overflow-wrap: anywhere; }
|
|
2081
|
+
|
|
2082
|
+
/* (sprint 115) the popup LOV's end of list while the next page loads, and the wait popup (dynamic action wait_popup) */
|
|
2083
|
+
.popup-lov-sentinel { margin: 0.5rem 0 0; text-align: center; font-size: 0.875rem; }
|
|
2084
|
+
dialog.wait-popup { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); box-shadow: var(--shadow); padding: 1.25rem 1.5rem; max-width: min(360px, calc(100vw - 2rem)); display: none; }
|
|
2085
|
+
dialog.wait-popup[open] { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; }
|
|
2086
|
+
dialog.wait-popup:focus { outline: none; }
|
|
2087
|
+
dialog.wait-popup::backdrop { background: rgb(15 20 30 / 35%); cursor: wait; }
|
|
2088
|
+
.wait-popup-spinner { width: 2rem; height: 2rem; border-radius: 50%; border: 3px solid var(--border); border-top-color: var(--accent); }
|
|
2089
|
+
.wait-popup-text { margin: 0; text-align: center; overflow-wrap: anywhere; }
|
|
2090
|
+
@media (prefers-reduced-motion: no-preference) { .wait-popup-spinner { animation: icon-spin 0.9s linear infinite; } }
|
package/public/app.js
CHANGED
|
@@ -483,6 +483,9 @@ const pgkilnUnsaved = (() => {
|
|
|
483
483
|
// (077) APEX: apex.page.isChanged()
|
|
484
484
|
isChanged: () => pgkilnUnsaved.changed(),
|
|
485
485
|
setValue: (name, value) => setItemValue(name, value),
|
|
486
|
+
// (124) APEX: apex.widget.waitPopup; the returned function removes it
|
|
487
|
+
waitPopup: (message) => (showWait(message == null ? '' : String(message)), removeWait),
|
|
488
|
+
removeWaitPopup: () => removeWait(),
|
|
486
489
|
showSuccess: (message) => showMessage(String(message), 'success'),
|
|
487
490
|
showError: (message, item) => (item ? itemError(item, String(message)) : showMessage(String(message), 'error')),
|
|
488
491
|
clearErrors: (...items) => clearErrors(items),
|
|
@@ -521,6 +524,39 @@ const pgkilnUnsaved = (() => {
|
|
|
521
524
|
}
|
|
522
525
|
}
|
|
523
526
|
|
|
527
|
+
// (124) APEX: Show / Remove Wait Popup. A modal dialog (the page below is inert, Escape doesn't
|
|
528
|
+
// close it) with a spinner and a status text read out by screen readers; <main> is aria-busy.
|
|
529
|
+
// It goes with the page on a submit; a submit the browser refused (an invalid field) or a page
|
|
530
|
+
// restored from the back/forward cache removes it.
|
|
531
|
+
let waitDlg = null;
|
|
532
|
+
function showWait(message) {
|
|
533
|
+
if (!waitDlg) {
|
|
534
|
+
waitDlg = document.createElement('dialog');
|
|
535
|
+
waitDlg.className = 'wait-popup';
|
|
536
|
+
waitDlg.tabIndex = -1;
|
|
537
|
+
const spin = document.createElement('span');
|
|
538
|
+
spin.className = 'wait-popup-spinner';
|
|
539
|
+
spin.setAttribute('aria-hidden', 'true');
|
|
540
|
+
const p = document.createElement('p');
|
|
541
|
+
p.className = 'wait-popup-text';
|
|
542
|
+
p.id = 'pgkiln-wait-text';
|
|
543
|
+
p.setAttribute('role', 'status');
|
|
544
|
+
waitDlg.setAttribute('aria-labelledby', p.id);
|
|
545
|
+
waitDlg.append(spin, p);
|
|
546
|
+
waitDlg.addEventListener('cancel', (e) => e.preventDefault());
|
|
547
|
+
body.appendChild(waitDlg);
|
|
548
|
+
}
|
|
549
|
+
waitDlg.querySelector('.wait-popup-text').textContent = message || text('da.wait');
|
|
550
|
+
if (!waitDlg.open) waitDlg.showModal();
|
|
551
|
+
(document.getElementById('main') || body).setAttribute('aria-busy', 'true');
|
|
552
|
+
}
|
|
553
|
+
function removeWait() {
|
|
554
|
+
if (waitDlg && waitDlg.open) waitDlg.close();
|
|
555
|
+
(document.getElementById('main') || body).removeAttribute('aria-busy');
|
|
556
|
+
}
|
|
557
|
+
document.addEventListener('invalid', () => waitDlg && waitDlg.open && removeWait(), true);
|
|
558
|
+
window.addEventListener('pageshow', (e) => e.persisted && removeWait());
|
|
559
|
+
|
|
524
560
|
async function runDa(da, initial, value, extra) {
|
|
525
561
|
const holds = conditionHolds(da, value);
|
|
526
562
|
switch (da.action) {
|
|
@@ -567,6 +603,13 @@ const pgkilnUnsaved = (() => {
|
|
|
567
603
|
case 'print':
|
|
568
604
|
window.print();
|
|
569
605
|
return;
|
|
606
|
+
// (124) the wait popup: until remove_wait_popup, the next page or a refused submit
|
|
607
|
+
case 'wait_popup':
|
|
608
|
+
showWait(da.message);
|
|
609
|
+
return;
|
|
610
|
+
case 'remove_wait_popup':
|
|
611
|
+
removeWait();
|
|
612
|
+
return;
|
|
570
613
|
case 'current_position':
|
|
571
614
|
return new Promise((resolve) => {
|
|
572
615
|
if (!navigator.geolocation) return showError(text('item.locate_error')), resolve();
|
|
@@ -679,6 +722,28 @@ const pgkilnUnsaved = (() => {
|
|
|
679
722
|
}
|
|
680
723
|
|
|
681
724
|
const das = meta.das || [];
|
|
725
|
+
// (121) an AI agent's javascript tools: after the answer, the registered function of the action
|
|
726
|
+
// (pgkiln.actions.register) and the dynamic actions of the custom event of that name, with the arguments
|
|
727
|
+
scriptsLoaded.then(async () => {
|
|
728
|
+
for (const el of document.querySelectorAll('.assistant-action[data-ai-action]')) {
|
|
729
|
+
const name = el.dataset.aiAction;
|
|
730
|
+
let args = {};
|
|
731
|
+
try {
|
|
732
|
+
args = JSON.parse(el.dataset.aiArgs || '{}');
|
|
733
|
+
} catch {
|
|
734
|
+
// ignored
|
|
735
|
+
}
|
|
736
|
+
const region = el.closest('[id^="R"]');
|
|
737
|
+
el.remove();
|
|
738
|
+
const fn = actions.get(name);
|
|
739
|
+
try {
|
|
740
|
+
if (fn) await fn({ value: args, items: [], region, elements: [], message: null });
|
|
741
|
+
await runChain(das.filter((da) => da.event === 'custom' && da.trigger.some((t) => String(t).toLowerCase() === name.toLowerCase())), JSON.stringify(args));
|
|
742
|
+
} catch (e) {
|
|
743
|
+
console.error(e);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
682
747
|
document.addEventListener('change', (e) => {
|
|
683
748
|
const name = e.target.name;
|
|
684
749
|
if (!name) return;
|
|
@@ -688,7 +753,7 @@ const pgkilnUnsaved = (() => {
|
|
|
688
753
|
});
|
|
689
754
|
document.addEventListener('click', (e) => {
|
|
690
755
|
const btn = e.target.closest('[data-button]');
|
|
691
|
-
if (!btn) return;
|
|
756
|
+
if (!btn || e.target.closest('[data-menu-entry]')) return;
|
|
692
757
|
const run = das.filter((da) => da.event === 'click' && da.trigger.includes(btn.dataset.button));
|
|
693
758
|
// "Generate text with AI" on a submit button: the AJAX call replaces the submit
|
|
694
759
|
// (without JavaScript the button submits and the page process runs instead)
|
|
@@ -705,6 +770,21 @@ const pgkilnUnsaved = (() => {
|
|
|
705
770
|
});
|
|
706
771
|
}
|
|
707
772
|
});
|
|
773
|
+
// (124) menu_entry: an entry {"action": NAME} of a menu button was chosen (a declined confirm
|
|
774
|
+
// stops it in the capture handler below). Trigger: the entry's name or the menu button's name;
|
|
775
|
+
// the value a client-side condition tests is the entry's name.
|
|
776
|
+
document.addEventListener('click', (e) => {
|
|
777
|
+
const entry = e.target.closest?.('[data-menu-entry]');
|
|
778
|
+
if (!entry) return;
|
|
779
|
+
const name = entry.dataset.menuEntry;
|
|
780
|
+
const menu = entry.closest('details.menu');
|
|
781
|
+
const owner = (menu && menu.dataset.button) || '';
|
|
782
|
+
if (menu) {
|
|
783
|
+
menu.open = false;
|
|
784
|
+
menu.querySelector('summary')?.focus();
|
|
785
|
+
}
|
|
786
|
+
runChain(das.filter((da) => da.event === 'menu_entry' && (da.trigger.includes(name) || (owner && da.trigger.includes(owner)))), name);
|
|
787
|
+
});
|
|
708
788
|
for (const da of das) if (da.event !== 'dialog_closed' && ['show', 'hide', 'enable', 'disable'].includes(da.action)) runDa(da, true);
|
|
709
789
|
runChain(das.filter((da) => da.event === 'load'));
|
|
710
790
|
|
|
@@ -1882,7 +1962,7 @@ document.querySelectorAll('[data-rds]').forEach((nav) => {
|
|
|
1882
1962
|
|
|
1883
1963
|
// Popup list of values: the select (which works without JavaScript) is hidden
|
|
1884
1964
|
// behind a read-only field and a button that opens a dialog searching the
|
|
1885
|
-
// item's list of values on the server, page by page.
|
|
1965
|
+
// item's list of values on the server, page by page (loaded as the list scrolls).
|
|
1886
1966
|
function enhancePopupLovs(root) {
|
|
1887
1967
|
const selects = root.matches?.('select[data-popup-lov]') ? [root] : [...(root.querySelectorAll?.('select[data-popup-lov]') ?? [])];
|
|
1888
1968
|
selects.forEach(popupLov);
|
|
@@ -1964,8 +2044,16 @@ function popupLov(sel) {
|
|
|
1964
2044
|
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1965
2045
|
dlg.close();
|
|
1966
2046
|
};
|
|
2047
|
+
// (124) infinite scrolling: the next page loads when the end of the list scrolls into view
|
|
2048
|
+
// (APEX: lazy loading); without IntersectionObserver the dialog pages with ‹ and More
|
|
2049
|
+
const infinite = 'IntersectionObserver' in window;
|
|
2050
|
+
let observer = null;
|
|
2051
|
+
let seq = 0;
|
|
2052
|
+
let loading = false;
|
|
1967
2053
|
async function load(p) {
|
|
1968
2054
|
page = p;
|
|
2055
|
+
const my = ++seq;
|
|
2056
|
+
const append = infinite && p > 0;
|
|
1969
2057
|
const results = dlg.querySelector('.popup-lov-results');
|
|
1970
2058
|
const params = new URLSearchParams({ q: dlg.querySelector('.popup-lov-search').value, p: String(p), __csrf: csrf });
|
|
1971
2059
|
// a cascading LOV sends its parents' current values
|
|
@@ -1975,31 +2063,45 @@ function popupLov(sel) {
|
|
|
1975
2063
|
if (el) params.set(parent, el.value);
|
|
1976
2064
|
}
|
|
1977
2065
|
let json;
|
|
2066
|
+
loading = true;
|
|
1978
2067
|
try {
|
|
1979
2068
|
const res = await fetch(d.popupLov, { method: 'POST', body: params, headers: { accept: 'application/json' }, credentials: 'same-origin' });
|
|
1980
2069
|
json = await res.json();
|
|
1981
2070
|
if (!res.ok) throw new Error(json.error || res.statusText);
|
|
1982
2071
|
} catch (e) {
|
|
1983
|
-
|
|
2072
|
+
if (my !== seq) return;
|
|
2073
|
+
loading = false;
|
|
2074
|
+
const end = results.querySelector('.popup-lov-sentinel');
|
|
2075
|
+
if (append && end) end.textContent = e.message;
|
|
2076
|
+
else results.textContent = e.message;
|
|
1984
2077
|
return;
|
|
1985
2078
|
}
|
|
1986
|
-
|
|
1987
|
-
if (
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2079
|
+
// a newer search (or page) replaced this one
|
|
2080
|
+
if (my !== seq) return;
|
|
2081
|
+
loading = false;
|
|
2082
|
+
observer?.disconnect();
|
|
2083
|
+
results.querySelector('.popup-lov-pages, .popup-lov-sentinel')?.remove();
|
|
2084
|
+
let tb = append ? results.querySelector('.popup-lov-table tbody') : null;
|
|
2085
|
+
if (!tb) {
|
|
2086
|
+
results.replaceChildren();
|
|
2087
|
+
if (!json.rows.length) {
|
|
2088
|
+
const none = document.createElement('p');
|
|
2089
|
+
none.textContent = d.noneLabel || 'Nothing found.';
|
|
2090
|
+
results.append(none);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
const table = document.createElement('table');
|
|
2094
|
+
table.className = 'popup-lov-table';
|
|
2095
|
+
const tr = table.createTHead().insertRow();
|
|
2096
|
+
for (const hd of json.headings) {
|
|
2097
|
+
const th = document.createElement('th');
|
|
2098
|
+
th.scope = 'col';
|
|
2099
|
+
th.textContent = hd;
|
|
2100
|
+
tr.append(th);
|
|
2101
|
+
}
|
|
2102
|
+
tb = table.createTBody();
|
|
2103
|
+
results.append(table);
|
|
2001
2104
|
}
|
|
2002
|
-
const tb = table.createTBody();
|
|
2003
2105
|
for (const r of json.rows) {
|
|
2004
2106
|
const row = tb.insertRow();
|
|
2005
2107
|
r.columns.forEach((v, i) => {
|
|
@@ -2015,7 +2117,19 @@ function popupLov(sel) {
|
|
|
2015
2117
|
});
|
|
2016
2118
|
if (r.value === sel.value) row.className = 'is-current';
|
|
2017
2119
|
}
|
|
2018
|
-
|
|
2120
|
+
if (infinite) {
|
|
2121
|
+
if (!json.more) return;
|
|
2122
|
+
const end = document.createElement('p');
|
|
2123
|
+
end.className = 'popup-lov-sentinel muted';
|
|
2124
|
+
end.textContent = d.loadingLabel || 'Loading more…';
|
|
2125
|
+
results.append(end);
|
|
2126
|
+
// the implicit root: whichever box scrolls the list (the dialog's body or the results), it clips
|
|
2127
|
+
observer = observer || new IntersectionObserver((entries) => {
|
|
2128
|
+
if (entries.some((x) => x.isIntersecting) && !loading) load(page + 1);
|
|
2129
|
+
});
|
|
2130
|
+
observer.observe(end);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2019
2133
|
if (json.more || page > 0) {
|
|
2020
2134
|
const nav = document.createElement('div');
|
|
2021
2135
|
nav.className = 'popup-lov-pages';
|
package/public/builder.css
CHANGED
|
@@ -1359,3 +1359,119 @@ pre.wrap-pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
|
1359
1359
|
.sm-toolbar:not([hidden]) { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.25rem 0 0.75rem; }
|
|
1360
1360
|
.sm-toolbar select { width: auto; }
|
|
1361
1361
|
.sm-toolbar [aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
1362
|
+
|
|
1363
|
+
/* (sprint 109, WS-1) the Workspace home (/builder/home) */
|
|
1364
|
+
.wh-title { margin: 0 0 14px; font-size: var(--fs-xl); overflow-wrap: anywhere; }
|
|
1365
|
+
.wh-summary h2, .wh-top-head h2 { margin: 0 0 10px; }
|
|
1366
|
+
.wh-stats a.stat { color: inherit; text-decoration: none; }
|
|
1367
|
+
.wh-stats a.stat:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
|
|
1368
|
+
.wh-stats a.stat span { color: var(--accent); }
|
|
1369
|
+
.wh-top-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; }
|
|
1370
|
+
.wh-top-head h2 { margin: 0; }
|
|
1371
|
+
.wh-timeframe { display: inline-flex; }
|
|
1372
|
+
.wh-top { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: 16px; margin: 10px 0 20px; }
|
|
1373
|
+
.wh-top > .region { margin: 0; min-width: 0; }
|
|
1374
|
+
.wh-message { margin-bottom: 16px; }
|
|
1375
|
+
.wh-message-text { margin: 0 0 8px; white-space: pre-line; overflow-wrap: anywhere; }
|
|
1376
|
+
.wh-edit > summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
1377
|
+
.wh-edit > summary::-webkit-details-marker { display: none; }
|
|
1378
|
+
.wh-edit[open] > summary { margin-bottom: 10px; }
|
|
1379
|
+
.wh-release { display: flex; gap: 0.6rem; align-items: flex-start; }
|
|
1380
|
+
.wh-release p { margin: 0.2rem 0 0; overflow-wrap: anywhere; }
|
|
1381
|
+
.wh-release-form { margin-top: 10px; }
|
|
1382
|
+
@media (max-width: 640px) {
|
|
1383
|
+
.wh-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
1384
|
+
.wh-stats .stat { padding: 0.6rem 0.75rem; }
|
|
1385
|
+
.wh-stats .stat b { font-size: 1.2rem; }
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/* (sprint 110) App Builder → Data Reporter: datasets */
|
|
1389
|
+
.dataset-checks { display: flex; flex-direction: column; gap: 0.35rem; margin: 0.25rem 0 0.5rem; overflow-wrap: anywhere; }
|
|
1390
|
+
.dataset-filter { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.4rem; }
|
|
1391
|
+
.dataset-filter > select, .dataset-filter > input { flex: 1 1 10rem; min-width: 0; }
|
|
1392
|
+
.dataset-columns input { min-width: 0; width: 100%; }
|
|
1393
|
+
.dataset-page pre.code { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1394
|
+
.dataset-schema { flex-wrap: wrap; }
|
|
1395
|
+
.dataset-schema > select { flex: 1 1 10rem; min-width: 0; }
|
|
1396
|
+
|
|
1397
|
+
/* (sprint 113) Team Development: issues, board, labels, milestones, templates */
|
|
1398
|
+
.team-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; margin-bottom: 8px; }
|
|
1399
|
+
.team-head h1 { margin: 0; font-size: var(--fs-xl); }
|
|
1400
|
+
.team-head .team-counts { margin: 0; flex: 1 1 200px; }
|
|
1401
|
+
.team-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
|
|
1402
|
+
@media (min-width: 1024px) { .team-layout { grid-template-columns: minmax(0, 1fr) 280px; } }
|
|
1403
|
+
.team-main { min-width: 0; }
|
|
1404
|
+
.team-side { min-width: 0; }
|
|
1405
|
+
.team-side section { padding: 10px 0; border-top: 1px solid var(--border); }
|
|
1406
|
+
.team-side section:first-child { border-top: 0; padding-top: 0; }
|
|
1407
|
+
.team-side h2 { margin: 0 0 6px; font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
|
|
1408
|
+
.team-side p, .team-side li { overflow-wrap: anywhere; }
|
|
1409
|
+
.team-filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); gap: 8px 12px; align-items: end; margin-bottom: 10px; }
|
|
1410
|
+
.team-filters .field { margin: 0; min-width: 0; }
|
|
1411
|
+
.team-filters select, .team-filters input { width: 100%; min-width: 0; }
|
|
1412
|
+
.team-filters .buttons { margin: 0; }
|
|
1413
|
+
.team-issues { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
1414
|
+
.team-issue { padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); min-width: 0; }
|
|
1415
|
+
.team-issue.is-closed { opacity: 0.8; }
|
|
1416
|
+
.team-issue-title { display: flex; gap: 6px; align-items: baseline; font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
|
|
1417
|
+
.team-issue-title .icon { flex: none; width: 15px; height: 15px; color: var(--hot); align-self: center; }
|
|
1418
|
+
.team-issue.is-closed .team-issue-title .icon { color: var(--success, #2f7d32); }
|
|
1419
|
+
.team-issue-title a { min-width: 0; }
|
|
1420
|
+
.team-labels { display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0 0; }
|
|
1421
|
+
.team-issue-meta { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 6px; overflow-wrap: anywhere; }
|
|
1422
|
+
.team-issue-meta span { display: inline-flex; gap: 4px; align-items: center; min-width: 0; }
|
|
1423
|
+
.team-issue-meta .icon { width: 13px; height: 13px; flex: none; }
|
|
1424
|
+
.team-label { display: inline-block; max-width: 100%; padding: 1px 8px; border-radius: 999px; font-size: 0.75rem; font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; }
|
|
1425
|
+
.team-label-red { background: #b42318; color: #fff; }
|
|
1426
|
+
.team-label-orange { background: #c4520f; color: #fff; }
|
|
1427
|
+
.team-label-yellow { background: #e6b400; color: #1e1d1b; }
|
|
1428
|
+
.team-label-green { background: #2f7d32; color: #fff; }
|
|
1429
|
+
.team-label-teal { background: #0e7a74; color: #fff; }
|
|
1430
|
+
.team-label-blue { background: #1a5f99; color: #fff; }
|
|
1431
|
+
.team-label-purple { background: #6941c6; color: #fff; }
|
|
1432
|
+
.team-label-gray { background: #5e5a54; color: #fff; }
|
|
1433
|
+
.team-board-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; }
|
|
1434
|
+
.team-board-filters { margin-bottom: 10px; }
|
|
1435
|
+
.team-board-filters > summary { cursor: pointer; display: inline-flex; gap: 6px; align-items: center; color: var(--muted); }
|
|
1436
|
+
.team-board-filters[open] > summary { margin-bottom: 8px; }
|
|
1437
|
+
.team-board { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 12px; align-items: start; }
|
|
1438
|
+
.team-col { min-width: 0; padding: 10px; border-radius: var(--radius-sm); background: var(--surface-2); border: 1px solid var(--border); }
|
|
1439
|
+
.team-col-head { display: flex; gap: 8px; align-items: center; margin: 0 0 8px; font-size: var(--fs-sm, 0.9rem); overflow-wrap: anywhere; }
|
|
1440
|
+
.team-move { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 8px; }
|
|
1441
|
+
.team-move select { flex: 1 1 120px; min-width: 0; }
|
|
1442
|
+
.team-status { display: inline-block; padding: 1px 10px; border-radius: 999px; font-size: 0.8rem; font-weight: 700; margin-right: 6px; }
|
|
1443
|
+
.team-status-open { background: var(--accent-soft); color: var(--text); }
|
|
1444
|
+
.team-status-closed { background: var(--surface-3); color: var(--muted); }
|
|
1445
|
+
.team-issue-h { margin: 0 0 4px; font-size: var(--fs-lg, 1.2rem); overflow-wrap: anywhere; }
|
|
1446
|
+
.team-md { overflow-wrap: anywhere; min-width: 0; }
|
|
1447
|
+
.team-md pre { overflow-x: auto; max-width: 100%; }
|
|
1448
|
+
.team-body { padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
|
|
1449
|
+
.mention { font-weight: 600; color: var(--accent); }
|
|
1450
|
+
.team-timeline { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 8px; }
|
|
1451
|
+
.team-comment { padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); min-width: 0; }
|
|
1452
|
+
.team-comment-head { margin-bottom: 4px; overflow-wrap: anywhere; }
|
|
1453
|
+
.team-event { display: flex; gap: 6px; align-items: flex-start; overflow-wrap: anywhere; min-width: 0; }
|
|
1454
|
+
.team-event .icon { flex: none; width: 13px; height: 13px; margin-top: 3px; }
|
|
1455
|
+
.team-response-pick, .team-template-pick, .team-save-view { display: flex; flex-wrap: wrap; gap: 6px; align-items: end; margin-bottom: 8px; }
|
|
1456
|
+
.team-response-pick .field, .team-template-pick .field { margin: 0; min-width: 0; flex: 1 1 200px; }
|
|
1457
|
+
.team-save-view input { flex: 1 1 140px; min-width: 0; }
|
|
1458
|
+
.team-edit { margin-top: 16px; }
|
|
1459
|
+
.team-edit > summary { list-style: none; }
|
|
1460
|
+
.team-edit[open] > summary { margin-bottom: 10px; }
|
|
1461
|
+
.team-fieldset { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 12px; margin: 8px 0; min-width: 0; }
|
|
1462
|
+
.team-check-group { display: flex; flex-wrap: wrap; gap: 4px 14px; align-items: center; margin-bottom: 4px; }
|
|
1463
|
+
.team-check-group > .small { flex-basis: 100%; }
|
|
1464
|
+
.team-label-list, .team-template-list, .team-milestones, .team-views, .team-mentions { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
1465
|
+
.team-label-list li, .team-template-list li, .team-views li, .team-mentions li { overflow-wrap: anywhere; min-width: 0; }
|
|
1466
|
+
.team-views li { display: flex; gap: 6px; justify-content: space-between; align-items: center; }
|
|
1467
|
+
.team-views a[aria-current] { font-weight: 700; }
|
|
1468
|
+
.team-inline-edit { margin-top: 4px; }
|
|
1469
|
+
.team-inline-edit[open] { padding: 8px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
|
|
1470
|
+
.team-milestone { padding: 8px 0; border-bottom: 1px solid var(--border); }
|
|
1471
|
+
.team-milestone-head { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; overflow-wrap: anywhere; }
|
|
1472
|
+
.team-milestone-head .icon { width: 14px; height: 14px; margin-right: 4px; }
|
|
1473
|
+
.team-progress { width: 100%; max-width: 360px; height: 8px; }
|
|
1474
|
+
.team-page-issues { display: flex; gap: 6px; align-items: flex-start; margin: 0 0 6px; padding: 6px 10px; border-radius: var(--radius-sm); background: var(--warning-soft); overflow-wrap: anywhere; }
|
|
1475
|
+
.team-page-issues .icon { flex: none; width: 14px; height: 14px; margin-top: 2px; }
|
|
1476
|
+
.wh-team-n { margin-left: auto; padding-left: 8px; }
|
|
1477
|
+
.feedback-issue { margin: 4px 0; }
|
package/public/builder.js
CHANGED
|
@@ -1282,3 +1282,50 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
|
|
|
1282
1282
|
});
|
|
1283
1283
|
for (const b of form.querySelectorAll('[data-cron-nojs]')) b.hidden = true;
|
|
1284
1284
|
})();
|
|
1285
|
+
|
|
1286
|
+
// (123) Workflow diagram as PNG: the server's SVG file drawn on a canvas. Without JavaScript the
|
|
1287
|
+
// button stays hidden and the SVG link is the download.
|
|
1288
|
+
(() => {
|
|
1289
|
+
for (const button of document.querySelectorAll('[data-wf-png]')) {
|
|
1290
|
+
const link = button.parentElement?.querySelector('[data-wf-svg]');
|
|
1291
|
+
if (!link || typeof HTMLCanvasElement === 'undefined') continue;
|
|
1292
|
+
button.hidden = false;
|
|
1293
|
+
button.addEventListener('click', async () => {
|
|
1294
|
+
button.disabled = true;
|
|
1295
|
+
try {
|
|
1296
|
+
const res = await fetch(link.href, { credentials: 'same-origin' });
|
|
1297
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1298
|
+
const svg = await res.text();
|
|
1299
|
+
const img = new Image();
|
|
1300
|
+
await new Promise((ok, fail) => {
|
|
1301
|
+
img.onload = ok;
|
|
1302
|
+
img.onerror = () => fail(new Error('The diagram could not be drawn.'));
|
|
1303
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
1304
|
+
});
|
|
1305
|
+
const scale = 2;
|
|
1306
|
+
const canvas = document.createElement('canvas');
|
|
1307
|
+
canvas.width = Math.max(1, Math.round((img.naturalWidth || 800) * scale));
|
|
1308
|
+
canvas.height = Math.max(1, Math.round((img.naturalHeight || 600) * scale));
|
|
1309
|
+
const g = canvas.getContext('2d');
|
|
1310
|
+
g.fillStyle = '#ffffff';
|
|
1311
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
1312
|
+
g.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
1313
|
+
const blob = await new Promise((ok) => canvas.toBlob(ok, 'image/png'));
|
|
1314
|
+
if (!blob) throw new Error('The browser could not make a PNG.');
|
|
1315
|
+
const a = document.createElement('a');
|
|
1316
|
+
a.href = URL.createObjectURL(blob);
|
|
1317
|
+
a.download = 'workflow.png';
|
|
1318
|
+
const name = /([^/?]+)\.svg/.exec(res.headers.get('content-disposition') || '');
|
|
1319
|
+
if (name) a.download = `${name[1]}.png`;
|
|
1320
|
+
document.body.append(a);
|
|
1321
|
+
a.click();
|
|
1322
|
+
a.remove();
|
|
1323
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 10000);
|
|
1324
|
+
} catch (e) {
|
|
1325
|
+
window.alert(e.message);
|
|
1326
|
+
} finally {
|
|
1327
|
+
button.disabled = false;
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
})();
|