pgkiln 0.53.0 → 0.55.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 +2 -0
- package/db/migrations/127_builder_language.sql +16 -0
- package/docs/apex-feature-parity.md +7 -7
- package/docs/guide/03-builder.md +62 -8
- package/docs/guide/12-development.md +50 -3
- package/examples/gallery/README.md +37 -6
- package/examples/gallery/gallery.json +23 -1
- package/examples/gallery/samples/sample-calendar.sql +172 -0
- package/examples/gallery/samples/sample-cards.sql +157 -0
- package/examples/gallery/samples/sample-charts.sql +265 -0
- package/examples/gallery/samples/sample-dynamic-actions.sql +177 -0
- package/examples/gallery/samples/sample-grids.sql +151 -0
- package/examples/gallery/samples/sample-reporting.sql +151 -0
- package/examples/gallery/samples/ux-patterns.sql +295 -0
- package/examples/gallery/utilities/announcements.json +24 -0
- package/examples/gallery/utilities/lookup-lists.json +28 -0
- package/examples/gallery/utilities/release-notes.json +41 -0
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/builder.css +39 -1
- package/public/worksheet.css +3 -2
- package/src/builder/advisor.ts +1 -1
- package/src/builder/ai-agents.ts +5 -5
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/api.ts +1 -1
- package/src/builder/autorest.ts +2 -2
- package/src/builder/availability.ts +1 -1
- package/src/builder/backups.ts +1 -1
- package/src/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +4 -4
- package/src/builder/data-security.ts +1 -1
- package/src/builder/dataload.ts +1 -1
- package/src/builder/datapump.ts +1 -1
- package/src/builder/ddl-tools.ts +2 -2
- package/src/builder/designer.ts +134 -124
- package/src/builder/diagnostics.ts +2 -2
- package/src/builder/errors.ts +10 -12
- package/src/builder/feedback.ts +1 -1
- package/src/builder/gallery.ts +94 -47
- package/src/builder/globalization.ts +1 -1
- package/src/builder/home.ts +59 -57
- package/src/builder/i18n/de.ts +675 -0
- package/src/builder/i18n/en.ts +678 -0
- package/src/builder/i18n/es.ts +675 -0
- package/src/builder/i18n/fr.ts +675 -0
- package/src/builder/i18n/nl.ts +675 -0
- package/src/builder/i18n/pt.ts +675 -0
- package/src/builder/i18n.ts +116 -0
- package/src/builder/language.ts +74 -0
- package/src/builder/launchpad.ts +13 -8
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/plugins.ts +1 -1
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/rest-modules.ts +2 -2
- package/src/builder/routes.ts +88 -66
- package/src/builder/sampledata.ts +1 -1
- package/src/builder/scheduling.ts +1 -1
- package/src/builder/scripts.ts +1 -1
- package/src/builder/search.ts +1 -1
- package/src/builder/shared.ts +5 -5
- package/src/builder/sql-charts.ts +2 -2
- package/src/builder/sql-monitor.ts +2 -2
- package/src/builder/sql-tools.ts +6 -0
- package/src/builder/sql-utilities.ts +3 -3
- package/src/builder/sql.ts +24 -22
- package/src/builder/static-files.ts +1 -1
- package/src/builder/subscriptions.ts +1 -1
- package/src/builder/supporting.ts +1 -1
- package/src/builder/table-editor.ts +1 -1
- package/src/builder/team.ts +7 -5
- package/src/builder/themeroller.ts +1 -1
- package/src/builder/top-sql.ts +1 -1
- package/src/builder/ui.ts +87 -77
- package/src/builder/unload.ts +1 -1
- package/src/builder/wizards.ts +1 -1
- package/src/builder/workflow-migrate.ts +1 -1
- package/src/builder/workingcopies.ts +2 -2
- package/src/builder/worksheet.ts +1 -1
- package/src/builder/workspace-home.ts +62 -59
- package/src/builder/workspaces.ts +2 -1
- package/src/runtime/charts.ts +4 -1
- package/src/runtime/regions.ts +8 -5
|
@@ -9,6 +9,7 @@ import { isAdmin } from './locks.ts';
|
|
|
9
9
|
import { back, BASE, csrf, developer, flash, region, send, shell, type Req } from './ui.ts';
|
|
10
10
|
import { currentWorkspace, inWorkspaceSql, workspaceAdmin } from './workspaces.ts';
|
|
11
11
|
import { teamHomeBlock } from './team.ts';
|
|
12
|
+
import { tr } from './i18n.ts';
|
|
12
13
|
|
|
13
14
|
// (sprint 109, WS-1) The Workspace home (APEX: the workspace home page), the page the builder opens after
|
|
14
15
|
// signing in: cards for the tools (App Builder, SQL Workshop, App Gallery), the workspace message (edited in place
|
|
@@ -83,6 +84,7 @@ export async function homeData(ws: number, tf: Timeframe) {
|
|
|
83
84
|
|
|
84
85
|
/** The System message: the running version, and a newer release when the check is on. */
|
|
85
86
|
async function systemMessage(s: Session, admin: boolean) {
|
|
87
|
+
const tx = tr(s);
|
|
86
88
|
const check = await releaseCheckEnabled();
|
|
87
89
|
// never wait for GitHub: show what is cached, refresh in the background
|
|
88
90
|
if (check.on) void refreshRelease();
|
|
@@ -91,27 +93,26 @@ async function systemMessage(s: Session, admin: boolean) {
|
|
|
91
93
|
const newer = r && newerVersion(r.version, VERSION) ? r : null;
|
|
92
94
|
const body = newer
|
|
93
95
|
? html`<div class="alert alert-info wh-release" role="status">${icon('bell')}<div>
|
|
94
|
-
<strong
|
|
95
|
-
<p>${newer.name}${newer.publishedAt ? html` <span class="muted">(${ago(newer.publishedAt)})</span>` : ''}.
|
|
96
|
-
<p><a href="${newer.url}" target="_blank" rel="noopener noreferrer"
|
|
97
|
-
<a href="https://github.com/pgkiln/pgkiln/blob/main/docs/guide/01-installation.md#upgrading" target="_blank" rel="noopener"
|
|
98
|
-
<p class="small muted"
|
|
99
|
-
|
|
100
|
-
: html`<p class="u-mt0">This server runs <strong>pgkiln ${VERSION}</strong>.${check.on && r && !newer ? ' It is the latest release.' : ''}</p>`;
|
|
96
|
+
<strong>${tx('wh.available', { version: newer.version })}</strong>
|
|
97
|
+
<p>${newer.name}${newer.publishedAt ? html` <span class="muted">(${ago(newer.publishedAt, Date.now(), tx)})</span>` : ''}. ${tx('wh.server_runs', { version: VERSION })}</p>
|
|
98
|
+
<p><a href="${newer.url}" target="_blank" rel="noopener noreferrer">${tx('wh.release_notes')} ${icon('external')}</a> ·
|
|
99
|
+
<a href="https://github.com/pgkiln/pgkiln/blob/main/docs/guide/01-installation.md#upgrading" target="_blank" rel="noopener">${tx('wh.how_upgrade')}</a></p>
|
|
100
|
+
<p class="small muted">${tx.rich('wh.upgrade_steps', { npm: html`<code>npm install -g pgkiln@${newer.version}</code>`, docker: html`<code>pgkiln/pgkiln:${newer.version}</code>`, migrate: html`<code>pgkiln migrate</code>` })}</p></div></div>`
|
|
101
|
+
: html`<p class="u-mt0">${tx.rich('wh.server_runs_strong', { version: html`<strong>pgkiln ${VERSION}</strong>` })}${check.on && r && !newer ? ` ${tx('wh.latest')}` : ''}</p>`;
|
|
101
102
|
const status = !check.on
|
|
102
|
-
? '
|
|
103
|
+
? tx('wh.check_off')
|
|
103
104
|
: c?.error
|
|
104
|
-
?
|
|
105
|
-
: c ?
|
|
105
|
+
? tx(c.release ? 'wh.check_failed_older' : 'wh.check_failed', { error: c.error })
|
|
106
|
+
: c ? tx('wh.checked', { ago: ago(new Date(c.at), Date.now(), tx) }) : tx('wh.checking');
|
|
106
107
|
const admins = admin
|
|
107
108
|
? html`<form method="post" action="${HOME}/release-check" class="wh-release-form">${csrf(s)}
|
|
108
|
-
<p class="small muted u-mt0">${status} ${check.on ? '' :
|
|
109
|
+
<p class="small muted u-mt0">${status} ${check.on ? '' : tx.rich('wh.check_explain', { host: html`<code>api.github.com</code>` })}
|
|
109
110
|
${check.from === 'environment' ? html`(<code>PGKILN_RELEASE_CHECK</code>)` : ''}</p>
|
|
110
111
|
<div class="buttons">${check.on
|
|
111
|
-
? html`<button class="btn btn-sm" name="check" value="now">${icon('refresh')}
|
|
112
|
-
: html`<button class="btn btn-sm" name="check" value="on"
|
|
112
|
+
? html`<button class="btn btn-sm" name="check" value="now">${icon('refresh')} ${tx('wh.check_now')}</button><button class="btn btn-sm" name="check" value="off">${tx('wh.turn_off')}</button>`
|
|
113
|
+
: html`<button class="btn btn-sm" name="check" value="on">${tx('wh.check_on')}</button>`}</div></form>`
|
|
113
114
|
: '';
|
|
114
|
-
return region('
|
|
115
|
+
return region(tx('wh.system'), html`${body}${admins}`, '', 'system-message');
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/** (Instance settings page) the release check: on, off, or as the environment says. */
|
|
@@ -133,16 +134,17 @@ function workspaceMessage(s: Session, canEdit: boolean) {
|
|
|
133
134
|
const w = s.workspace;
|
|
134
135
|
if (!w) return '';
|
|
135
136
|
if (!w.message && !canEdit) return '';
|
|
137
|
+
const tx = tr(s);
|
|
136
138
|
return html`<section class="region region-standard wh-message" id="workspace-message" aria-labelledby="wh-message-title">
|
|
137
|
-
<header class="region-header"><h2 id="wh-message-title"
|
|
139
|
+
<header class="region-header"><h2 id="wh-message-title">${tx('wh.message')}</h2></header>
|
|
138
140
|
<div class="region-body">
|
|
139
|
-
${w.message ? html`<p class="wh-message-text">${w.message}</p>` : html`<p class="muted u-mt0"
|
|
140
|
-
${canEdit ? html`<details class="wh-edit"><summary class="btn btn-sm" title="
|
|
141
|
+
${w.message ? html`<p class="wh-message-text">${w.message}</p>` : html`<p class="muted u-mt0">${tx('wh.message_none', { name: w.name })}</p>`}
|
|
142
|
+
${canEdit ? html`<details class="wh-edit"><summary class="btn btn-sm" title="${tx('wh.message_edit_title')}">${icon('edit')}<span>${tx('wh.message_edit')}</span></summary>
|
|
141
143
|
<form method="post" action="${HOME}/message">${csrf(s)}<input type="hidden" name="workspace" value="${w.id}">
|
|
142
|
-
<div class="field"><label class="label" for="f_message"
|
|
144
|
+
<div class="field"><label class="label" for="f_message">${tx('wh.message')}</label>
|
|
143
145
|
<textarea id="f_message" name="message" rows="3" maxlength="2000">${w.message ?? ''}</textarea>
|
|
144
|
-
<small class="help"
|
|
145
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
146
|
+
<small class="help">${tx('wh.message_help', { name: w.name })}</small></div>
|
|
147
|
+
<div class="buttons"><button class="btn btn-hot">${tx('wh.message_save')}</button></div></form></details>` : ''}
|
|
146
148
|
</div></section>`;
|
|
147
149
|
}
|
|
148
150
|
|
|
@@ -160,68 +162,69 @@ export async function workspaceHomeRoutes(app: FastifyInstance) {
|
|
|
160
162
|
const data = await homeData(ws, tf);
|
|
161
163
|
const sql = s.workspace?.sql_workshop !== false;
|
|
162
164
|
const reporter = app.hasRoute({ method: 'GET', url: REPORTER });
|
|
165
|
+
const tx = tr(s);
|
|
163
166
|
const card = (href: string, ic: string, title: string, text: string) =>
|
|
164
167
|
html`<a class="ab-tile" href="${href}"><span class="ab-tile-icon">${icon(ic)}</span><strong>${title}</strong><span>${text}</span></a>`;
|
|
165
168
|
const stat = (n: number, one: string, many: string, href: string | null) => {
|
|
166
169
|
const inner = html`<b>${n}</b><span>${n === 1 ? one : many}</span>`;
|
|
167
170
|
return href ? html`<a class="stat" href="${href}">${inner}</a>` : html`<div class="stat">${inner}</div>`;
|
|
168
171
|
};
|
|
169
|
-
const tfLabel =
|
|
172
|
+
const tfLabel = tx(`wh.tf.${tf}`);
|
|
170
173
|
const timeframe = html`<form method="post" action="${HOME}/timeframe" class="segmented wh-timeframe" role="group" aria-labelledby="wh-tf-label">${csrf(s)}
|
|
171
|
-
<span class="sr-only" id="wh-tf-label"
|
|
172
|
-
${(Object.keys(TIMEFRAMES) as Timeframe[]).map((k) => html`<button name="timeframe" value="${k}" aria-pressed="${k === tf ? 'true' : 'false'}">${
|
|
173
|
-
const none = html`<p class="muted u-mt0"
|
|
174
|
+
<span class="sr-only" id="wh-tf-label">${tx('wh.timeframe')}</span>
|
|
175
|
+
${(Object.keys(TIMEFRAMES) as Timeframe[]).map((k) => html`<button name="timeframe" value="${k}" aria-pressed="${k === tf ? 'true' : 'false'}">${tx(`wh.tf.${k}`)}</button>`)}</form>`;
|
|
176
|
+
const none = html`<p class="muted u-mt0">${tx('wh.no_views', { tf: tfLabel })}</p>`;
|
|
174
177
|
const people = (rows: { username: string; views: number; apps: number }[], caption: string, who: string) => rows.length
|
|
175
|
-
? html`<div class="table-wrap"><table class="report"><caption class="sr-only">${caption}
|
|
176
|
-
<thead><tr><th scope="col">${who}</th><th scope="col" class="num"
|
|
178
|
+
? html`<div class="table-wrap"><table class="report"><caption class="sr-only">${caption}</caption>
|
|
179
|
+
<thead><tr><th scope="col">${who}</th><th scope="col" class="num">${tx('wh.col.views')}</th><th scope="col" class="num">${tx('wh.col.apps')}</th></tr></thead>
|
|
177
180
|
<tbody>${rows.map((u) => html`<tr><td class="cell-break">${u.username}</td><td class="num">${u.views}</td><td class="num">${u.apps}</td></tr>`)}</tbody></table></div>`
|
|
178
181
|
: none;
|
|
179
182
|
const topApps = data.apps.length
|
|
180
|
-
? html`<div class="table-wrap"><table class="report"><caption class="sr-only"
|
|
181
|
-
<thead><tr><th scope="col"
|
|
183
|
+
? html`<div class="table-wrap"><table class="report"><caption class="sr-only">${tx('wh.top_apps_caption', { tf: tfLabel })}</caption>
|
|
184
|
+
<thead><tr><th scope="col">${tx('wh.col.app')}</th><th scope="col" class="num">${tx('wh.col.views')}</th><th scope="col" class="num">${tx('wh.col.users')}</th></tr></thead>
|
|
182
185
|
<tbody>${data.apps.map((a) => html`<tr><td class="cell-break"><a href="${BASE}/apps/${a.id}">${a.name}</a></td><td class="num">${a.views}</td><td class="num">${a.users}</td></tr>`)}</tbody></table></div>`
|
|
183
186
|
: none;
|
|
184
187
|
const ext = (href: string, label: string) => html`<li><a href="${href}" target="_blank" rel="noopener">${label}${icon('chevron')}</a></li>`;
|
|
185
188
|
const main = html`<div class="ab-home wh-home">
|
|
186
189
|
<div class="ab-main">
|
|
187
|
-
<h1 class="wh-title">${s.workspace ?
|
|
188
|
-
<nav class="ab-tiles ab-tiles-wrap wh-tools" aria-label="
|
|
189
|
-
${card(`${BASE}/apps`, 'grid', '
|
|
190
|
-
${sql ? card(`${BASE}/sql`, 'database', '
|
|
191
|
-
${reporter ? card(REPORTER, 'pie-chart', '
|
|
192
|
-
${card(`${BASE}/gallery`, 'package', '
|
|
190
|
+
<h1 class="wh-title">${s.workspace ? tx('wh.title_ws', { name: s.workspace.name }) : tx('wh.title')}</h1>
|
|
191
|
+
<nav class="ab-tiles ab-tiles-wrap wh-tools" aria-label="${tx('wh.tools')}">
|
|
192
|
+
${card(`${BASE}/apps`, 'grid', tx('rail.app_builder'), tx('wh.card.apps'))}
|
|
193
|
+
${sql ? card(`${BASE}/sql`, 'database', tx('rail.sql_workshop'), tx('wh.card.sql')) : ''}
|
|
194
|
+
${reporter ? card(REPORTER, 'pie-chart', tx('rail.reporter'), tx('wh.card.reporter')) : ''}
|
|
195
|
+
${card(`${BASE}/gallery`, 'package', tx('rail.gallery'), tx('wh.card.gallery'))}
|
|
193
196
|
</nav>
|
|
194
197
|
${workspaceMessage(s, canEdit)}
|
|
195
198
|
<section class="wh-summary" aria-labelledby="wh-summary-title">
|
|
196
|
-
<h2 id="wh-summary-title"
|
|
199
|
+
<h2 id="wh-summary-title">${tx('wh.summary')}</h2>
|
|
197
200
|
<div class="stat-grid wh-stats">
|
|
198
|
-
${stat(data.counts.apps, '
|
|
199
|
-
${stat(data.counts.tables, 'table', 'tables', sql ? `${BASE}/sql/objects` : null)}
|
|
200
|
-
${stat(data.counts.developers, 'developer', 'developers', `${BASE}/developers`)}
|
|
201
|
-
${stat(data.counts.reporting, 'reporting
|
|
201
|
+
${stat(data.counts.apps, tx('wh.stat.app'), tx('wh.stat.apps'), `${BASE}/apps`)}
|
|
202
|
+
${stat(data.counts.tables, tx('wh.stat.table'), tx('wh.stat.tables'), sql ? `${BASE}/sql/objects` : null)}
|
|
203
|
+
${stat(data.counts.developers, tx('wh.stat.developer'), tx('wh.stat.developers'), `${BASE}/developers`)}
|
|
204
|
+
${stat(data.counts.reporting, tx('wh.stat.reporting'), tx('wh.stat.reportings'), reporter ? REPORTER : null)}
|
|
202
205
|
</div>
|
|
203
206
|
</section>
|
|
204
|
-
<div class="wh-top-head"><h2
|
|
207
|
+
<div class="wh-top-head"><h2>${tx('wh.activity')}</h2>${timeframe}</div>
|
|
205
208
|
<div class="wh-top">
|
|
206
|
-
${region(
|
|
207
|
-
${region(
|
|
208
|
-
${region(
|
|
209
|
+
${region(tx('wh.top_apps', { tf: tfLabel }), topApps)}
|
|
210
|
+
${region(tx('wh.top_users', { tf: tfLabel }), people(data.users, tx('wh.top_users_caption', { tf: tfLabel }), tx('wh.col.user')))}
|
|
211
|
+
${region(tx('wh.top_devs', { tf: tfLabel }), people(data.developers, tx('wh.top_devs_caption', { tf: tfLabel }), tx('wh.col.developer')))}
|
|
209
212
|
</div>
|
|
210
213
|
${await systemMessage(s, admin)}
|
|
211
214
|
</div>
|
|
212
|
-
<aside class="ab-side" aria-label="
|
|
213
|
-
<section><h2
|
|
215
|
+
<aside class="ab-side" aria-label="${tx('wh.aside')}">
|
|
216
|
+
<section><h2>${tx('common.about')}</h2><p>${tx('wh.about')} ${s.workspace ? tx('wh.you_work', { name: s.workspace.name }) : ''}</p></section>
|
|
214
217
|
${await teamHomeBlock(s)}
|
|
215
|
-
<section><h2
|
|
216
|
-
${ext(`${GUIDE}/docs/README.md`, '
|
|
217
|
-
${ext(`${GUIDE}/docs/guide/10-tutorial.md`, '
|
|
218
|
-
${ext(`${GUIDE}/docs/guide/11-from-apex.md`, '
|
|
219
|
-
${ext(`${GUIDE}/CHANGELOG.md`,
|
|
220
|
-
${ext(`${REPO}/discussions`, '
|
|
218
|
+
<section><h2>${tx('wh.learn_more')}</h2><ul class="ab-links ab-tasks">
|
|
219
|
+
${ext(`${GUIDE}/docs/README.md`, tx('wh.guide'))}
|
|
220
|
+
${ext(`${GUIDE}/docs/guide/10-tutorial.md`, tx('wh.tutorial'))}
|
|
221
|
+
${ext(`${GUIDE}/docs/guide/11-from-apex.md`, tx('wh.from_apex'))}
|
|
222
|
+
${ext(`${GUIDE}/CHANGELOG.md`, tx('wh.whats_new'))}
|
|
223
|
+
${ext(`${REPO}/discussions`, tx('wh.discussions'))}
|
|
221
224
|
</ul></section>
|
|
222
|
-
<section><h2
|
|
223
|
-
${ext(`${GUIDE}/docs/guide/20-ai-agents.md`,
|
|
224
|
-
${ext(`${REPO}/issues`, '
|
|
225
|
+
<section><h2>${tx('wh.resources')}</h2><ul class="ab-links ab-tasks">
|
|
226
|
+
${ext(`${GUIDE}/docs/guide/20-ai-agents.md`, tx('wh.mcp'))}
|
|
227
|
+
${ext(`${REPO}/issues`, tx('wh.report_problem'))}
|
|
225
228
|
</ul></section>
|
|
226
229
|
</aside>
|
|
227
230
|
</div>`;
|
|
@@ -247,13 +250,13 @@ export async function workspaceHomeRoutes(app: FastifyInstance) {
|
|
|
247
250
|
if (!(await workspaceAdmin(s.username, id))) return reply.code(403).send('Only the workspace\'s administrators change its message.');
|
|
248
251
|
const message = String(req.body?.message ?? '').replace(/\r\n/g, '\n').trim();
|
|
249
252
|
if (message.length > 2000) {
|
|
250
|
-
flash(s, '
|
|
253
|
+
flash(s, tr(s)('wh.flash.too_long'), 'error');
|
|
251
254
|
return back(reply, s, HOME);
|
|
252
255
|
}
|
|
253
256
|
const r = await owner.query('update meta.workspace set message = $2 where id = $1', [id, message || null]);
|
|
254
257
|
if (!r.rowCount) return reply.code(404).send('Not found');
|
|
255
258
|
await logActivity({ appId: null, username: s.username, event: 'workspace', ip: clientIp(req), detail: `workspace ${id}: message` });
|
|
256
|
-
flash(s, message ? '
|
|
259
|
+
flash(s, tr(s)(message ? 'wh.flash.saved' : 'wh.flash.removed'));
|
|
257
260
|
return back(reply, s, `${HOME}#workspace-message`);
|
|
258
261
|
});
|
|
259
262
|
|
|
@@ -270,15 +273,15 @@ export async function workspaceHomeRoutes(app: FastifyInstance) {
|
|
|
270
273
|
await logActivity({ appId: null, username: s.username, event: 'instance_settings', ip: clientIp(req), detail: `release check ${v}` });
|
|
271
274
|
const now = await releaseCheckEnabled();
|
|
272
275
|
if (now.on) await refreshRelease(true);
|
|
273
|
-
flash(s, now.on ? '
|
|
276
|
+
flash(s, tr(s)(now.on ? 'wh.flash.check_daily' : 'wh.check_off'));
|
|
274
277
|
} else if (v === 'now') {
|
|
275
278
|
if (!(await releaseCheckEnabled()).on) {
|
|
276
|
-
flash(s, '
|
|
279
|
+
flash(s, tr(s)('wh.check_off'), 'error');
|
|
277
280
|
return back(reply, s, to);
|
|
278
281
|
}
|
|
279
282
|
await refreshRelease(true);
|
|
280
283
|
const c = cachedRelease();
|
|
281
|
-
flash(s, c?.error ?
|
|
284
|
+
flash(s, c?.error ? tr(s)('wh.flash.check_failed', { error: c.error }) : tr(s)('wh.flash.checked'), c?.error ? 'error' : 'ok');
|
|
282
285
|
} else return reply.code(400).send('Unknown action');
|
|
283
286
|
return back(reply, s, to);
|
|
284
287
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
2
|
+
import { tr } from './i18n.ts';
|
|
2
3
|
import { owner, type Db } from '../db.ts';
|
|
3
4
|
import { devDbForWorkspace, workspacesChanged } from '../workspace-db.ts';
|
|
4
5
|
import { ownerUrl, runtimeUrl } from '../db.ts';
|
|
@@ -118,7 +119,7 @@ export const inWorkspaceSql = (alias: string, param: number) => `meta.app_worksp
|
|
|
118
119
|
export function workspaceMenu(s: Session) {
|
|
119
120
|
const list = s.workspaces ?? [];
|
|
120
121
|
if (list.length < 2) return '';
|
|
121
|
-
return html`<div class="menu-section"><span class="small muted" id="ws-label"
|
|
122
|
+
return html`<div class="menu-section"><span class="small muted" id="ws-label">${tr(s)('status.workspace')}</span>
|
|
122
123
|
<form method="post" action="${BASE}/workspace" class="ws-switch" aria-labelledby="ws-label">${csrf(s)}
|
|
123
124
|
${list.map((w) => html`<button name="workspace" value="${w.id}" aria-pressed="${s.workspace?.id === w.id ? 'true' : 'false'}">${icon('layers')}<span>${w.name}</span></button>`)}
|
|
124
125
|
</form></div>`;
|
package/src/runtime/charts.ts
CHANGED
|
@@ -230,7 +230,10 @@ function column(labels: string[], series: Series[]) {
|
|
|
230
230
|
const col = html`<span class="col s${si + 1}${v < 0 ? ' neg' : ''} ${css.cls(`bottom:${pct(bottom)};height:${pct(top - bottom)}`)}"></span>`;
|
|
231
231
|
return multi && link ? mark('span', i, si, 'col-slot', `${l} · ${s.name}: ${fmt.format(v)}`, col, false) : html`<span class="col-slot">${col}</span>`;
|
|
232
232
|
})}${!multi && labels.length <= 12
|
|
233
|
-
|
|
233
|
+
// (sprint 120) inside a drill link the value is drawn by CSS (attr()), so the link's name is its tip alone
|
|
234
|
+
? link
|
|
235
|
+
? html`<span class="col-value ${css.cls(`bottom:${pct(Math.max(y(series[0].values[i]), zero))}`)}" aria-hidden="true" data-value="${compact.format(series[0].values[i])}"></span>`
|
|
236
|
+
: html`<span class="col-value ${css.cls(`bottom:${pct(Math.max(y(series[0].values[i]), zero))}`)}" aria-hidden="true">${compact.format(series[0].values[i])}</span>`
|
|
234
237
|
: ''}`;
|
|
235
238
|
return multi ? html`<div class="col-group" role="img" data-tip="${tip(l, series, i)}" tabindex="0" aria-label="${tip(l, series, i)}">${inner}</div>` : mark('div', i, 0, 'col-group', tip(l, series, i), inner);
|
|
236
239
|
})}</div>
|
package/src/runtime/regions.ts
CHANGED
|
@@ -465,7 +465,7 @@ async function regionShell(ctx: PageContext, r: Region, hidden: Set<string>, bod
|
|
|
465
465
|
return html`<dialog class="${cls}" id="R${r.id}"${staticAttr} aria-labelledby="${titleId}" data-inline-region>
|
|
466
466
|
<header class="region-header"><h2 id="${titleId}">${r.title}</h2>
|
|
467
467
|
<a class="region-dialog-close icon-button" href="#" role="button" data-region-close aria-label="${ctx.locale.t('common.dismiss')}">${icon('close')}</a></header>
|
|
468
|
-
<div class="region-body">${body}</div>
|
|
468
|
+
<div class="region-body"${scrollable(r)}>${body}</div>
|
|
469
469
|
${buttons ? html`<footer class="region-footer">${buttons}</footer>` : ''}
|
|
470
470
|
</dialog>`;
|
|
471
471
|
|
|
@@ -473,17 +473,20 @@ async function regionShell(ctx: PageContext, r: Region, hidden: Set<string>, bod
|
|
|
473
473
|
if (r.template === 'collapsible')
|
|
474
474
|
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr} aria-labelledby="${titleId}">
|
|
475
475
|
<details open><summary class="region-header">${header}</summary>
|
|
476
|
-
<div class="region-body">${body}</div>
|
|
476
|
+
<div class="region-body"${scrollable(r)}>${body}</div>
|
|
477
477
|
${!buttonsOnTop && buttons ? html`<footer class="region-footer">${buttons}</footer>` : ''}
|
|
478
478
|
</details></section>`;
|
|
479
479
|
|
|
480
480
|
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr}${r.title ? raw(` aria-labelledby="${titleId}"`) : ''}>
|
|
481
481
|
${r.title || (buttonsOnTop && buttons) ? html`<header class="region-header">${header}</header>` : ''}
|
|
482
|
-
<div class="region-body">${body}</div>
|
|
482
|
+
<div class="region-body"${scrollable(r)}>${body}</div>
|
|
483
483
|
${!buttonsOnTop && buttons ? html`<footer class="region-footer">${buttons}</footer>` : ''}
|
|
484
484
|
</section>`;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
/** (sprint 120) A body that scrolls (template option to-scroll) can be reached and scrolled from the keyboard. */
|
|
488
|
+
const scrollable = (r: Region) => (r.template_options?.includes('to-scroll') ? raw(' tabindex="0"') : '');
|
|
489
|
+
|
|
487
490
|
/**
|
|
488
491
|
* (099) A region whose buttons have positions (APEX: Button Position): create, edit, change, copy, help and up in
|
|
489
492
|
* the header; previous, close and delete at the left end of the bottom bar, next at its right end; the others
|
|
@@ -506,9 +509,9 @@ async function slottedShell(ctx: PageContext, r: Region, hidden: Set<string>, bo
|
|
|
506
509
|
const header = html`<h2 id="${titleId}">${r.title}</h2>${top}`;
|
|
507
510
|
if (r.template === 'collapsible')
|
|
508
511
|
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr} aria-labelledby="${titleId}">
|
|
509
|
-
<details open><summary class="region-header">${header}</summary><div class="region-body">${body}</div>${footer}</details></section>`;
|
|
512
|
+
<details open><summary class="region-header">${header}</summary><div class="region-body"${scrollable(r)}>${body}</div>${footer}</details></section>`;
|
|
510
513
|
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr}${r.title ? raw(` aria-labelledby="${titleId}"`) : ''}>
|
|
511
514
|
${r.title || top ? html`<header class="region-header">${header}</header>` : ''}
|
|
512
|
-
<div class="region-body">${body}</div>${footer}
|
|
515
|
+
<div class="region-body"${scrollable(r)}>${body}</div>${footer}
|
|
513
516
|
</section>`;
|
|
514
517
|
}
|