pgkiln 0.56.0 → 0.57.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 +1 -0
- package/docs/apex-feature-parity.md +1 -1
- package/docs/guide/03-builder.md +3 -2
- package/docs/guide/12-development.md +14 -0
- package/package.json +1 -1
- package/public/builder.css +1 -1
- package/public/worksheet.js +31 -26
- package/src/builder/i18n/de.ts +371 -0
- package/src/builder/i18n/en.ts +371 -0
- package/src/builder/i18n/es.ts +371 -0
- package/src/builder/i18n/fr.ts +371 -0
- package/src/builder/i18n/nl.ts +371 -0
- package/src/builder/i18n/pt.ts +371 -0
- package/src/builder/i18n.ts +10 -0
- package/src/builder/object-browser.ts +71 -67
- package/src/builder/table-editor.ts +51 -48
- package/src/builder/ui.ts +3 -2
- package/src/builder/worksheet.ts +160 -154
package/SECURITY.md
CHANGED
|
@@ -111,6 +111,7 @@ first version (MVP). Every finding below has a regression test in
|
|
|
111
111
|
| App Gallery (sprint 120) | Installing a sample app runs shipped SQL as the owner, so only administrators may, with the CSRF token, into their current workspace; a request names only a key of `gallery.json` of its own kind (sample, starter, utility), never a path, and the file names in the catalogue must match fixed patterns. The script and the workspace placement run in one transaction: a failing script leaves nothing behind. Utility apps are blueprints: any developer installs one into their own workspace, only after the review. An installed sample is edited only from its own workspace. The search term is escaped. |
|
|
112
112
|
| Builder language (sprint 121) | Only the codes of the builder's languages are accepted: anything else on a sign-in link is ignored (also a repeated parameter), the status bar's menu refuses it (400) and checks the CSRF token. A sign-in link sets only an HttpOnly, SameSite=Lax cookie that the sign-in page reads; a developer's stored choice changes only through their own signed-in session. Translated texts are escaped like the English ones; placeholders are filled with escaped values unless the code passes markup on purpose (`tx.rich`). |
|
|
113
113
|
| Ask and smart filters set facets (sprint 122) | A phrase can only set facets of the faceted search and smart filters regions on the page that the user sees, on their configured columns; values and range bounds become the facets' own URL parameters (bounds checked as numbers or dates) and reach SQL as query parameters, never as text. Values in a phrase are looked up in the report's rows with a query parameter, as the application's role. A facet chip's Remove is checked against the chips the form showed (a tampered key is refused). Sample apps keep their demo accounts with known passwords: install them on development or demo instances only. |
|
|
114
|
+
| Builder texts in the browser (sprint 123) | The builder's JavaScript gets its texts as JSON in an attribute (`<div id="builder-texts" data-texts>`), escaped like any attribute value, so builder pages still have no inline script. The texts are the fixed message files, never data; scripts put them into the page with `textContent` and attributes, not as HTML. |
|
|
114
115
|
|
|
115
116
|
## Findings of the MVP review and their fixes
|
|
116
117
|
|
|
@@ -249,7 +249,7 @@ row above where there is one.
|
|
|
249
249
|
| WS-4 AI Agents with tools | ✅ | (sprint 111) AI Agents and AI Tools as shared components, a Navigate tool, Gemini and OpenAI-compatible services ([Workflow, automation and AI](#workflow-automation-and-ai)) |
|
|
250
250
|
| WS-5 AI interactive reports: chips, computed columns | ✅ | (sprint 112) *Ask in your own words* with chips, Undo and computed columns; a phrase parser that needs no AI service; (sprint 122) phrases set faceted search and smart filter facets |
|
|
251
251
|
| WS-6 Workflow 26.2: outcomes, migrating running instances, drafts, diagram export | ✅ | (sprint 114) Custom task outcomes, task events, Send E-Mail and draft steps, migrating running instances, the diagram as SVG / PNG ([Workflow, automation and AI](#workflow-automation-and-ai)) |
|
|
252
|
-
| WS-7 The builder in more languages | ✅ | (sprint 121) The builder in Deutsch, English, Español, Français, Nederlands and Português: links on the sign-in page, a language menu in the status bar, the choice stored per developer (migration 127). Translated: the builder's frame, sign-in page, Workspace home, App Builder home, an application's home and tabs, the Page Designer's toolbar, menus, tree and gallery, the SQL Workshop's home and tabs; other screens move over incrementally (English until then). Builder texts are keyed message files with a coverage check in CI |
|
|
252
|
+
| WS-7 The builder in more languages | ✅ | (sprint 121) The builder in Deutsch, English, Español, Français, Nederlands and Português: links on the sign-in page, a language menu in the status bar, the choice stored per developer (migration 127). Translated: the builder's frame, sign-in page, Workspace home, App Builder home, an application's home and tabs, the Page Designer's toolbar, menus, tree and gallery, the SQL Workshop's home and tabs; other screens move over incrementally (English until then). Builder texts are keyed message files with a coverage check in CI (sprint 123) Also the SQL Worksheet, the Object Browser and Table Properties, with the texts of the browser code. |
|
|
253
253
|
| WS-8 App Gallery: more sample apps, Utility Apps | ✅ | (sprint 120) Four tabs with counts and a search that also works without JavaScript ([chapter 3](guide/03-builder.md#app-gallery)). *Sample Apps*: HR Demo, Tasks, ten single-topic apps (Sample Charts, Sample Cards, Sample Calendar, Sample Interactive Grids, Sample Dynamic Actions, Sample Reporting; sprint 122: Sample Maps, Sample Approvals, Sample Data Loading, Sample Files) and the **UX Pattern Catalog**, each with its own schema, data, demo account and explanatory texts, installed by administrators from one SQL file in one transaction; *Utility Apps*: Lookup Lists, Release Notes and Announcements as blueprints any developer installs into the workspace. APEX's 27 sample apps include topics pgkiln shows in HR Demo instead (REST, trees, collections); no remote catalogue |
|
|
254
254
|
| WS-9 Mail queue reporting, workspace SMTP, workspace network ACLs | ✅ | (sprint 118) Administration → Mail queue (per hour and per workspace / application, the slowest provider calls, failures with the provider's answer, Retry and Purge), a workspace's own mail service, and a workspace's network access list that narrows the installation's allow-list for every outgoing call ([Administration](#administration)) |
|
|
255
255
|
| WS-10 Universal Theme 26.2: Pillar appearances, navigation polish | ✅ | (sprint 117) Theme Roller → Navigation appearance Pillar Light / Pillar Dark per application or style, with a header strip colour for light and dark mode; the current page filled, its parents bold with an accent guide line; `u-color-*-subtle` utilities; a lighter processing indicator; RTL mirrored |
|
package/docs/guide/03-builder.md
CHANGED
|
@@ -274,8 +274,9 @@ device; other developers keep their own. It only changes the builder: your appli
|
|
|
274
274
|
|
|
275
275
|
What is translated: the builder's frame (the rail, the top bar and its search, your account menu, the status bar,
|
|
276
276
|
standard messages and error pages), the sign-in page, the Workspace home, the App Builder home, an application's
|
|
277
|
-
home page and its tabs, the Page Designer's toolbar, menus, component tree, panes and gallery,
|
|
278
|
-
home page and tabs
|
|
277
|
+
home page and its tabs, the Page Designer's toolbar, menus, component tree, panes and gallery, the SQL Workshop's
|
|
278
|
+
home page and tabs, and (sprint 123) the **SQL Worksheet** (with its messages in the browser), the **Object Browser**
|
|
279
|
+
and **Table Properties**. Other builder pages are still in English (they move over screen by screen); so are the
|
|
279
280
|
Page Designer's property names and their help, the links to this guide, and any SQL the builder generates.
|
|
280
281
|
|
|
281
282
|
## Workspace home
|
|
@@ -589,6 +589,20 @@ translated label (German nouns). Flash messages are translated where they are se
|
|
|
589
589
|
display position, button position, component kind and SQL Workshop tool has its run-time key; and that every
|
|
590
590
|
language file has exactly the English keys, the same `{placeholders}`, no empty texts and is mostly translated.
|
|
591
591
|
|
|
592
|
+
**Texts in the browser** (sprint 123). The builder's own JavaScript (`public/*.js`) asks for texts with keys
|
|
593
|
+
`js.<screen>.<what>`: `shell()` puts every `js.*` key in the developer's language into the page as JSON in an
|
|
594
|
+
attribute, `<div id="builder-texts" hidden data-texts="…">` (escaped like any value; builder pages keep having no
|
|
595
|
+
inline script). A script reads it once and fills placeholders itself:
|
|
596
|
+
|
|
597
|
+
```js
|
|
598
|
+
const TEXTS = (() => { try { return JSON.parse(document.getElementById('builder-texts')?.dataset.texts || '{}'); } catch { return {}; } })();
|
|
599
|
+
const msg = (key, params = {}) => (TEXTS[key] ?? key).replace(/\{(\w+)\}/g, (m, k) => (k in params ? String(params[k]) : m));
|
|
600
|
+
announce(msg('js.ws.copied', { what }));
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
The coverage check also scans `public/*.js` for `msg('js.…')` and fails when a key has no English text. Text
|
|
604
|
+
from the server (a JSON answer's `message`) is translated on the server.
|
|
605
|
+
|
|
592
606
|
Stays English by design: the property editor's field names and help, links to the guide, generated SQL, and pages
|
|
593
607
|
not moved yet. The builder's language is separate from an application's languages (`src/i18n.ts`).
|
|
594
608
|
|
package/package.json
CHANGED
package/public/builder.css
CHANGED
|
@@ -1216,7 +1216,7 @@ details.prop-group-more[open] > summary { margin-bottom: 8px; }
|
|
|
1216
1216
|
.ob-folder > summary .kind { margin-left: auto; }
|
|
1217
1217
|
.ob-folder .tree { padding-left: 0.75rem; }
|
|
1218
1218
|
.ob-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 0.5rem; margin-bottom: 0.75rem; }
|
|
1219
|
-
.ob-title { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.5rem; overflow-wrap: anywhere; }
|
|
1219
|
+
.ob-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0 0 0.5rem; overflow-wrap: anywhere; }
|
|
1220
1220
|
.ob-actions { display: grid; gap: 0.6rem; margin-top: 1rem; }
|
|
1221
1221
|
.ob-action { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
1222
1222
|
.ob-action input:not([type=checkbox]) { width: 10rem; }
|
package/public/worksheet.js
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
'use strict';
|
|
22
22
|
const form = document.getElementById('ws');
|
|
23
23
|
if (!form) return;
|
|
24
|
+
// (sprint 123) texts in the builder's language: #builder-texts (shell(), keys js.*), English in en.ts
|
|
25
|
+
const TEXTS = (() => { try { return JSON.parse(document.getElementById('builder-texts')?.dataset.texts || '{}'); } catch { return {}; } })();
|
|
26
|
+
const msg = (key, params = {}) => (TEXTS[key] ?? key).replace(/\{(\w+)\}/g, (m, k) => (k in params ? String(params[k]) : m));
|
|
24
27
|
const ta = form.querySelector('textarea[name=sql]');
|
|
25
28
|
const out = form.querySelector('.ws-output');
|
|
26
29
|
const store = {
|
|
@@ -149,9 +152,9 @@
|
|
|
149
152
|
const copy = async (text, what) => {
|
|
150
153
|
try {
|
|
151
154
|
await navigator.clipboard.writeText(text);
|
|
152
|
-
announce(
|
|
155
|
+
announce(msg('js.ws.copied', { what }));
|
|
153
156
|
} catch {
|
|
154
|
-
announce('
|
|
157
|
+
announce(msg('js.ws.no_copy'));
|
|
155
158
|
}
|
|
156
159
|
};
|
|
157
160
|
const announce = (msg) => {
|
|
@@ -165,7 +168,7 @@
|
|
|
165
168
|
if (e.target.closest('[data-ws-copy]')) {
|
|
166
169
|
const head = [...grid.tHead.rows[0].cells].slice(1).map((th) => th.textContent.trim());
|
|
167
170
|
const rows = [...grid.tBodies[0].rows].map((tr) => [...tr.cells].slice(1).map((td) => cellValue(td).replace(/[\t\n]/g, ' ')));
|
|
168
|
-
copy([head, ...rows].map((r) => r.join('\t')).join('\n'),
|
|
171
|
+
copy([head, ...rows].map((r) => r.join('\t')).join('\n'), msg('js.ws.rows', { n: rows.length }));
|
|
169
172
|
}
|
|
170
173
|
const sort = e.target.closest('[data-ws-sort]');
|
|
171
174
|
if (sort) {
|
|
@@ -218,7 +221,7 @@
|
|
|
218
221
|
run('statement');
|
|
219
222
|
}
|
|
220
223
|
const c = e.target.closest('[data-ws-copy-text]');
|
|
221
|
-
if (c) copy(c.dataset.wsCopyText, '
|
|
224
|
+
if (c) copy(c.dataset.wsCopyText, msg('js.ws.the_statement'));
|
|
222
225
|
});
|
|
223
226
|
|
|
224
227
|
// ------------------------------------------------------------ (sprint 93) worksheets as tabs, kept in this browser
|
|
@@ -226,7 +229,7 @@
|
|
|
226
229
|
const readTabs = () => { try { const v = JSON.parse(store.get('tabs') || 'null'); return v && Array.isArray(v.tabs) ? v : null; } catch { return null; } };
|
|
227
230
|
const book = readTabs() || { active: '', tabs: [] };
|
|
228
231
|
const newKey = () => Math.random().toString(36).slice(2, 10);
|
|
229
|
-
const untitled = () => { let n = 1; while (book.tabs.some((
|
|
232
|
+
const untitled = () => { let n = 1; while (book.tabs.some((x) => x.name === msg('js.ws.untitled', { n }))) n++; return msg('js.ws.untitled', { n }); };
|
|
230
233
|
const src = form.dataset.source;
|
|
231
234
|
const sid = form.dataset.scriptId ? Number(form.dataset.scriptId) : null;
|
|
232
235
|
const sname = form.dataset.scriptName;
|
|
@@ -273,16 +276,16 @@
|
|
|
273
276
|
if (t === cur) b.setAttribute('aria-current', 'true');
|
|
274
277
|
b.dataset.wsWtab = t.key;
|
|
275
278
|
b.textContent = `${t.name}${dirty(t) ? '*' : ''}`;
|
|
276
|
-
b.title = t.scriptId ?
|
|
279
|
+
b.title = t.scriptId ? `${msg('js.ws.script', { name: t.name })}${dirty(t) ? ` ${msg('js.ws.unsaved')}` : ''}` : msg('js.ws.not_saved');
|
|
277
280
|
const x = document.createElement('button');
|
|
278
281
|
x.type = 'button';
|
|
279
282
|
x.className = 'ws-wtab-close';
|
|
280
283
|
x.dataset.wsWclose = t.key;
|
|
281
|
-
x.setAttribute('aria-label',
|
|
284
|
+
x.setAttribute('aria-label', msg('js.ws.close_tab', { name: t.name }));
|
|
282
285
|
x.textContent = '×';
|
|
283
286
|
wrap.append(b, x);
|
|
284
287
|
return wrap;
|
|
285
|
-
}), Object.assign(document.createElement('button'), { type: 'button', className: 'ws-wtab-new', title: '
|
|
288
|
+
}), Object.assign(document.createElement('button'), { type: 'button', className: 'ws-wtab-new', title: msg('js.ws.new_tab'), textContent: '+' }));
|
|
286
289
|
persist();
|
|
287
290
|
};
|
|
288
291
|
const switchTo = (t) => {
|
|
@@ -297,8 +300,8 @@
|
|
|
297
300
|
const newTab = () => switchTo(add({ key: newKey(), name: untitled(), scriptId: null, text: '', saved: '' }));
|
|
298
301
|
const closeTab = (t) => {
|
|
299
302
|
const txnOpen = t === cur ? txnBox.dataset.state !== 'none' : !!t.txnOpen;
|
|
300
|
-
if (txnOpen && !window.confirm(
|
|
301
|
-
if (!txnOpen && dirty(t) && t.text.trim() && !window.confirm(
|
|
303
|
+
if (txnOpen && !window.confirm(msg('js.ws.close_txn', { name: t.name }))) return;
|
|
304
|
+
if (!txnOpen && dirty(t) && t.text.trim() && !window.confirm(msg('js.ws.close_unsaved', { name: t.name }))) return;
|
|
302
305
|
if (t.txn) txnPost(t.key, 'rollback'); // the server knows best whether one is open
|
|
303
306
|
const i = book.tabs.indexOf(t);
|
|
304
307
|
book.tabs.splice(i, 1);
|
|
@@ -360,15 +363,15 @@
|
|
|
360
363
|
const rollbackBtn = txnBox.querySelector('[data-ws-txn-rollback]');
|
|
361
364
|
const commitNote = form.querySelector('[data-ws-commit-note]');
|
|
362
365
|
const idleSeconds = Number(txnBox.dataset.idle) || 300;
|
|
363
|
-
const idleWords = idleSeconds % 60 ?
|
|
366
|
+
const idleWords = idleSeconds % 60 ? msg('js.ws.idle_seconds', { n: idleSeconds }) : msg(idleSeconds === 60 ? 'js.ws.idle_minute' : 'js.ws.idle_minutes', { n: idleSeconds / 60 });
|
|
364
367
|
let txnInfo = { state: txnBox.dataset.state, statements: Number(txnBox.dataset.statements) || 0, startedMs: Number(txnBox.dataset.started) || null,
|
|
365
368
|
lastMs: Number(txnBox.dataset.last) || null, note: txnBox.dataset.note || null };
|
|
366
369
|
let clockSkew = Date.now() - (Number(txnBox.dataset.now) || Date.now());
|
|
367
370
|
let txnTimer = 0;
|
|
368
371
|
const badgeText = (i) => {
|
|
369
|
-
if (i.state === 'none') return i.note || '
|
|
372
|
+
if (i.state === 'none') return i.note || msg('js.ws.txn_none');
|
|
370
373
|
const min = i.startedMs ? Math.floor((Date.now() - clockSkew - i.startedMs) / 60000) : 0;
|
|
371
|
-
return `${i.state === 'aborted' ? '
|
|
374
|
+
return `${msg(i.state === 'aborted' ? 'js.ws.txn_failed' : 'js.ws.txn_open')} · ${msg(i.statements === 1 ? 'js.ws.stmt_one' : 'js.ws.stmt', { n: i.statements })} · ${min < 1 ? msg('js.ws.under_minute') : msg('js.ws.min', { n: min })}`;
|
|
372
375
|
};
|
|
373
376
|
const txnApply = (i) => {
|
|
374
377
|
txnInfo = i;
|
|
@@ -381,8 +384,8 @@
|
|
|
381
384
|
commitBtn.disabled = i.state !== 'open';
|
|
382
385
|
rollbackBtn.disabled = i.state === 'none';
|
|
383
386
|
commitNote.textContent = on
|
|
384
|
-
?
|
|
385
|
-
: '
|
|
387
|
+
? msg('js.ws.commit_txn', { idle: idleWords })
|
|
388
|
+
: msg('js.ws.commit_auto');
|
|
386
389
|
cur.txn = on;
|
|
387
390
|
cur.txnOpen = i.state !== 'none';
|
|
388
391
|
persist();
|
|
@@ -407,7 +410,7 @@
|
|
|
407
410
|
if (j.state !== 'none') modeSel.value = 'on';
|
|
408
411
|
fromServer(j);
|
|
409
412
|
} catch {
|
|
410
|
-
badge.textContent = '
|
|
413
|
+
badge.textContent = msg('js.ws.txn_unknown');
|
|
411
414
|
}
|
|
412
415
|
}
|
|
413
416
|
async function txnPost(key, action) {
|
|
@@ -421,13 +424,13 @@
|
|
|
421
424
|
}
|
|
422
425
|
return j;
|
|
423
426
|
} catch {
|
|
424
|
-
announce('
|
|
427
|
+
announce(msg('js.ws.txn_not_ended'));
|
|
425
428
|
return null;
|
|
426
429
|
}
|
|
427
430
|
}
|
|
428
431
|
modeSel.addEventListener('change', async () => {
|
|
429
432
|
if (modeSel.value !== 'on' && txnInfo.state !== 'none') {
|
|
430
|
-
if (!window.confirm('
|
|
433
|
+
if (!window.confirm(msg('js.ws.txn_confirm'))) { modeSel.value = 'on'; return; }
|
|
431
434
|
await txnPost(cur.key, 'rollback');
|
|
432
435
|
modeSel.value = '';
|
|
433
436
|
}
|
|
@@ -486,9 +489,9 @@
|
|
|
486
489
|
const out = (await res.json()).sql;
|
|
487
490
|
if (whole) replaceRange(0, ta.value.length, out, false);
|
|
488
491
|
else replaceRange(a, b, out);
|
|
489
|
-
announce('
|
|
492
|
+
announce(msg('js.ws.formatted'));
|
|
490
493
|
} catch {
|
|
491
|
-
announce('
|
|
494
|
+
announce(msg('js.ws.format_failed'));
|
|
492
495
|
}
|
|
493
496
|
};
|
|
494
497
|
const convertCase = () => {
|
|
@@ -521,7 +524,7 @@
|
|
|
521
524
|
try {
|
|
522
525
|
return new RegExp(fre.checked ? fq.value : fq.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), `g${fcs.checked ? '' : 'i'}`);
|
|
523
526
|
} catch (err) {
|
|
524
|
-
fmsg.textContent =
|
|
527
|
+
fmsg.textContent = msg('js.ws.bad_pattern', { error: err.message });
|
|
525
528
|
return null;
|
|
526
529
|
}
|
|
527
530
|
};
|
|
@@ -537,7 +540,7 @@
|
|
|
537
540
|
re.lastIndex = from;
|
|
538
541
|
let m = re.exec(ta.value);
|
|
539
542
|
if (!m) { re.lastIndex = 0; m = re.exec(ta.value); }
|
|
540
|
-
if (!m || !m[0]) { fmsg.textContent = '
|
|
543
|
+
if (!m || !m[0]) { fmsg.textContent = msg('js.ws.not_found'); return false; }
|
|
541
544
|
fmsg.textContent = '';
|
|
542
545
|
reveal(m.index, m.index + m[0].length);
|
|
543
546
|
return true;
|
|
@@ -580,7 +583,7 @@
|
|
|
580
583
|
if (!re) return;
|
|
581
584
|
const n = (ta.value.match(re) || []).length;
|
|
582
585
|
if (n) replaceRange(0, ta.value.length, ta.value.replace(re, fr.value), false);
|
|
583
|
-
fmsg.textContent =
|
|
586
|
+
fmsg.textContent = msg('js.ws.replaced', { n });
|
|
584
587
|
}
|
|
585
588
|
});
|
|
586
589
|
|
|
@@ -660,6 +663,8 @@
|
|
|
660
663
|
// to open it; Open shows the Object Browser's detail in a panel at the side (its tabs and pages load in the panel).
|
|
661
664
|
(() => {
|
|
662
665
|
'use strict';
|
|
666
|
+
const TEXTS = (() => { try { return JSON.parse(document.getElementById('builder-texts')?.dataset.texts || '{}'); } catch { return {}; } })();
|
|
667
|
+
const msg = (key) => TEXTS[key] ?? key;
|
|
663
668
|
document.addEventListener('contextmenu', (e) => {
|
|
664
669
|
const item = e.target.closest?.('.ws-obj, .ob-item');
|
|
665
670
|
const menu = item?.querySelector(':scope > .obj-menu');
|
|
@@ -678,8 +683,8 @@
|
|
|
678
683
|
const head = document.createElement('div');
|
|
679
684
|
head.className = 'obj-slide-head';
|
|
680
685
|
const title = Object.assign(document.createElement('strong'), { id: 'obj-slide-title' });
|
|
681
|
-
const full = Object.assign(document.createElement('a'), { className: 'btn btn-sm', textContent: '
|
|
682
|
-
const close = Object.assign(document.createElement('button'), { type: 'button', className: 'btn btn-sm', textContent: '
|
|
686
|
+
const full = Object.assign(document.createElement('a'), { className: 'btn btn-sm', textContent: msg('js.ws.open_ob') });
|
|
687
|
+
const close = Object.assign(document.createElement('button'), { type: 'button', className: 'btn btn-sm', textContent: msg('js.ws.close') });
|
|
683
688
|
close.addEventListener('click', () => slide.close());
|
|
684
689
|
const tools = document.createElement('div');
|
|
685
690
|
tools.append(full, ' ', close);
|
|
@@ -708,7 +713,7 @@
|
|
|
708
713
|
d.querySelector('#obj-slide-title').textContent = root?.dataset.label ?? '';
|
|
709
714
|
d.querySelector('.obj-slide-head a').href = url.replace('/sql/objects/panel', '/sql/objects');
|
|
710
715
|
} catch {
|
|
711
|
-
body.textContent = '
|
|
716
|
+
body.textContent = msg('js.ws.open_failed');
|
|
712
717
|
} finally {
|
|
713
718
|
body.removeAttribute('aria-busy');
|
|
714
719
|
}
|