pgkiln 0.87.0 → 0.89.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/156_pwa_prefetch.sql +13 -0
- package/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +53 -25
- package/docs/guide/09-reference.md +1 -1
- package/docs/guide/17-mobile.md +35 -4
- package/examples/hr/hr_82_pwa_prefetch.sql +11 -0
- package/package.json +1 -1
- package/public/app.js +11 -0
- package/public/builder.css +57 -3
- package/public/builder.js +66 -0
- package/public/palette.js +8 -1
- package/public/sw.js +51 -1
- package/public/worksheet.css +39 -4
- package/public/worksheet.js +15 -1
- package/src/builder/designer.ts +2 -2
- package/src/builder/gallery.ts +5 -5
- package/src/builder/home.ts +71 -60
- package/src/builder/i18n/de.ts +28 -0
- package/src/builder/i18n/en.ts +28 -0
- package/src/builder/i18n/es.ts +28 -0
- package/src/builder/i18n/fr.ts +28 -0
- package/src/builder/i18n/it.ts +28 -0
- package/src/builder/i18n/ja.ts +28 -0
- package/src/builder/i18n/ko.ts +28 -0
- package/src/builder/i18n/nl.ts +28 -0
- package/src/builder/i18n/pt.ts +28 -0
- package/src/builder/i18n/zh-cn.ts +28 -0
- package/src/builder/i18n/zh-tw.ts +28 -0
- package/src/builder/pwa.ts +14 -3
- package/src/builder/routes.ts +21 -10
- package/src/builder/sql.ts +2 -2
- package/src/builder/ui.ts +47 -33
- package/src/builder/worksheet.ts +33 -24
- package/src/builder/workspace-home.ts +17 -0
- package/src/metadata.ts +3 -1
- package/src/runtime/pwa.ts +8 -3
- package/src/runtime/routes.ts +25 -6
- package/src/session.ts +2 -0
package/public/sw.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// (offline, the cached copy of another version)
|
|
5
5
|
// pages (navigations): network first; offline, the cached copy (only when the app keeps pages) or the offline page
|
|
6
6
|
// signing in or out empties the page cache, so the next person on the device doesn't see them
|
|
7
|
+
// (168) with prefetchHours: the pages app.js hands over (the navigation's) are fetched one by one and kept, again
|
|
8
|
+
// when the copies are older than prefetchHours (a stamp in the page cache, so a sign-in starts over)
|
|
7
9
|
// push notifications (migration 074): shown as they arrive, a click opens their page of the app;
|
|
8
10
|
// signing out turns them off on this device
|
|
9
11
|
/* global PGKILN */
|
|
@@ -77,10 +79,58 @@ self.addEventListener('message', (event) => {
|
|
|
77
79
|
caches
|
|
78
80
|
.open(PAGES)
|
|
79
81
|
.then((c) => c.keys())
|
|
80
|
-
.then((keys) => event.source.postMessage({ type: 'pgkiln:pages', pages: keys.map((k) => k.url) }));
|
|
82
|
+
.then((keys) => event.source.postMessage({ type: 'pgkiln:pages', pages: keys.map((k) => k.url).filter((u) => new URL(u).pathname !== STAMP) }));
|
|
81
83
|
}
|
|
84
|
+
if (event.data && event.data.type === 'pgkiln:prefetch' && Array.isArray(event.data.urls)) event.waitUntil(prefetchPages(event.data.urls));
|
|
82
85
|
});
|
|
83
86
|
|
|
87
|
+
// ------------------------------------------------------------------ every page offline (168, PGKILN.prefetchHours)
|
|
88
|
+
// The server answers a request with X-Pgkiln-Prefetch read-only, keeps no session state, and refuses (409) pages
|
|
89
|
+
// that run processes when they open; only 200 pages are kept. One page at a time, a short pause between them;
|
|
90
|
+
// the stamp is written when the list is done, so a list cut short by the network is tried again next time.
|
|
91
|
+
const STAMP = `${PGKILN.base}/__prefetched`;
|
|
92
|
+
const MAX_PREFETCH = 60;
|
|
93
|
+
let prefetching = false;
|
|
94
|
+
async function prefetchPages(urls) {
|
|
95
|
+
if (!PGKILN.offlinePages || !PGKILN.prefetchHours || prefetching) return;
|
|
96
|
+
prefetching = true;
|
|
97
|
+
try {
|
|
98
|
+
const cache = await caches.open(PAGES);
|
|
99
|
+
const stamp = await cache.match(STAMP);
|
|
100
|
+
if (stamp && Date.now() - Number(await stamp.text()) < PGKILN.prefetchHours * 3600 * 1000) return;
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
let kept = 0;
|
|
103
|
+
for (const u of urls) {
|
|
104
|
+
if (seen.size >= MAX_PREFETCH) break;
|
|
105
|
+
let url;
|
|
106
|
+
try {
|
|
107
|
+
url = new URL(u, self.location.origin);
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
url.hash = '';
|
|
112
|
+
if (url.origin !== self.location.origin || !url.pathname.startsWith(`${PGKILN.base}/`) || signInOrOut(url) || seen.has(url.href)) continue;
|
|
113
|
+
seen.add(url.href);
|
|
114
|
+
let res;
|
|
115
|
+
try {
|
|
116
|
+
res = await fetch(url.href, { credentials: 'same-origin', redirect: 'manual', headers: { 'X-Pgkiln-Prefetch': '1' } });
|
|
117
|
+
} catch {
|
|
118
|
+
return; // offline: again on the next page
|
|
119
|
+
}
|
|
120
|
+
if (res.status === 200 && (res.headers.get('content-type') || '').startsWith('text/html')) {
|
|
121
|
+
await cache.put(new Request(url.href), res);
|
|
122
|
+
kept++;
|
|
123
|
+
}
|
|
124
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
125
|
+
}
|
|
126
|
+
if (!seen.size) return;
|
|
127
|
+
await cache.put(STAMP, new Response(String(Date.now())));
|
|
128
|
+
for (const c of await self.clients.matchAll({ type: 'window' })) c.postMessage({ type: 'pgkiln:prefetched', kept });
|
|
129
|
+
} finally {
|
|
130
|
+
prefetching = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
84
134
|
// ------------------------------------------------------------------ offline form queue (PGKILN.offlineSubmit)
|
|
85
135
|
// A form posted while the network is down is kept on the device (IndexedDB, files included) and the
|
|
86
136
|
// browser shows the page again with ?queued=1. It is sent later (on "online", when a page of the app
|
package/public/worksheet.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
screens: navigator | editor over output tabs, filling the window, with a status bar at the bottom. */
|
|
4
4
|
.ws-page { display: flex; flex-direction: column; padding: 8px 10px 0; background: var(--bg); }
|
|
5
5
|
.ws-page > .sw-tabs { margin-bottom: 6px; }
|
|
6
|
-
.ws { --ws-nav: 280px; --ws-out:
|
|
6
|
+
.ws { --ws-nav: 280px; --ws-out: 45%; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; background: var(--surface);
|
|
7
7
|
border: 1px solid var(--border); border-radius: var(--radius); min-width: 0; }
|
|
8
8
|
.ws-default { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); border: 0; padding: 0; }
|
|
9
9
|
.ws-nav { display: flex; flex-direction: column; min-width: 0; border-bottom: 1px solid var(--border); background: var(--surface-2); }
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
.ws-obj-actions { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 10px 8px 34px; }
|
|
33
33
|
.ws-obj-actions .btn { font-size: 11px; padding: 1px 6px; }
|
|
34
34
|
|
|
35
|
-
.ws-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
35
|
+
.ws-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; container-type: inline-size; }
|
|
36
36
|
.ws-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 6px 8px; border-bottom: 1px solid var(--border); }
|
|
37
37
|
.ws-toolbar .btn { display: inline-flex; align-items: center; gap: 4px; }
|
|
38
38
|
.ws-toolbar .icon { width: 15px; height: 15px; }
|
|
@@ -99,12 +99,16 @@
|
|
|
99
99
|
.js .ide-full .ws-editor .ce { flex: 1 1 auto; min-height: 0; grid-template-rows: minmax(0, 1fr) auto; border: 0; border-radius: 0; }
|
|
100
100
|
.js .ide-full .ws-editor .ce-main { min-height: 0; }
|
|
101
101
|
.js .ide-full .ws-editor .ce textarea.ce-input { height: 100%; min-height: 0; resize: none; }
|
|
102
|
-
.js .ide-full .ws-output { flex: 0
|
|
102
|
+
.js .ide-full .ws-output { flex: 0 1 var(--ws-out); }
|
|
103
|
+
/* (UX-1) the editor keeps at least ten lines; an empty output is only its tab strip until a run */
|
|
104
|
+
.js .ide-full .ws-editor { min-height: 13.5rem; }
|
|
105
|
+
.js .ide-full .ws-output[data-empty] { flex: 0 0 auto; }
|
|
106
|
+
.js .ide-full .ws-output[data-empty] .ws-pane { display: none; }
|
|
103
107
|
.js .ide-full .ws-pane { flex: 1 1 auto; }
|
|
104
108
|
.js .ide-full .ws-nav { overflow: hidden; }
|
|
105
109
|
}
|
|
106
110
|
@media (max-width: 899px) {
|
|
107
|
-
.ws-toolbar .btn span:not(.sr-only) { display: none; }
|
|
111
|
+
.ws-toolbar .btn span:not(.sr-only):not(.ws-dots) { display: none; }
|
|
108
112
|
.ws-toolbar .btn-hot span:not(.sr-only) { display: inline; }
|
|
109
113
|
}
|
|
110
114
|
/* fullscreen: only the editor and its toolbar */
|
|
@@ -299,3 +303,34 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
|
|
|
299
303
|
.ws-txn-badge[data-state="open"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); background: color-mix(in srgb, var(--warn) 10%, transparent); font-weight: 600; }
|
|
300
304
|
.ws-txn-badge[data-state="aborted"] { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); font-weight: 600; }
|
|
301
305
|
.ws-txn-alert { margin: 6px 8px; }
|
|
306
|
+
|
|
307
|
+
/* (UX-1) one toolbar row: the run buttons, Suggest / Check, Commit mode, fullscreen and a ⋯ menu with the rest */
|
|
308
|
+
/* it fits on one row; an open transaction (badge, Commit, Rollback) may take a second one */
|
|
309
|
+
.ws-toolbar > * { flex: none; }
|
|
310
|
+
/* the commit mode's options name it (Autocommit / Transaction): no label beside the select */
|
|
311
|
+
.ws-toolbar .ws-txn .ws-runas > span { display: none; }
|
|
312
|
+
@container (max-width: 950px) {
|
|
313
|
+
.ws-toolbar .btn:not(.btn-hot) > span:not(.sr-only):not(.ws-dots) { display: none; }
|
|
314
|
+
}
|
|
315
|
+
@container (max-width: 640px) {
|
|
316
|
+
.ws-toolbar > * { flex: 0 1 auto; min-width: 0; }
|
|
317
|
+
.ws-toolbar .ws-txn { flex-wrap: wrap; }
|
|
318
|
+
}
|
|
319
|
+
.ws-more .menu-panel { min-width: 18rem; max-height: calc(100dvh - 190px); overflow: auto; }
|
|
320
|
+
/* with script the worksheet tabs name the file: the file menu is its icon */
|
|
321
|
+
.js .ws-toolbar .ws-file-name { display: none; }
|
|
322
|
+
.ws-more-tools { display: flex; flex-direction: column; gap: 2px; }
|
|
323
|
+
.ws-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border: 0; border-radius: 6px; background: none;
|
|
324
|
+
color: var(--text); font: inherit; text-align: left; text-decoration: none; cursor: pointer; }
|
|
325
|
+
.ws-menu-item:hover, .ws-menu-item:focus-visible { background: var(--surface-2); }
|
|
326
|
+
.ws-menu-item .icon { width: 15px; height: 15px; }
|
|
327
|
+
.ws-menu-item kbd { margin-left: auto; font-size: 11.5px; color: var(--muted); }
|
|
328
|
+
.ws-more-tools .ws-check { padding: 6px 8px; }
|
|
329
|
+
.ws-more-head { display: block; margin-bottom: 4px; }
|
|
330
|
+
/* the code editor's hint bar under the editor: only while it has something to say (Check's result) */
|
|
331
|
+
.ws-editor .ce-bar:not(:has(.ce-msg:not(:empty))) { display: none; }
|
|
332
|
+
.ws-editor .ce-bar :is(.ce-hint, .ce-suggest, .ce-check) { display: none; }
|
|
333
|
+
/* (UX-1) who the statements run as: a chip at the right of the status bar */
|
|
334
|
+
.ws-status-run { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1 1 auto; }
|
|
335
|
+
.ws-runas-chip { flex: none; display: inline-flex; align-items: center; gap: 6px; }
|
|
336
|
+
.ws-runas-chip select { padding: 1px 22px 1px 6px; min-height: 0; font-size: 12.5px; }
|
package/public/worksheet.js
CHANGED
|
@@ -104,11 +104,15 @@
|
|
|
104
104
|
out.dataset.active = key;
|
|
105
105
|
};
|
|
106
106
|
show(out.dataset.active || 'result');
|
|
107
|
+
// (UX-1) before a run the output is only its tab strip; choosing a tab opens it
|
|
107
108
|
out.querySelector('[role=tablist]').addEventListener('click', (e) => {
|
|
108
109
|
const t = e.target.closest('[role=tab]');
|
|
109
|
-
if (t)
|
|
110
|
+
if (!t) return;
|
|
111
|
+
delete out.dataset.empty;
|
|
112
|
+
show(t.dataset.wsTab);
|
|
110
113
|
});
|
|
111
114
|
out.querySelector('[role=tablist]').addEventListener('keydown', (e) => {
|
|
115
|
+
delete out.dataset.empty;
|
|
112
116
|
const k = tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true');
|
|
113
117
|
const next = { ArrowRight: k + 1, ArrowLeft: k - 1, Home: 0, End: tabs.length - 1 }[e.key];
|
|
114
118
|
if (next === undefined) return;
|
|
@@ -552,6 +556,16 @@
|
|
|
552
556
|
(k === 'h' ? fr : k === 'g' ? bar.querySelector('[data-ws-goto]') : fq).focus();
|
|
553
557
|
};
|
|
554
558
|
form.querySelector('[data-ws-find]').addEventListener('click', () => openFind('f'));
|
|
559
|
+
// (UX-1) Suggest and Check in the toolbar: the code editor's own buttons (its bar below the editor stays hidden
|
|
560
|
+
// until it has a message, such as Check's result)
|
|
561
|
+
const ceButton = (cls) => form.querySelector(`.ws-editor .${cls}`);
|
|
562
|
+
for (const [attr, cls] of [['data-ws-suggest', 'ce-suggest'], ['data-ws-check', 'ce-check']]) {
|
|
563
|
+
const btn = form.querySelector(`[${attr}]`);
|
|
564
|
+
btn?.addEventListener('click', () => ceButton(cls)?.click());
|
|
565
|
+
const sync = () => { if (btn) btn.hidden = !ceButton(cls) || ceButton(cls).hidden; };
|
|
566
|
+
sync();
|
|
567
|
+
window.addEventListener('load', sync);
|
|
568
|
+
}
|
|
555
569
|
form.querySelector('[data-ws-format]').addEventListener('click', format);
|
|
556
570
|
form.querySelector('[data-ws-case]').addEventListener('click', convertCase);
|
|
557
571
|
bar.addEventListener('keydown', (e) => {
|
package/src/builder/designer.ts
CHANGED
|
@@ -123,7 +123,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
123
123
|
// (sprint 90) a commented-out component: struck through in the tree, Uncomment in its menu
|
|
124
124
|
const commented = (r: any) => (r.build_option === 'COMMENTED_OUT' ? raw(' data-commented') : '');
|
|
125
125
|
const leaf = (k: string, r: any, ic: Raw | '', label: string, note = '') =>
|
|
126
|
-
html`<li><a class="pd-node" href="${url(`c=${k}-${r.id}`)}"${cur(`${k}-${r.id}`)} data-kind="${k}" data-id="${r.id}"${commented(r)}>${ic}<span class="pd-label">${label}</span>${badges(r)}${note ? html`<span class="pd-note">${note}</span>` : ''}</a></li>`;
|
|
126
|
+
html`<li><a class="pd-node" href="${url(`c=${k}-${r.id}`)}"${cur(`${k}-${r.id}`)} data-kind="${k}" data-id="${r.id}" title="${note ? `${label} (${note})` : label}"${commented(r)}>${ic}<span class="pd-label">${label}</span>${badges(r)}${note ? html`<span class="pd-note">${note}</span>` : ''}</a></li>`;
|
|
127
127
|
const folder = (label: string, children: Raw[] | Raw, open = true) =>
|
|
128
128
|
html`<li${open ? '' : raw(' data-collapsed')}><span class="pd-node pd-folder"><span class="pd-label">${label}</span></span><ul>${children}</ul></li>`;
|
|
129
129
|
const none = (text: string) => html`<li class="pd-empty">${text}</li>`;
|
|
@@ -136,7 +136,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
136
136
|
const bySlot = ['region', ...TOP_SLOTS, ...BOTTOM_SLOTS].map((sl) => [sl, buttons.filter((b) => (b.slot ?? 'region') === sl)] as const).filter(([, list]) => list.length);
|
|
137
137
|
const kids = [...(items.length ? [folder(tx('pd.tree.items'), items.map(itemLeaf))] : []),
|
|
138
138
|
...bySlot.map(([sl, list]) => folder(sl === 'region' ? tx('pd.tree.buttons') : tx('pd.tree.buttons_at', { slot: slotLabel(sl) }), list.map(buttonLeaf)))];
|
|
139
|
-
return html`<li><a class="pd-node" href="${url(`c=region-${r.id}`)}"${cur(`region-${r.id}`)} data-kind="region" data-id="${r.id}" data-position="${r.position ?? ''}" data-next="${next?.id ?? ''}"${commented(r)}>${bicon(regionIcon(r.type))}<span class="pd-label">${regionTitle(r)}</span>${badges(r)}<span class="pd-note">${r.type}</span></a>${kids.length ? html`<ul>${kids}</ul>` : ''}</li>`;
|
|
139
|
+
return html`<li><a class="pd-node" href="${url(`c=region-${r.id}`)}"${cur(`region-${r.id}`)} data-kind="region" data-id="${r.id}" data-position="${r.position ?? ''}" data-next="${next?.id ?? ''}" title="${regionTitle(r)} (${r.type})"${commented(r)}>${bicon(regionIcon(r.type))}<span class="pd-label">${regionTitle(r)}</span>${badges(r)}<span class="pd-note">${r.type}</span></a>${kids.length ? html`<ul>${kids}</ul>` : ''}</li>`;
|
|
140
140
|
};
|
|
141
141
|
// (sprint 77, APEX: the rendering tree by display position) the page template's positions, and others in use
|
|
142
142
|
const treeSide = p.page_template === 'left_side_column' ? 'left' : p.page_template === 'right_side_column' ? 'right' : null;
|
package/src/builder/gallery.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { migrate } from '../migrate.ts';
|
|
|
9
9
|
import { clientIp, logActivity } from '../session.ts';
|
|
10
10
|
import { openBlueprint } from './blueprints.ts';
|
|
11
11
|
import { isAdmin } from './locks.ts';
|
|
12
|
-
import { back, BASE, csrf, developer, flash, send, shell, type Req } from './ui.ts';
|
|
12
|
+
import { back, BASE, csrf, developer, flash, pageHeader, send, shell, type Req } from './ui.ts';
|
|
13
13
|
import { currentWorkspace, inWorkspaceSql, placeApp } from './workspaces.ts';
|
|
14
14
|
import { workspacesChanged } from '../workspace-db.ts';
|
|
15
15
|
import { scopedTx as tx, type BuilderKey } from './i18n.ts';
|
|
@@ -104,7 +104,7 @@ export async function galleryRoutes(app: FastifyInstance) {
|
|
|
104
104
|
const tabs = html`<nav class="gal-tabs segmented" aria-label="${tx('gal.kinds')}">${TABS.map(([t, l]) =>
|
|
105
105
|
html`<a href="${BASE}/gallery?tab=${t}${q ? `&q=${encodeURIComponent(q)}` : ''}"${tab === t ? raw(' aria-current="page"') : ''}>${tx(l)} <span class="gal-count">${counts[t]}</span></a>`)}</nav>`;
|
|
106
106
|
const blueprintCard = (kind: 'starters' | 'utilities') => (x: Starter) => card(x.icon, x.title, 'pgkiln', x.description,
|
|
107
|
-
html`<form method="post" action="${BASE}/gallery/${kind}/${x.key}">${csrf(s)}<button class="btn btn-
|
|
107
|
+
html`<form method="post" action="${BASE}/gallery/${kind}/${x.key}">${csrf(s)}<button class="btn btn-sm">${tx('gal.install')}</button></form>`);
|
|
108
108
|
const cards =
|
|
109
109
|
tab === 'samples'
|
|
110
110
|
? samples.filter(matches).map((x) => {
|
|
@@ -115,13 +115,13 @@ export async function galleryRoutes(app: FastifyInstance) {
|
|
|
115
115
|
? html`<a class="btn btn-sm" href="${BASE}/apps/${it.id}">${tx('gal.edit')}</a>`
|
|
116
116
|
: html`<span class="muted small">${tx('gal.other_workspace')}</span>`}`
|
|
117
117
|
: admin
|
|
118
|
-
? html`<form method="post" action="${BASE}/gallery/samples/${x.key}">${csrf(s)}<button class="btn btn-
|
|
118
|
+
? html`<form method="post" action="${BASE}/gallery/samples/${x.key}">${csrf(s)}<button class="btn btn-sm" data-confirm="${tx('gal.confirm_install', { title: x.title })}">${tx('gal.install')}</button></form>`
|
|
119
119
|
: html`<span class="muted small">${tx('gal.admin_installs')}</span>`);
|
|
120
120
|
})
|
|
121
121
|
: tab === 'starters' || tab === 'utilities'
|
|
122
122
|
? (tab === 'starters' ? starters : utilities).filter(matches).map(blueprintCard(tab))
|
|
123
123
|
: custom.filter(matches).map((x) => card('copy', x.name, tx('gal.this_workspace'), x.description ?? tx('gal.boilerplate_desc'),
|
|
124
|
-
html`<a class="btn btn-
|
|
124
|
+
html`<a class="btn btn-sm" href="${BASE}/create?boilerplate=${x.id}">${tx('gal.create_from')}</a><a class="btn btn-sm" href="${BASE}/apps/${x.id}">${tx('gal.edit')}</a>`));
|
|
125
125
|
const empty = q && counts[tab]
|
|
126
126
|
? html`<p class="muted">${tx.rich('gal.no_match', { q: html`<strong>${q}</strong>` })} <a href="${BASE}/gallery?tab=${tab}">${tx('gal.show_all')}</a></p>`
|
|
127
127
|
: tab === 'custom'
|
|
@@ -129,13 +129,13 @@ export async function galleryRoutes(app: FastifyInstance) {
|
|
|
129
129
|
: html`<p class="muted">${tx('gal.empty')}</p>`;
|
|
130
130
|
const intro = tx(({ samples: 'gal.intro.samples', starters: 'gal.intro.starters', utilities: 'gal.intro.utilities', custom: 'gal.intro.custom' } as const)[tab]);
|
|
131
131
|
const main = html`<div class="gal">
|
|
132
|
+
${pageHeader(tx('rail.gallery'))}
|
|
132
133
|
<div class="gal-bar">
|
|
133
134
|
<form class="ab-search gal-search" role="search" method="get" action="${BASE}/gallery"><label class="sr-only" for="gal-q">${tx('gal.search')}</label>${icon('search')}
|
|
134
135
|
<input type="hidden" name="tab" value="${tab}">
|
|
135
136
|
<input id="gal-q" name="q" type="search" placeholder="${tx('gal.search')}" autocomplete="off" value="${q}" data-filter-list=".gal [data-filter-row]"></form>
|
|
136
137
|
${tabs}
|
|
137
138
|
</div>
|
|
138
|
-
<h1 class="sr-only">${tx('rail.gallery')}</h1>
|
|
139
139
|
<p class="muted">${intro}</p>
|
|
140
140
|
${cards.length ? html`<div class="gal-grid">${cards}</div>` : empty}
|
|
141
141
|
</div>`;
|
package/src/builder/home.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
|
2
2
|
import { owner } from '../db.ts';
|
|
3
3
|
import { html, raw, type Raw } from '../html.ts';
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
|
-
import { APP_COLORS, BASE, csrf, developer, input, region, send, shell, type Req } from './ui.ts';
|
|
5
|
+
import { APP_COLORS, BASE, csrf, developer, input, pageHeader, region, send, shell, type Req } from './ui.ts';
|
|
6
6
|
import { crossAppLinks } from './cross-app.ts';
|
|
7
7
|
import type { Session } from '../session.ts';
|
|
8
8
|
import { isAdmin } from './locks.ts';
|
|
@@ -53,16 +53,6 @@ const lockedText = (tx: BuilderTranslate, by: string) => tx('ab.locked_by', { us
|
|
|
53
53
|
const appIcon = (a: { id: number; name: string }, cls = '') =>
|
|
54
54
|
html`<span class="app-icon app-color-${a.id % APP_COLORS}${cls ? ` ${cls}` : ''}" aria-hidden="true">${a.name.slice(0, 1).toUpperCase()}</span>`;
|
|
55
55
|
|
|
56
|
-
/** The four tiles at the top of the App Builder. */
|
|
57
|
-
const tiles = (tx: BuilderTranslate) => html`<nav class="ab-tiles" aria-label="${tx('rail.app_builder')}">
|
|
58
|
-
${([
|
|
59
|
-
['create', 'plus', tx('ab.tile.create'), tx('ab.tile.create_text')],
|
|
60
|
-
['import', 'upload', tx('ab.tile.import'), tx('ab.tile.import_text')],
|
|
61
|
-
['dashboard', 'activity', tx('ab.tile.dashboard'), tx('ab.tile.dashboard_text')],
|
|
62
|
-
['utilities', 'settings', tx('ab.tile.utilities'), tx('ab.tile.utilities_text')],
|
|
63
|
-
] as const).map(([path, ic, title, text]) => html`<a class="ab-tile" href="${BASE}/${path}"><span class="ab-tile-icon">${icon(ic)}</span><strong>${title}</strong><span>${text}</span></a>`)}
|
|
64
|
-
</nav>`;
|
|
65
|
-
|
|
66
56
|
/** The side column: About, Recent, Tasks. */
|
|
67
57
|
function aside(tx: BuilderTranslate, recent: { id: number; name: string }[], about: string, tasks: [string, string][]) {
|
|
68
58
|
return html`<aside class="ab-side" aria-label="${tx('ab.aside')}">
|
|
@@ -73,10 +63,9 @@ function aside(tx: BuilderTranslate, recent: { id: number; name: string }[], abo
|
|
|
73
63
|
</aside>`;
|
|
74
64
|
}
|
|
75
65
|
|
|
66
|
+
// (UX-5) what the header doesn't offer already (Create and Import are there)
|
|
76
67
|
const TASKS: [BuilderKey, string][] = [
|
|
77
|
-
['ab.task.create', `${BASE}/create`],
|
|
78
68
|
['ab.task.create_file', `${BASE}/create/file`],
|
|
79
|
-
['ab.task.import', `${BASE}/import`],
|
|
80
69
|
['ab.task.users', `${BASE}/users`],
|
|
81
70
|
['ab.task.providers', `${BASE}/users/providers`],
|
|
82
71
|
['rail.sql_workshop', `${BASE}/sql`],
|
|
@@ -174,9 +163,13 @@ export async function homeRoutes(app: FastifyInstance) {
|
|
|
174
163
|
html`<a class="tb-btn" href="${link({ view: v })}" title="${label}"${view === v ? raw(' aria-current="true"') : ''}>${icon(ic)}<span class="sr-only">${label}</span></a>`;
|
|
175
164
|
const main = html`<div class="ab-home">
|
|
176
165
|
<div class="ab-main">
|
|
177
|
-
|
|
166
|
+
${pageHeader(tx('rail.app_builder'), {
|
|
167
|
+
primary: [`${BASE}/create`, tx('ab.tile.create'), 'plus'],
|
|
168
|
+
secondary: [[`${BASE}/import`, tx('ab.tile.import'), 'upload']],
|
|
169
|
+
more: [[`${BASE}/dashboard`, tx('ab.tile.dashboard'), 'activity'], [`${BASE}/utilities`, tx('rail.admin'), 'settings']],
|
|
170
|
+
moreLabel: tx('ws.more'),
|
|
171
|
+
})}
|
|
178
172
|
${s.workspace?.message ? html`<div class="alert alert-info ab-message" role="status">${icon('bell')}<div><strong>${s.workspace.name}</strong><p>${s.workspace.message}</p></div></div>` : ''}
|
|
179
|
-
${tiles(tx)}
|
|
180
173
|
<div class="ab-toolbar">
|
|
181
174
|
<form class="ab-search" method="get" action="${BASE}/apps" role="search">
|
|
182
175
|
<label class="sr-only" for="ab-q">${tx('ab.search_label')}</label>${icon('search')}
|
|
@@ -190,13 +183,8 @@ export async function homeRoutes(app: FastifyInstance) {
|
|
|
190
183
|
<div class="menu-panel" role="menu">
|
|
191
184
|
<div class="menu-section"><span class="small muted">${tx('ab.sort_by')}</span>
|
|
192
185
|
${(Object.keys(SORTS) as Sort[]).map((k) => html`<a role="menuitem" href="${link({ sort: k })}"${sort === k ? raw(' aria-current="true"') : ''}>${tx(`ab.sort.${k}`)}</a>`)}</div>
|
|
193
|
-
<div class="menu-section menu-links">
|
|
194
|
-
<a role="menuitem" href="${BASE}/import">${icon('upload')} ${tx('ab.task.import')}</a>
|
|
195
|
-
<a role="menuitem" href="${BASE}/dashboard">${icon('activity')} ${tx('ab.ws_dashboard')}</a>
|
|
196
|
-
</div>
|
|
197
186
|
</div></details>
|
|
198
187
|
<span class="ab-count muted" aria-live="polite">${rows.length === total.n ? tx(total.n === 1 ? 'ab.count_one' : 'ab.count', { n: total.n }) : tx('ab.count_of', { n: rows.length, total: total.n })}</span>
|
|
199
|
-
<a class="btn btn-hot ab-create" href="${BASE}/create">${tx('ab.tile.create')}</a>
|
|
200
188
|
</div>
|
|
201
189
|
${list}
|
|
202
190
|
</div>
|
|
@@ -271,49 +259,72 @@ export async function homeRoutes(app: FastifyInstance) {
|
|
|
271
259
|
});
|
|
272
260
|
|
|
273
261
|
// ---------------------------------------------------------------- workspace utilities
|
|
262
|
+
// (UX-4) Administration: the workspace utilities in sections, with a filter; the rail's Administration entry (UX-3)
|
|
274
263
|
app.get(`${BASE}/utilities`, async (req: Req, reply) => {
|
|
275
264
|
const s = await developer(req, reply);
|
|
276
265
|
if (!s) return;
|
|
277
266
|
const tx = tr(s);
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
267
|
+
const admin = await isAdmin(s.username);
|
|
268
|
+
const wsAdmin = !!s.workspace?.admin || (!!s.workspace && (await workspaceAdmin(s.username, s.workspace.id)));
|
|
269
|
+
const ws = s.workspace;
|
|
270
|
+
type Tile = [href: string, ic: string, title: string, text: string | Raw] | null;
|
|
271
|
+
const tile = (t: Tile) =>
|
|
272
|
+
t ? html`<li><a class="adm-tile" href="${t[0]}"><span class="adm-tile-head">${icon(t[1])}<strong>${t[2]}</strong></span>${t[3] ? html`<span class="adm-tile-text">${t[3]}</span>` : ''}</a></li>` : '';
|
|
273
|
+
const section = (id: string, title: string, tiles: Tile[]) => {
|
|
274
|
+
const shown = tiles.filter(Boolean);
|
|
275
|
+
return shown.length ? html`<section class="adm-section" aria-labelledby="adm-${id}"><h2 id="adm-${id}">${title}</h2><ul class="adm-tiles">${shown.map(tile)}</ul></section>` : '';
|
|
276
|
+
};
|
|
277
|
+
const main = html`<div class="adm">
|
|
278
|
+
<div class="title-row"><h1>${tx('rail.admin')}</h1>
|
|
279
|
+
<div class="adm-filter" hidden><label class="sr-only" for="adm-q">${tx('wu.filter')}</label>
|
|
280
|
+
<input id="adm-q" type="search" placeholder="${tx('wu.filter')}" autocomplete="off" data-adm-filter></div></div>
|
|
281
|
+
<p class="u-mt0 adm-lede"><a href="https://github.com/pgkiln/pgkiln/blob/main/docs/guide/03-builder.md#workspaces" target="_blank" rel="noopener">${tx('wu.learn_more')}</a></p>
|
|
282
|
+
${section('people', tx('wu.sec.people'), [
|
|
283
|
+
[`${BASE}/users`, 'user', tx('wu.users'), tx('wu.users_text')],
|
|
284
|
+
[`${BASE}/developers`, 'users', tx('wu.developers'), tx('wu.developers_text')],
|
|
285
|
+
[`${BASE}/users/providers`, 'key', tx('wu.providers'), tx('wu.providers_text')],
|
|
286
|
+
[`${BASE}/users/directories`, 'org', tx('wu.ldap'), tx('wu.ldap_text')],
|
|
287
|
+
[`${BASE}/users#account-settings`, 'shield', tx('wu.password_policy'), tx('wu.password_policy_text')],
|
|
288
|
+
])}
|
|
289
|
+
${section('connections', tx('wu.sec.connections'), [
|
|
290
|
+
[`${BASE}/remote-servers`, 'server', tx('wu.servers'), tx('wu.servers_text')],
|
|
291
|
+
admin ? [`${BASE}/remote-databases`, 'database', tx('wu.remote_dbs'), tx('wu.remote_dbs_text')] : null,
|
|
292
|
+
[`${BASE}/workspace-credentials`, 'key', tx('wu.credentials'), tx('wu.credentials_text')],
|
|
293
|
+
[`${BASE}/rest-catalogs`, 'globe', tx('wu.catalogs'), tx('wu.catalogs_text')],
|
|
294
|
+
admin ? [`${BASE}/mail`, 'send', tx('wu.mail'), tx('wu.mail_text')] : null,
|
|
295
|
+
wsAdmin ? [`${BASE}/mail-queue`, 'inbox', tx('admin.mail_queue'), ''] : null,
|
|
296
|
+
wsAdmin ? [`${BASE}/network`, 'globe', tx('admin.network'), ''] : null,
|
|
297
|
+
])}
|
|
298
|
+
${section('data', tx('wu.sec.data'), [
|
|
299
|
+
admin ? [`${BASE}/ai`, 'bolt', tx('wu.ai'), tx('wu.ai_text')] : null,
|
|
300
|
+
[`${BASE}/vector-providers`, 'search', tx('wu.vectors'), tx('wu.vectors_text')],
|
|
301
|
+
[`${BASE}/ui-defaults`, 'sliders-horizontal', tx('wu.ui_defaults'), tx('wu.ui_defaults_text')],
|
|
302
|
+
[`${BASE}/workspace-files`, 'file', tx('wu.files'), tx('wu.files_text', { tag: '#WORKSPACE_FILES#' })],
|
|
303
|
+
[`${BASE}/icons`, 'image', tx('wu.icons'), tx('wu.icons_text')],
|
|
304
|
+
])}
|
|
305
|
+
${section('operations', tx('wu.sec.operations'), [
|
|
306
|
+
[`${BASE}/app-groups`, 'folder', tx('wu.app_groups'), tx('wu.app_groups_text')],
|
|
307
|
+
[`${BASE}/backups`, 'archive', tx('wu.backups'), tx('wu.backups_text')],
|
|
308
|
+
wsAdmin ? [`${BASE}/workspace-export`, 'download', tx('wu.export'), tx('wu.export_text')] : null,
|
|
309
|
+
[`${BASE}/dashboard`, 'activity', tx('wu.dashboard'), tx('wu.dashboard_text')],
|
|
310
|
+
admin ? [`${BASE}/telemetry`, 'activity', tx('wu.otel'), tx('wu.otel_text')] : null,
|
|
311
|
+
])}
|
|
312
|
+
${ws && wsAdmin ? section('workspace', tx('wu.sec.workspace', { name: ws.name }), [
|
|
313
|
+
[`${BASE}/workspaces/${ws.id}`, 'layers', tx('admin.ws_devs'), tx('wu.workspace_text')],
|
|
314
|
+
[`${BASE}/workspaces/${ws.id}#service`, 'bell', tx('admin.ws_message'), ''],
|
|
315
|
+
[`${BASE}/workspaces/${ws.id}#preferences`, 'sliders-horizontal', tx('admin.ws_prefs'), ''],
|
|
316
|
+
[`${BASE}/workspaces/${ws.id}#links`, 'external-link', tx('admin.ext_links'), ''],
|
|
317
|
+
[`${BASE}/workspaces/${ws.id}/utilization`, 'chart', tx('admin.utilization'), ''],
|
|
318
|
+
]) : ''}
|
|
319
|
+
${admin ? section('instance', tx('wu.sec.instance'), [
|
|
320
|
+
[`${BASE}/instance`, 'settings', tx('wu.instance'), tx('wu.instance_text')],
|
|
321
|
+
[`${BASE}/workspaces`, 'layers', tx('wu.workspaces'), tx('wu.workspaces_text')],
|
|
322
|
+
s.instanceAdmin ? [`${BASE}/db-users`, 'key', tx('admin.db_users'), ''] : null,
|
|
323
|
+
[`${BASE}/installation`, 'history', tx('wu.installation'), tx('wu.installation_text')],
|
|
324
|
+
]) : ''}
|
|
325
|
+
<section class="adm-section" aria-labelledby="adm-reports"><h2 id="adm-reports">${tx('wu.cross_app')}</h2><ul class="ab-links ab-tasks adm-reports">${crossAppLinks()}</ul></section>
|
|
326
|
+
<p class="adm-none muted" hidden>${tx('wu.filter_none')}</p>
|
|
327
|
+
</div>`;
|
|
328
|
+
return send(reply, s, shell(s, tx('rail.admin'), [[tx('rail.admin')]], main));
|
|
318
329
|
});
|
|
319
330
|
}
|
package/src/builder/i18n/de.ts
CHANGED
|
@@ -7510,4 +7510,32 @@ export const de: BuilderMessages = {
|
|
|
7510
7510
|
'pjb.steps': 'Schritte',
|
|
7511
7511
|
'pjb.message': 'Meldung',
|
|
7512
7512
|
'pjb.no_runs': 'Noch keine Ausführungen.',
|
|
7513
|
+
// ------------------------------------------------------------ PWA: every page offline (sprint 168)
|
|
7514
|
+
'pwa.prefetch': 'Alle Seiten der Navigation nach der Anmeldung offline verfügbar machen',
|
|
7515
|
+
'pwa.prefetch_help': 'Erfordert „Besuchte Seiten behalten“. Nach einer Anmeldung und auf der ersten auf einem Gerät geöffneten Seite lädt das Gerät nacheinander alle Seiten des Navigationsmenüs und der Navigationsleiste, die der Benutzer öffnen darf. Der Server zeigt sie schreibgeschützt: Seiten, die beim Öffnen Prozesse ausführen, werden ausgelassen und erst gespeichert, wenn der Benutzer sie öffnet.',
|
|
7516
|
+
'pwa.prefetch_hours': 'Erneut laden nach (Stunden)',
|
|
7517
|
+
'pwa.prefetch_hours_help': '1 bis 168. Die Seiten werden nach dieser Zeit beim ersten Seitenaufruf erneut geladen, sofern eine Verbindung besteht.',
|
|
7518
|
+
'pwa.prefetch_hours_range': 'Erneut laden nach: eine ganze Zahl von Stunden zwischen 1 und 168.',
|
|
7519
|
+
// ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
|
|
7520
|
+
'ws.more': 'Weitere Aktionen',
|
|
7521
|
+
'ws.suggest': 'Vorschläge',
|
|
7522
|
+
'ws.suggest_title': 'Vorschläge an der Cursorposition (Strg+Leertaste)',
|
|
7523
|
+
'ws.check': 'Prüfen',
|
|
7524
|
+
'ws.check_title': 'Das SQL planen, ohne es auszuführen, wie der Advisor',
|
|
7525
|
+
'ws.key.indent': 'Einrücken (Esc, dann Tab: nächstes Feld)',
|
|
7526
|
+
'ws.search_path_plain': '{schema} zuerst im Suchpfad',
|
|
7527
|
+
// ------------------------------------------------------------ Rail labels (UX-3)
|
|
7528
|
+
'rail.labels_show': 'Beschriftungen anzeigen',
|
|
7529
|
+
'rail.labels_hide': 'Beschriftungen ausblenden',
|
|
7530
|
+
// ------------------------------------------------------------ Administration page (UX-4)
|
|
7531
|
+
'wu.filter': 'Hilfsprogramme filtern',
|
|
7532
|
+
'wu.filter_none': 'Nichts entspricht dem Filter.',
|
|
7533
|
+
'wu.sec.people': 'Personen und Anmeldung',
|
|
7534
|
+
'wu.sec.connections': 'Verbindungen',
|
|
7535
|
+
'wu.sec.data': 'Daten und KI',
|
|
7536
|
+
'wu.sec.operations': 'Betrieb',
|
|
7537
|
+
'wu.sec.workspace': 'Arbeitsbereich {name}',
|
|
7538
|
+
'wu.sec.instance': 'Instanz',
|
|
7539
|
+
// ------------------------------------------------------------ Workspace home: needs attention (UX-13)
|
|
7540
|
+
'wh.attention': 'Erfordert Aufmerksamkeit',
|
|
7513
7541
|
};
|
package/src/builder/i18n/en.ts
CHANGED
|
@@ -7514,4 +7514,32 @@ export const en = {
|
|
|
7514
7514
|
'pjb.steps': 'Steps',
|
|
7515
7515
|
'pjb.message': 'Message',
|
|
7516
7516
|
'pjb.no_runs': 'No runs yet.',
|
|
7517
|
+
// ------------------------------------------------------------ PWA: every page offline (sprint 168)
|
|
7518
|
+
'pwa.prefetch': 'Make every page of the navigation available offline after sign-in',
|
|
7519
|
+
'pwa.prefetch_help': 'Needs Keep visited pages. After a sign-in, and on the first page opened on a device, the device fetches every page of the navigation menu and navigation bar the user may open, one at a time. The server shows them read-only: pages that run processes when they open are left out and kept only when the user opens them.',
|
|
7520
|
+
'pwa.prefetch_hours': 'Fetch them again after (hours)',
|
|
7521
|
+
'pwa.prefetch_hours_help': '1 to 168. The pages are fetched again on the first page opened after this time, while there is a connection.',
|
|
7522
|
+
'pwa.prefetch_hours_range': 'Fetch them again after: a whole number of hours from 1 to 168.',
|
|
7523
|
+
// ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
|
|
7524
|
+
'ws.more': 'More actions',
|
|
7525
|
+
'ws.suggest': 'Suggest',
|
|
7526
|
+
'ws.suggest_title': 'Suggestions at the cursor (Ctrl+Space)',
|
|
7527
|
+
'ws.check': 'Check',
|
|
7528
|
+
'ws.check_title': 'Plan the SQL without running it, as the Advisor does',
|
|
7529
|
+
'ws.key.indent': 'Indent (Esc, then Tab: next field)',
|
|
7530
|
+
'ws.search_path_plain': '{schema} first in the search path',
|
|
7531
|
+
// ------------------------------------------------------------ Rail labels (UX-3)
|
|
7532
|
+
'rail.labels_show': 'Show labels',
|
|
7533
|
+
'rail.labels_hide': 'Hide labels',
|
|
7534
|
+
// ------------------------------------------------------------ Administration page (UX-4)
|
|
7535
|
+
'wu.filter': 'Filter the utilities',
|
|
7536
|
+
'wu.filter_none': 'Nothing matches the filter.',
|
|
7537
|
+
'wu.sec.people': 'People and sign-in',
|
|
7538
|
+
'wu.sec.connections': 'Connections',
|
|
7539
|
+
'wu.sec.data': 'Data and AI',
|
|
7540
|
+
'wu.sec.operations': 'Operations',
|
|
7541
|
+
'wu.sec.workspace': 'Workspace {name}',
|
|
7542
|
+
'wu.sec.instance': 'Instance',
|
|
7543
|
+
// ------------------------------------------------------------ Workspace home: needs attention (UX-13)
|
|
7544
|
+
'wh.attention': 'Needs attention',
|
|
7517
7545
|
} as const;
|
package/src/builder/i18n/es.ts
CHANGED
|
@@ -7510,4 +7510,32 @@ export const es: BuilderMessages = {
|
|
|
7510
7510
|
'pjb.steps': 'Pasos',
|
|
7511
7511
|
'pjb.message': 'Mensaje',
|
|
7512
7512
|
'pjb.no_runs': 'Aún no hay ejecuciones.',
|
|
7513
|
+
// ------------------------------------------------------------ PWA: every page offline (sprint 168)
|
|
7514
|
+
'pwa.prefetch': 'Hacer que todas las páginas de la navegación estén disponibles sin conexión después de iniciar sesión',
|
|
7515
|
+
'pwa.prefetch_help': 'Requiere Guardar las páginas visitadas. Después de iniciar sesión, y en la primera página abierta en un dispositivo, el dispositivo obtiene una por una todas las páginas del menú y de la barra de navegación que el usuario puede abrir. El servidor las muestra en solo lectura: las páginas que ejecutan procesos al abrirse se omiten y solo se guardan cuando el usuario las abre.',
|
|
7516
|
+
'pwa.prefetch_hours': 'Volver a obtenerlas después de (horas)',
|
|
7517
|
+
'pwa.prefetch_hours_help': 'De 1 a 168. Las páginas se vuelven a obtener en la primera página abierta después de este tiempo, mientras haya conexión.',
|
|
7518
|
+
'pwa.prefetch_hours_range': 'Volver a obtenerlas después de: un número entero de horas entre 1 y 168.',
|
|
7519
|
+
// ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
|
|
7520
|
+
'ws.more': 'Más acciones',
|
|
7521
|
+
'ws.suggest': 'Sugerir',
|
|
7522
|
+
'ws.suggest_title': 'Sugerencias en el cursor (Ctrl+Espacio)',
|
|
7523
|
+
'ws.check': 'Comprobar',
|
|
7524
|
+
'ws.check_title': 'Planificar el SQL sin ejecutarlo, como el Advisor',
|
|
7525
|
+
'ws.key.indent': 'Sangrar (Esc y luego Tab: siguiente campo)',
|
|
7526
|
+
'ws.search_path_plain': '{schema} primero en la ruta de búsqueda',
|
|
7527
|
+
// ------------------------------------------------------------ Rail labels (UX-3)
|
|
7528
|
+
'rail.labels_show': 'Mostrar etiquetas',
|
|
7529
|
+
'rail.labels_hide': 'Ocultar etiquetas',
|
|
7530
|
+
// ------------------------------------------------------------ Administration page (UX-4)
|
|
7531
|
+
'wu.filter': 'Filtrar las utilidades',
|
|
7532
|
+
'wu.filter_none': 'Nada coincide con el filtro.',
|
|
7533
|
+
'wu.sec.people': 'Personas e inicio de sesión',
|
|
7534
|
+
'wu.sec.connections': 'Conexiones',
|
|
7535
|
+
'wu.sec.data': 'Datos e IA',
|
|
7536
|
+
'wu.sec.operations': 'Operaciones',
|
|
7537
|
+
'wu.sec.workspace': 'Espacio de trabajo {name}',
|
|
7538
|
+
'wu.sec.instance': 'Instancia',
|
|
7539
|
+
// ------------------------------------------------------------ Workspace home: needs attention (UX-13)
|
|
7540
|
+
'wh.attention': 'Requiere atención',
|
|
7513
7541
|
};
|
package/src/builder/i18n/fr.ts
CHANGED
|
@@ -7510,4 +7510,32 @@ export const fr: BuilderMessages = {
|
|
|
7510
7510
|
'pjb.steps': 'Étapes',
|
|
7511
7511
|
'pjb.message': 'Message',
|
|
7512
7512
|
'pjb.no_runs': 'Aucune exécution pour l’instant.',
|
|
7513
|
+
// ------------------------------------------------------------ PWA: every page offline (sprint 168)
|
|
7514
|
+
'pwa.prefetch': 'Rendre toutes les pages de la navigation disponibles hors ligne après la connexion',
|
|
7515
|
+
'pwa.prefetch_help': 'Nécessite Conserver les pages visitées. Après une connexion, et sur la première page ouverte sur un appareil, l’appareil récupère une à une toutes les pages du menu et de la barre de navigation que l’utilisateur peut ouvrir. Le serveur les affiche en lecture seule : les pages qui exécutent des processus à l’ouverture sont ignorées et conservées seulement quand l’utilisateur les ouvre.',
|
|
7516
|
+
'pwa.prefetch_hours': 'Les récupérer à nouveau après (heures)',
|
|
7517
|
+
'pwa.prefetch_hours_help': 'De 1 à 168. Les pages sont récupérées à nouveau à la première page ouverte après ce délai, s’il y a une connexion.',
|
|
7518
|
+
'pwa.prefetch_hours_range': 'Les récupérer à nouveau après : un nombre entier d’heures entre 1 et 168.',
|
|
7519
|
+
// ------------------------------------------------------------ SQL Worksheet: one toolbar row (UX-1)
|
|
7520
|
+
'ws.more': 'Plus d’actions',
|
|
7521
|
+
'ws.suggest': 'Suggérer',
|
|
7522
|
+
'ws.suggest_title': 'Suggestions au curseur (Ctrl+Espace)',
|
|
7523
|
+
'ws.check': 'Vérifier',
|
|
7524
|
+
'ws.check_title': 'Planifier le SQL sans l’exécuter, comme l’Advisor',
|
|
7525
|
+
'ws.key.indent': 'Indenter (Échap puis Tab : champ suivant)',
|
|
7526
|
+
'ws.search_path_plain': '{schema} en premier dans le chemin de recherche',
|
|
7527
|
+
// ------------------------------------------------------------ Rail labels (UX-3)
|
|
7528
|
+
'rail.labels_show': 'Afficher les libellés',
|
|
7529
|
+
'rail.labels_hide': 'Masquer les libellés',
|
|
7530
|
+
// ------------------------------------------------------------ Administration page (UX-4)
|
|
7531
|
+
'wu.filter': 'Filtrer les utilitaires',
|
|
7532
|
+
'wu.filter_none': 'Rien ne correspond au filtre.',
|
|
7533
|
+
'wu.sec.people': 'Personnes et connexion',
|
|
7534
|
+
'wu.sec.connections': 'Connexions',
|
|
7535
|
+
'wu.sec.data': 'Données et IA',
|
|
7536
|
+
'wu.sec.operations': 'Exploitation',
|
|
7537
|
+
'wu.sec.workspace': 'Espace de travail {name}',
|
|
7538
|
+
'wu.sec.instance': 'Instance',
|
|
7539
|
+
// ------------------------------------------------------------ Workspace home: needs attention (UX-13)
|
|
7540
|
+
'wh.attention': 'À traiter',
|
|
7513
7541
|
};
|