pgkiln 0.84.0 → 0.85.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/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +14 -5
- package/docs/guide/12-development.md +6 -1
- package/package.json +1 -1
- package/src/app.ts +9 -0
- package/src/builder/ai-agents.ts +86 -85
- package/src/builder/ai-builder.ts +30 -30
- package/src/builder/ai.ts +93 -92
- package/src/builder/api.ts +47 -47
- package/src/builder/app-groups.ts +33 -32
- package/src/builder/appfromfile.ts +57 -58
- package/src/builder/appsheets.ts +33 -32
- package/src/builder/appwizard.ts +65 -65
- package/src/builder/arrange.ts +23 -22
- package/src/builder/assistant.ts +39 -38
- package/src/builder/automations.ts +32 -31
- package/src/builder/availability.ts +24 -24
- package/src/builder/backups.ts +30 -31
- package/src/builder/blueprints.ts +62 -61
- package/src/builder/create-page.ts +52 -50
- package/src/builder/cross-app.ts +37 -35
- package/src/builder/dataload.ts +2 -2
- package/src/builder/datasets.ts +107 -106
- package/src/builder/dependencies.ts +20 -19
- package/src/builder/designer.ts +36 -36
- package/src/builder/diagnostics.ts +58 -57
- package/src/builder/features.ts +39 -35
- package/src/builder/feedback.ts +23 -20
- package/src/builder/find-workspace.ts +28 -24
- package/src/builder/form-designer.ts +36 -35
- package/src/builder/gallery.ts +25 -29
- package/src/builder/globalization.ts +50 -49
- package/src/builder/i18n/de.ts +3453 -0
- package/src/builder/i18n/en.ts +3453 -0
- package/src/builder/i18n/es.ts +3453 -0
- package/src/builder/i18n/fr.ts +3453 -0
- package/src/builder/i18n/it.ts +3453 -0
- package/src/builder/i18n/ja.ts +3453 -0
- package/src/builder/i18n/ko.ts +3453 -0
- package/src/builder/i18n/nl.ts +3453 -0
- package/src/builder/i18n/pt.ts +3453 -0
- package/src/builder/i18n/zh-cn.ts +3453 -0
- package/src/builder/i18n/zh-tw.ts +3453 -0
- package/src/builder/i18n.ts +17 -3
- package/src/builder/json-sources.ts +63 -62
- package/src/builder/layouts.ts +12 -11
- package/src/builder/locks.ts +29 -28
- package/src/builder/login-page.ts +26 -25
- package/src/builder/mail.ts +148 -147
- package/src/builder/newapp.ts +15 -12
- package/src/builder/plugins.ts +47 -44
- package/src/builder/process-jobs.ts +11 -10
- package/src/builder/pwa.ts +22 -21
- package/src/builder/remote-databases.ts +55 -56
- package/src/builder/report-settings.ts +1 -1
- package/src/builder/reporter.ts +43 -42
- package/src/builder/rest-catalogs.ts +87 -86
- package/src/builder/rest.ts +9 -8
- package/src/builder/routes.ts +27 -27
- package/src/builder/search.ts +14 -13
- package/src/builder/static-files.ts +31 -32
- package/src/builder/subscriptions.ts +47 -45
- package/src/builder/supporting.ts +23 -18
- package/src/builder/team.ts +179 -179
- package/src/builder/telemetry.ts +59 -62
- package/src/builder/templates.ts +66 -65
- package/src/builder/themeroller.ts +60 -63
- package/src/builder/ui-defaults.ts +21 -20
- package/src/builder/ui.ts +2 -1
- package/src/builder/upgrade.ts +25 -24
- package/src/builder/utilities.ts +48 -47
- package/src/builder/vector-providers.ts +67 -66
- package/src/builder/websources.ts +43 -42
- package/src/builder/wizards.ts +95 -89
- package/src/builder/workflow-migrate.ts +30 -31
- package/src/builder/workflows.ts +40 -39
- package/src/builder/workingcopies.ts +41 -49
- package/src/builder/workspace-backups.ts +21 -20
- package/src/builder/workspace-mail.ts +17 -16
- package/src/builder/workspace-resources.ts +104 -112
- package/src/builder/workspace-transfer.ts +34 -26
package/src/builder/designer.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { icon } from '../icons.ts';
|
|
|
7
7
|
import { COMPONENTS } from './components.ts';
|
|
8
8
|
import { back, BASE, bicon, csrf, designerUrl, developer, flash, input, select, send, shell, type Req } from './ui.ts';
|
|
9
9
|
import { wantsHtml } from './errors.ts';
|
|
10
|
-
import { tr } from './i18n.ts';
|
|
10
|
+
import { scopedTx as tx, tr } from './i18n.ts';
|
|
11
11
|
import type { Session } from '../session.ts';
|
|
12
12
|
import { buildOptionChoices, componentForm, lookups, saveComponent } from './forms.ts';
|
|
13
13
|
import { regionConfigHelp } from './region-help.ts';
|
|
@@ -299,20 +299,20 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
299
299
|
</div>
|
|
300
300
|
</div>`;
|
|
301
301
|
const help = html`<div class="pd-help">
|
|
302
|
-
<h3
|
|
303
|
-
<p
|
|
302
|
+
<h3>${tx('pdz.layout')}</h3>
|
|
303
|
+
<p>${tx.rich('pdz.grid_help', { span: html`<em>${tx('pdz.column_span')}</em>` })}</p>
|
|
304
304
|
<ul>
|
|
305
|
-
<li
|
|
306
|
-
<li
|
|
307
|
-
<li
|
|
308
|
-
<li
|
|
309
|
-
<li
|
|
305
|
+
<li>${tx('pdz.drag_a_region_by_its')}</li>
|
|
306
|
+
<li>${tx('pdz.drag_items_and_buttons_to')}</li>
|
|
307
|
+
<li>${tx('pdz.drag_a_region_item_or')}</li>
|
|
308
|
+
<li>${tx.rich('pdz.keyboard_help', { move: html`<kbd>${tx('pdz.alt')}</kbd>+<kbd>↑</kbd> / <kbd>↓</kbd>`, resize: html`<kbd>${tx('pdz.alt')}</kbd>+<kbd>${tx('pdz.shift')}</kbd>+<kbd>←</kbd> / <kbd>→</kbd>`, arrange: html`<strong>${tx('pdz.arrange')}</strong>` })}</li>
|
|
309
|
+
<li>${tx.rich('pdz.undo_help', { undo: html`<strong>${tx('pdz.undo')}</strong>`, redo: html`<strong>${tx('pdz.redo')}</strong>` })}</li>
|
|
310
310
|
</ul>
|
|
311
|
-
<h3
|
|
312
|
-
<p
|
|
313
|
-
<p
|
|
314
|
-
<p
|
|
315
|
-
<p
|
|
311
|
+
<h3>${tx('pdz.cheat_sheet')}</h3>
|
|
312
|
+
<p>${tx.code('pdz.p1_item_binds_an_item')}</p>
|
|
313
|
+
<p>${tx.code('pdz.p1_item_substitutes_into_titles')}</p>
|
|
314
|
+
<p>${tx.code('pdz.in_sql_pl_pgsql_and')}</p>
|
|
315
|
+
<p>${tx.code('pdz.a_pl_pgsql_raise_exception')}</p>
|
|
316
316
|
</div>`;
|
|
317
317
|
const pageSearch = html`<form method="get" action="${BASE}/apps/${p.app_id}/search" class="pd-search" role="search">
|
|
318
318
|
<label class="label" for="pd-q">${tx('pd.search_label')}</label>
|
|
@@ -388,34 +388,34 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
388
388
|
heading = html`${icon('file')}<span>${tx('pd.page_title', { n: p.page_no, name: p.name })}</span>`;
|
|
389
389
|
props = html`${peTabs([tab('pd-r-props', tx('pd.copy.page'), html`
|
|
390
390
|
<form method="post" action="${BASE}/pages/${p.id}" class="component-form" id="${formId}">${csrf(s)}
|
|
391
|
-
<fieldset class="prop-group"><legend
|
|
392
|
-
${input('page_no', '
|
|
393
|
-
${input('name', '
|
|
394
|
-
${input('title', '
|
|
391
|
+
<fieldset class="prop-group"><legend>${tx('pdz.identification')}</legend><div class="form-grid">
|
|
392
|
+
${input('page_no', tx('pdz.page_number'), p.page_no, { type: 'number', required: true })}
|
|
393
|
+
${input('name', tx('pdz.name'), p.name, { required: true })}
|
|
394
|
+
${input('title', tx('pdz.title'), p.title, { help: tx('pdz.title_help') })}
|
|
395
395
|
</div></fieldset>
|
|
396
|
-
<fieldset class="prop-group"><legend
|
|
397
|
-
${select('mode', '
|
|
398
|
-
${select('page_group', '
|
|
399
|
-
${select('page_template', '
|
|
400
|
-
${select('dialog_position', '
|
|
401
|
-
${select('dialog_size', '
|
|
402
|
-
${input('static_includes', '
|
|
403
|
-
<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="warn_unsaved" value="true"${p.warn_unsaved ? raw(' checked') : ''}>
|
|
404
|
-
<small class="help"
|
|
405
|
-
${select('parent_page', '
|
|
396
|
+
<fieldset class="prop-group"><legend>${tx('pdz.appearance')}</legend><div class="form-grid">
|
|
397
|
+
${select('mode', tx('pdz.page_mode'), p.mode, [['normal', tx('pdz.normal')], ['modal', tx('pdz.modal_dialog')]])}
|
|
398
|
+
${select('page_group', tx('pdz.page_group'), p.page_group ?? '', [['', tx('pdz.none')], ...lk.pageGroups.map((g): [string, string] => [g, g]), ...(p.page_group && !lk.pageGroups.includes(p.page_group) ? [[p.page_group, `${p.page_group} (no such group)`] as [string, string]] : [])], tx('pdz.organises_the_pages_in_the'))}
|
|
399
|
+
${select('page_template', tx('pdz.page_template'), p.page_template ?? 'standard', [['standard', tx('pdz.standard')], ['left_side_column', tx('pdz.left_side_column')], ['right_side_column', tx('pdz.right_side_column')], ['minimal', tx('pdz.minimal_no_navigation')]], tx('pdz.side_column_regions_with_position'))}
|
|
400
|
+
${select('dialog_position', tx('pdz.dialog_position'), p.dialog_position ?? 'center', [['center', tx('pdz.centred_dialog')], ['right', tx('pdz.drawer_from_the_right')], ['left', tx('pdz.drawer_from_the_left')], ['top', tx('pdz.drawer_from_the_top')], ['bottom', tx('pdz.drawer_from_the_bottom')]], tx('pdz.modal_pages_only_on_phones'))}
|
|
401
|
+
${select('dialog_size', tx('pdz.dialog_size'), p.dialog_size ?? 'medium', [['small', tx('pdz.small')], ['medium', tx('pdz.medium')], ['large', tx('pdz.large')]], tx('pdz.the_width_the_height_of'))}
|
|
402
|
+
${input('static_includes', tx('pdz.javascript_and_css_files'), (p.static_includes ?? []).join(', '), { placeholder: tx('pdz.e_g_page_3_js'), help: tx('pdz.static_application_files_js_css') })}
|
|
403
|
+
<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="warn_unsaved" value="true"${p.warn_unsaved ? raw(' checked') : ''}> ${tx('pdz.warn_on_unsaved_changes')}</label>
|
|
404
|
+
<small class="help">${tx('pdz.ask_before_the_user_leaves')}</small></div>
|
|
405
|
+
${select('parent_page', tx('pdz.breadcrumb_parent'), p.parent_page ?? '', [['', tx('pdz.none')], ...lk.pages.filter((x) => x.page_no !== p.page_no).map((x): [string, string] => [String(x.page_no), `${x.page_no}. ${x.name}`])])}
|
|
406
406
|
</div></fieldset>
|
|
407
|
-
<fieldset class="prop-group"><legend
|
|
408
|
-
<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="requires_auth" value="true"${p.requires_auth ? raw(' checked') : ''}>
|
|
409
|
-
${select('authz', '
|
|
410
|
-
${select('protection', '
|
|
407
|
+
<fieldset class="prop-group"><legend>${tx('pdz.security')}</legend><div class="form-grid">
|
|
408
|
+
<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="requires_auth" value="true"${p.requires_auth ? raw(' checked') : ''}> ${tx('pdz.requires_authentication')}</label></div>
|
|
409
|
+
${select('authz', tx('pdz.authorization_scheme'), p.authz ?? '', [['', tx('pdz.none')], ...['MUST_NOT_BE_PUBLIC_USER', ...lk.authz].flatMap((n): [string, string][] => [[n, n], [`!${n}`, tx('pdz.not_authz', { name: n })]])])}
|
|
410
|
+
${select('protection', tx('pdz.page_access_protection'), p.protection, [['checksum', tx('pdz.arguments_must_have_checksum')], ['unrestricted', tx('pdz.unrestricted')]],
|
|
411
411
|
'With checksum, item values in the URL (?P3_ID=…) are only accepted from links the runtime generated.')}
|
|
412
|
-
${select('build_option', '
|
|
412
|
+
${select('build_option', tx('pdz.build_option'), p.build_option ?? '', buildOptionChoices(lk, p.build_option),
|
|
413
413
|
'While the option is excluded (or "Not": included) the page does not exist in the running application.')}
|
|
414
414
|
</div></fieldset>
|
|
415
|
-
<fieldset class="prop-group"><legend
|
|
416
|
-
<div class="field" data-wide><label class="label" for="f_help"
|
|
415
|
+
<fieldset class="prop-group"><legend>${tx('pdz.help')}</legend><div class="form-grid">
|
|
416
|
+
<div class="field" data-wide><label class="label" for="f_help">${tx('pdz.help_text')}</label>
|
|
417
417
|
<textarea id="f_help" name="help" rows="5" maxlength="20000">${p.help ?? ''}</textarea>
|
|
418
|
-
<small class="help"
|
|
418
|
+
<small class="help">${tx('pdz.markdown_with_amp_item_substitutions')}</small></div>
|
|
419
419
|
</div></fieldset>
|
|
420
420
|
<div class="buttons"><button class="btn btn-hot">${tx('pd.save_page')}</button></div>
|
|
421
421
|
</form>`, true)])}
|
|
@@ -494,7 +494,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
494
494
|
<section class="pd-pane pd-center tab-panel" id="pd-p-layout" data-tab${pane === 'pd-p-layout' ? raw(' data-tab-active') : ''}><h2 class="tab-title pd-pane-title"><span>${tx('pd.layout')}</span></h2>${center}</section>
|
|
495
495
|
<section class="pd-pane pd-right tab-panel" id="pd-p-props" data-tab${pane === 'pd-p-props' ? raw(' data-tab-active') : ''}><h2 class="tab-title pd-pane-title"><span>${tx('pd.properties')}</span></h2>${right}</section>
|
|
496
496
|
</div>`;
|
|
497
|
-
return send(reply, s, shell(s, tx('pd.page_n_title', { n: p.page_no }), [['
|
|
497
|
+
return send(reply, s, shell(s, tx('pd.page_n_title', { n: p.page_no }), [[tx('pdz.app_builder'), BASE], [p.app_name, `${BASE}/apps/${p.app_id}`], [tx('pd.page_title', { n: p.page_no, name: p.name })]], main, 'apps', { toolbar, full: true }));
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
// (sprint 59) Quick edit from the developer toolbar: an item or button of the page by its name
|
|
@@ -8,6 +8,7 @@ import { icon } from '../icons.ts';
|
|
|
8
8
|
import { DEBUG_LEVELS } from '../debug.ts';
|
|
9
9
|
import { isAdmin } from './locks.ts';
|
|
10
10
|
import { appHeader, back, BASE, csrf, developer, flash, region, select, send, shell, type Req } from './ui.ts';
|
|
11
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
11
12
|
|
|
12
13
|
// App Builder → Activity → Debug messages (APEX: Debug Messages): the page
|
|
13
14
|
// views an application recorded while its debug level was on, and the timed
|
|
@@ -66,46 +67,46 @@ export async function diagnosticsRoutes(app: FastifyInstance) {
|
|
|
66
67
|
};
|
|
67
68
|
const main = html`${appHeader(a, 'activity', s)}
|
|
68
69
|
<div class="columns">
|
|
69
|
-
${region('
|
|
70
|
+
${region(tx('diag.debug_level'), html`
|
|
70
71
|
<p class="muted u-mt0">${a.debug_level
|
|
71
|
-
? html`<b
|
|
72
|
-
:
|
|
72
|
+
? tx.rich('diag.on', { state: html`<b>${tx('diag.on_state', { level: a.debug_level })}</b>`, fn: html`<code>meta.debug(level, text)</code>` })
|
|
73
|
+
: tx.rich('diag.off', { fn: html`<code>meta.debug(level, text)</code>` })}</p>
|
|
73
74
|
<form method="post" action="${BASE}/apps/${a.id}/debug/settings">${csrf(s)}
|
|
74
75
|
<div class="form-grid">
|
|
75
|
-
${select('debug_level', '
|
|
76
|
-
${select('debug_retention_days', '
|
|
76
|
+
${select('debug_level', tx('diag.debug_level'), String(a.debug_level), DEBUG_LEVELS.map(([n, label]): [string, string] => [String(n), label]), tx('diag.level_9_records_posted_item'))}
|
|
77
|
+
${select('debug_retention_days', tx('diag.keep_messages_for'), String(a.debug_retention_days), [1, 2, 7, 14, 30, 90].map((d): [string, string] => [String(d), `${d} day${d === 1 ? '' : 's'}`]))}
|
|
77
78
|
</div>
|
|
78
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
79
|
+
<div class="buttons"><button class="btn btn-hot">${tx('diag.save')}</button></div>
|
|
79
80
|
</form>`)}
|
|
80
|
-
${region('
|
|
81
|
+
${region(tx('diag.page_views'), html`
|
|
81
82
|
<form method="get" action="${BASE}/apps/${a.id}/debug" class="u-mb1">
|
|
82
83
|
<div class="form-grid">
|
|
83
|
-
<div class="field"><label class="label" for="f_page"
|
|
84
|
-
<div class="field"><label class="label" for="f_user"
|
|
84
|
+
<div class="field"><label class="label" for="f_page">${tx('diag.page')}</label><input id="f_page" name="page" type="number" min="1" value="${pageNo ?? ''}"></div>
|
|
85
|
+
<div class="field"><label class="label" for="f_user">${tx('diag.user')}</label><input id="f_user" name="user" value="${user ?? ''}"></div>
|
|
85
86
|
</div>
|
|
86
|
-
<div class="field"><label class="check"><input type="checkbox" name="problems" value="1"${problems ? raw(' checked') : ''}>
|
|
87
|
-
<div class="buttons"><button class="btn">${icon('search')}
|
|
87
|
+
<div class="field"><label class="check"><input type="checkbox" name="problems" value="1"${problems ? raw(' checked') : ''}> ${tx('diag.only_with_errors_or_warnings')}</label></div>
|
|
88
|
+
<div class="buttons"><button class="btn">${icon('search')} ${tx('diag.filter')}</button></div>
|
|
88
89
|
</form>
|
|
89
90
|
${views.length
|
|
90
|
-
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th
|
|
91
|
+
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('diag.when')}</th><th class="num">${tx('diag.page')}</th><th>${tx('diag.request')}</th><th>${tx('diag.user')}</th><th class="num">${tx('diag.status')}</th><th class="num">ms</th><th class="num">${tx('diag.entries')}</th><th></th></tr></thead>
|
|
91
92
|
<tbody>${views.map((v) => html`<tr>
|
|
92
|
-
<td data-label="
|
|
93
|
-
<td class="num" data-label="
|
|
94
|
-
<td data-label="
|
|
95
|
-
<td data-label="
|
|
96
|
-
<td class="num" data-label="
|
|
93
|
+
<td data-label="${tx('diag.when')}">${String(v.started_at).slice(0, 19)}</td>
|
|
94
|
+
<td class="num" data-label="${tx('diag.page')}">${v.page_no ?? ''}${v.page_name ? html` <span class="muted">${v.page_name}</span>` : ''}</td>
|
|
95
|
+
<td data-label="${tx('diag.request')}"><code>${v.method} ${v.path}</code></td>
|
|
96
|
+
<td data-label="${tx('diag.user')}">${v.username ?? ''}</td>
|
|
97
|
+
<td class="num" data-label="${tx('diag.status')}">${v.status >= 400 ? html`<span class="ev ev-error">${v.status}</span>` : v.status}</td>
|
|
97
98
|
<td class="num" data-label="ms">${v.ms}</td>
|
|
98
|
-
<td class="num" data-label="
|
|
99
|
-
<td><a href="${BASE}/apps/${a.id}/debug/${v.id}"
|
|
100
|
-
${more ? html`<p><a class="btn" href="?${query({ before: String(views[views.length - 1].id) })}"
|
|
99
|
+
<td class="num" data-label="${tx('diag.entries')}">${v.entries}${v.problems ? html` <span class="ev ev-error">${v.problems} problem${v.problems === 1 ? '' : 's'}</span>` : ''}</td>
|
|
100
|
+
<td><a href="${BASE}/apps/${a.id}/debug/${v.id}">${tx('diag.view')}</a></td></tr>`)}</tbody></table></div>
|
|
101
|
+
${more ? html`<p><a class="btn" href="?${query({ before: String(views[views.length - 1].id) })}">${tx('diag.older_page_views')}</a></p>` : ''}`
|
|
101
102
|
: html`<p class="muted">No page views recorded${pageNo !== null || user || problems ? ' for this filter' : ''}.</p>`}
|
|
102
|
-
<p class="muted">${total} page view${total === 1 ? '' : 's'} kept for ${a.debug_retention_days} day${a.debug_retention_days === 1 ? '' : 's'} (at most 5000). <a href="${BASE}/apps/${a.id}/activity"
|
|
103
|
+
<p class="muted">${total} page view${total === 1 ? '' : 's'} kept for ${a.debug_retention_days} day${a.debug_retention_days === 1 ? '' : 's'} (at most 5000). <a href="${BASE}/apps/${a.id}/activity">${tx('diag.back_to_activity')}</a></p>
|
|
103
104
|
${total
|
|
104
105
|
? html`<form method="post" action="${BASE}/apps/${a.id}/debug/purge">${csrf(s)}
|
|
105
|
-
<button class="btn btn-danger" data-confirm="
|
|
106
|
+
<button class="btn btn-danger" data-confirm="${tx('diag.delete_confirm', { app: a.name })}">${icon('close')} ${tx('diag.delete_all_debug_messages')}</button></form>`
|
|
106
107
|
: ''}`)}
|
|
107
108
|
</div>`;
|
|
108
|
-
return send(reply, s, shell(s,
|
|
109
|
+
return send(reply, s, shell(s, tx('diag.title', { app: a.name }), [[tx('diag.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('diag.activity'), `${BASE}/apps/${a.id}/activity`], [tx('diag.debug_messages')]], main));
|
|
109
110
|
});
|
|
110
111
|
|
|
111
112
|
app.get(`${BASE}/apps/:id/debug/:view`, async (req: Req, reply) => {
|
|
@@ -132,29 +133,29 @@ export async function diagnosticsRoutes(app: FastifyInstance) {
|
|
|
132
133
|
).rows;
|
|
133
134
|
const slowest = Math.max(0, ...entries.map((e) => e.dur ?? 0));
|
|
134
135
|
const main = html`${appHeader(a, 'activity', s)}
|
|
135
|
-
${region(
|
|
136
|
+
${region(tx('diag.page_view', { id: v.id }), html`
|
|
136
137
|
<div class="stat-grid">
|
|
137
|
-
<div class="stat"><b>${v.ms ?? '?'} ms</b><span
|
|
138
|
+
<div class="stat"><b>${v.ms ?? '?'} ms</b><span>${tx('diag.total_time')}</span></div>
|
|
138
139
|
<div class="stat"><b>${v.page_no ?? '-'}</b><span>${v.page_name ?? 'page'}</span></div>
|
|
139
|
-
<div class="stat"><b>${v.status ?? '?'}</b><span
|
|
140
|
+
<div class="stat"><b>${v.status ?? '?'}</b><span>${tx('diag.http_status')}</span></div>
|
|
140
141
|
<div class="stat"><b>${v.entries}</b><span>entries (level ${v.level})</span></div>
|
|
141
142
|
</div>
|
|
142
143
|
<p class="muted u-mt0"><code>${v.method} ${v.path}</code> · ${v.username ?? ''} · ${String(v.started_at).slice(0, 23)}</p>
|
|
143
144
|
<p class="seg">
|
|
144
|
-
${v.prev ? html`<a href="${BASE}/apps/${a.id}/debug/${v.prev}"
|
|
145
|
-
<a href="${BASE}/apps/${a.id}/debug"
|
|
145
|
+
${v.prev ? html`<a href="${BASE}/apps/${a.id}/debug/${v.prev}">${tx('diag.previous')}</a>` : ''}
|
|
146
|
+
<a href="${BASE}/apps/${a.id}/debug">${tx('diag.all_page_views')}</a>
|
|
146
147
|
${v.page_no ? html`<a href="${BASE}/apps/${a.id}/debug?page=${v.page_no}">Page ${v.page_no} only</a>` : ''}
|
|
147
|
-
${v.next ? html`<a href="${BASE}/apps/${a.id}/debug/${v.next}"
|
|
148
|
+
${v.next ? html`<a href="${BASE}/apps/${a.id}/debug/${v.next}">${tx('diag.next')}</a>` : ''}
|
|
148
149
|
</p>
|
|
149
|
-
<div class="table-wrap"><table class="report report-reflow debug-log"><thead><tr><th class="num"
|
|
150
|
+
<div class="table-wrap"><table class="report report-reflow debug-log"><thead><tr><th class="num">${tx('diag.elapsed_ms')}</th><th class="num">${tx('diag.duration_ms')}</th><th class="num">${tx('diag.until_next')}</th><th>${tx('diag.level')}</th><th>${tx('diag.component')}</th><th>${tx('diag.message')}</th></tr></thead>
|
|
150
151
|
<tbody>${entries.map((e) => html`<tr${e.dur && e.dur === slowest && slowest > 0 ? raw(' class="debug-slowest"') : ''}>
|
|
151
|
-
<td class="num" data-label="
|
|
152
|
-
<td class="num" data-label="
|
|
153
|
-
<td class="num" data-label="
|
|
154
|
-
<td data-label="
|
|
155
|
-
<td data-label="
|
|
156
|
-
<td data-label="
|
|
157
|
-
return send(reply, s, shell(s,
|
|
152
|
+
<td class="num" data-label="${tx('diag.elapsed_ms')}">${e.ms}</td>
|
|
153
|
+
<td class="num" data-label="${tx('diag.duration_ms')}">${e.dur ?? ''}</td>
|
|
154
|
+
<td class="num" data-label="${tx('diag.until_next')}">${e.gap ?? ''}</td>
|
|
155
|
+
<td data-label="${tx('diag.level')}">${levelTag(e.level)}</td>
|
|
156
|
+
<td data-label="${tx('diag.component')}">${e.component ?? ''}</td>
|
|
157
|
+
<td data-label="${tx('diag.message')}" class="debug-text">${e.message}</td></tr>`)}</tbody></table></div>`)}`;
|
|
158
|
+
return send(reply, s, shell(s, tx('diag.view_title', { id: v.id, app: a.name }), [[tx('diag.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('diag.debug_messages'), `${BASE}/apps/${a.id}/debug`], [tx('diag.page_view', { id: v.id })]], main));
|
|
158
159
|
});
|
|
159
160
|
|
|
160
161
|
app.post(`${BASE}/apps/:id/debug/settings`, async (req: Req, reply) => {
|
|
@@ -165,11 +166,11 @@ export async function diagnosticsRoutes(app: FastifyInstance) {
|
|
|
165
166
|
const level = Number(req.body?.debug_level);
|
|
166
167
|
const days = Number(req.body?.debug_retention_days);
|
|
167
168
|
if (!DEBUG_LEVELS.some(([n]) => n === level) || !Number.isInteger(days) || days < 1 || days > 90) {
|
|
168
|
-
flash(s, '
|
|
169
|
+
flash(s, tx('diag.choose_a_debug_level_and'), 'error');
|
|
169
170
|
return back(reply, s, `${BASE}/apps/${a.id}/debug`);
|
|
170
171
|
}
|
|
171
172
|
await owner.query('update meta.app set debug_level = $2, debug_retention_days = $3, updated_at = now() where id = $1', [a.id, level, days]);
|
|
172
|
-
flash(s, level ?
|
|
173
|
+
flash(s, level ? tx('diag.on_flash', { level }) : tx('diag.debug_messages_off'));
|
|
173
174
|
return back(reply, s, `${BASE}/apps/${a.id}/debug`);
|
|
174
175
|
});
|
|
175
176
|
|
|
@@ -188,8 +189,8 @@ export async function diagnosticsRoutes(app: FastifyInstance) {
|
|
|
188
189
|
const s = await developer(req, reply);
|
|
189
190
|
if (!s) return;
|
|
190
191
|
if (!(await isAdmin(s.username))) {
|
|
191
|
-
const main = html`<h1 class="u-mb1"
|
|
192
|
-
return send(reply.code(403), s, shell(s, '
|
|
192
|
+
const main = html`<h1 class="u-mb1">${tx('diag.installation')}</h1><div class="alert alert-error" role="alert">${tx('diag.only_administrators_see_the_installation')}</div>`;
|
|
193
|
+
return send(reply.code(403), s, shell(s, tx('diag.installation'), [[tx('diag.app_builder'), BASE], [tx('diag.workspace_utilities'), `${BASE}/utilities`], [tx('diag.installation')]], main));
|
|
193
194
|
}
|
|
194
195
|
const has = async (t: string) => !!(await owner.one('select to_regclass($1) is not null as ok', [t]))?.ok;
|
|
195
196
|
const [runs, migrations, examples] = await Promise.all([
|
|
@@ -203,33 +204,33 @@ export async function diagnosticsRoutes(app: FastifyInstance) {
|
|
|
203
204
|
const pending = files.filter((f) => !applied.has(f));
|
|
204
205
|
const unknown = migrations.filter((m) => !files.includes(m.name));
|
|
205
206
|
const db = await owner.one(`select current_database() as name, split_part(version(), ' on ', 1) as version, pg_size_pretty(pg_database_size(current_database())) as size`);
|
|
206
|
-
const main = html`<h1 class="u-mb1"
|
|
207
|
+
const main = html`<h1 class="u-mb1">${tx('diag.installation')}</h1>
|
|
207
208
|
<div class="stat-grid">
|
|
208
|
-
<div class="stat"><b>${VERSION}</b><span
|
|
209
|
-
<div class="stat"><b>${migrations.length}</b><span
|
|
210
|
-
<div class="stat"><b>${pending.length}</b><span
|
|
209
|
+
<div class="stat"><b>${VERSION}</b><span>${tx('diag.pgkiln_version_of_this_server')}</span></div>
|
|
210
|
+
<div class="stat"><b>${migrations.length}</b><span>${migrations[0] ? tx('diag.applied_latest', { name: migrations[0].name.replace(/\.sql$/, '') }) : tx('diag.applied_n')}</span></div>
|
|
211
|
+
<div class="stat"><b>${pending.length}</b><span>${tx('diag.migrations_not_applied')}</span></div>
|
|
211
212
|
<div class="stat"><b>${db.size}</b><span>${db.name} · ${db.version}</span></div>
|
|
212
213
|
</div>
|
|
213
214
|
${pending.length
|
|
214
|
-
? html`<div class="alert alert-error" role="alert"
|
|
215
|
+
? html`<div class="alert alert-error" role="alert">${tx.rich(pending.length === 1 ? 'diag.missing_1' : 'diag.missing', { n: pending.length, list: pending.join(', '), npm: html`<code>npm run db:migrate</code>`, cli: html`<code>pgkiln migrate</code>` })}</div>`
|
|
215
216
|
: ''}
|
|
216
|
-
${unknown.length ? html`<div class="alert alert-error" role="alert"
|
|
217
|
+
${unknown.length ? html`<div class="alert alert-error" role="alert">${tx('diag.unknown', { list: unknown.map((m) => m.name).join(', ') })}</div>` : ''}
|
|
217
218
|
<div class="columns">
|
|
218
|
-
${region('
|
|
219
|
-
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th
|
|
219
|
+
${region(tx('diag.install_and_upgrade_runs'), runs.length
|
|
220
|
+
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('diag.started')}</th><th>${tx('diag.kind')}</th><th>${tx('diag.version')}</th><th>${tx('diag.status')}</th><th class="num">${tx('diag.files')}</th><th>${tx('diag.applied')}</th><th>${tx('diag.by')}</th></tr></thead>
|
|
220
221
|
<tbody>${runs.map((r) => html`<tr>
|
|
221
|
-
<td data-label="
|
|
222
|
-
<td data-label="
|
|
223
|
-
<td class="num" data-label="
|
|
224
|
-
<td data-label="
|
|
225
|
-
: html`<p class="muted u-mt0"
|
|
226
|
-
${region('
|
|
222
|
+
<td data-label="${tx('diag.started')}">${String(r.started_at).slice(0, 19)}</td><td data-label="${tx('diag.kind')}">${r.kind}</td><td data-label="${tx('diag.version')}">${r.version ?? ''}</td>
|
|
223
|
+
<td data-label="${tx('diag.status')}">${r.status === 'ok' ? html`<span class="ev ev-login">ok</span>` : html`<span class="ev ev-error">failed</span>`}${r.error ? html`<div class="small">${r.error}</div>` : ''}</td>
|
|
224
|
+
<td class="num" data-label="${tx('diag.files')}">${r.applied.length}</td>
|
|
225
|
+
<td data-label="${tx('diag.applied')}" class="debug-text">${r.applied.join(', ')}</td><td data-label="${tx('diag.by')}">${r.db_user}</td></tr>`)}</tbody></table></div>`
|
|
226
|
+
: html`<p class="muted u-mt0">${tx.code('diag.no_runs_recorded_yet_runs')}</p>`)}
|
|
227
|
+
${region(tx('diag.applied_migrations'), html`<div class="table-wrap"><table class="report"><thead><tr><th>${tx('diag.file')}</th><th>${tx('diag.applied')}</th></tr></thead>
|
|
227
228
|
<tbody>${migrations.map((m) => html`<tr><td><code>${m.name}</code></td><td>${String(m.applied_at).slice(0, 19)}</td></tr>`)}</tbody></table></div>`)}
|
|
228
229
|
${examples.length
|
|
229
|
-
? region('
|
|
230
|
+
? region(tx('diag.example_and_seed_scripts'), html`<div class="table-wrap"><table class="report"><thead><tr><th>${tx('diag.file')}</th><th>${tx('diag.applied')}</th></tr></thead>
|
|
230
231
|
<tbody>${examples.map((m) => html`<tr><td><code>${m.name}</code></td><td>${String(m.applied_at).slice(0, 19)}</td></tr>`)}</tbody></table></div>`)
|
|
231
232
|
: ''}
|
|
232
233
|
</div>`;
|
|
233
|
-
return send(reply, s, shell(s, '
|
|
234
|
+
return send(reply, s, shell(s, tx('diag.installation'), [[tx('diag.app_builder'), BASE], [tx('diag.workspace_utilities'), `${BASE}/utilities`], [tx('diag.installation')]], main));
|
|
234
235
|
});
|
|
235
236
|
}
|
package/src/builder/features.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { appOr404 } from './forms.ts';
|
|
|
8
8
|
import { createLoginPage } from './login-page.ts';
|
|
9
9
|
import { ABOUT_PAGE, ACTIVITY_PAGE, addAboutPage, addAccessSchemes, addActivityPage, ensureAdminScheme } from './newapp.ts';
|
|
10
10
|
import { appHeader, back, BASE, csrf, developer, flash, region, send, shell, type Req } from './ui.ts';
|
|
11
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
11
12
|
|
|
12
13
|
// (sprint 137, AP-3) Add feature (APEX: Create a Page → Feature, and App home → Tasks → Add Feature Page):
|
|
13
14
|
// the Create Application wizard's features (newapp.ts) and a few more, added to an existing application.
|
|
@@ -67,8 +68,8 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
67
68
|
icon: 'lock',
|
|
68
69
|
help: 'Authorization schemes for the roles admin, contributor and reader, to put on pages, regions and buttons.',
|
|
69
70
|
adds: () => [
|
|
70
|
-
'
|
|
71
|
-
'
|
|
71
|
+
tx('feat.a.access_1'),
|
|
72
|
+
tx('feat.a.access_2'),
|
|
72
73
|
],
|
|
73
74
|
installed: async (c, a) => (await c.query(`select count(*)::int as n from meta.authz_scheme where app_id = $1 and name in ('CONTRIBUTOR', 'READER')`, [a.id])).rows[0].n === 2,
|
|
74
75
|
install: async (c, a) => {
|
|
@@ -82,12 +83,12 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
82
83
|
icon: 'activity',
|
|
83
84
|
help: 'A page for administrators with the page views per day and the latest activity.',
|
|
84
85
|
adds: () => [
|
|
85
|
-
|
|
86
|
-
'
|
|
87
|
-
'
|
|
86
|
+
tx('feat.a.activity_1', { page: ACTIVITY_PAGE }),
|
|
87
|
+
tx('feat.a.activity_2'),
|
|
88
|
+
tx('feat.a.activity_3'),
|
|
88
89
|
],
|
|
89
90
|
installed: (c, a) => pageHas(c, a.id, ACTIVITY_PAGE, 'meta.app_activity'),
|
|
90
|
-
blocked: async (c, a) => ((await pageExists(c, a.id, ACTIVITY_PAGE)) ?
|
|
91
|
+
blocked: async (c, a) => ((await pageExists(c, a.id, ACTIVITY_PAGE)) ? tx('feat.page_in_use', { page: ACTIVITY_PAGE }) : null),
|
|
91
92
|
install: async (c, a) => {
|
|
92
93
|
await addActivityPage(c, a.id);
|
|
93
94
|
return pageId(c, a.id, ACTIVITY_PAGE);
|
|
@@ -99,19 +100,19 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
99
100
|
icon: 'sliders',
|
|
100
101
|
help: 'A page where the application\'s administrators switch build options on and off while it runs.',
|
|
101
102
|
adds: () => [
|
|
102
|
-
|
|
103
|
-
'
|
|
104
|
-
'
|
|
103
|
+
tx('feat.a.config_1', { page: CONFIG_PAGE }),
|
|
104
|
+
tx('feat.a.config_2'),
|
|
105
|
+
tx('feat.a.config_3'),
|
|
105
106
|
],
|
|
106
107
|
installed: (c, a) => pageHas(c, a.id, CONFIG_PAGE, 'meta.set_configurable_options'),
|
|
107
|
-
blocked: async (c, a) => ((await pageExists(c, a.id, CONFIG_PAGE)) ?
|
|
108
|
+
blocked: async (c, a) => ((await pageExists(c, a.id, CONFIG_PAGE)) ? tx('feat.page_in_use', { page: CONFIG_PAGE }) : null),
|
|
108
109
|
fields: async (a) => {
|
|
109
110
|
const opts = (await owner.query<{ name: string; description: string | null; status: string; configurable: boolean }>(
|
|
110
111
|
'select name, description, status, configurable from meta.build_option where app_id = $1 order by name', [a.id])).rows;
|
|
111
112
|
return opts.length
|
|
112
|
-
? html`<fieldset class="prop-group"><legend
|
|
113
|
+
? html`<fieldset class="prop-group"><legend>${tx('feat.cbo')}</legend><div class="qb-pick">${opts.map((o) =>
|
|
113
114
|
html`<label class="check"><input type="checkbox" name="options" value="${o.name}"${o.configurable ? raw(' checked') : ''}> ${o.name} <span class="muted small">${o.status}${o.description ? `, ${o.description}` : ''}</span></label>`)}</div></fieldset>`
|
|
114
|
-
: html`<p class="muted"
|
|
115
|
+
: html`<p class="muted">${tx.rich('feat.no_build_options', { configurable: html`<em>${tx('feat.configurable')}</em>` })}</p>`;
|
|
115
116
|
},
|
|
116
117
|
install: async (c, a, b) => {
|
|
117
118
|
const chosen = ([] as unknown[]).concat(b.options ?? []).map(String);
|
|
@@ -146,19 +147,19 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
146
147
|
icon: 'mail',
|
|
147
148
|
help: 'A weekly e-mail to the administrators with the application\'s activity, the rows as a CSV file.',
|
|
148
149
|
adds: () => [
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
150
|
+
tx('feat.a.email_1'),
|
|
151
|
+
tx('feat.a.email_2'),
|
|
152
|
+
tx('feat.a.email_3'),
|
|
152
153
|
],
|
|
153
154
|
installed: async (c, a) => !!(await c.query(`select 1 from meta.automation where app_id = $1 and name = 'EMAIL_REPORTING'`, [a.id])).rowCount,
|
|
154
|
-
fields: async () => html`<div class="field"><label class="label" for="f_report_to"
|
|
155
|
+
fields: async () => html`<div class="field"><label class="label" for="f_report_to">${tx('feat.send_to')}</label>
|
|
155
156
|
<input id="f_report_to" name="report_to" type="text" placeholder="admin@example.com, boss@example.com" autocomplete="off">
|
|
156
|
-
<small class="help"
|
|
157
|
+
<small class="help">${tx('feat.send_to_help')}</small></div>`,
|
|
157
158
|
install: async (c, a, b) => {
|
|
158
159
|
const to = String(b.report_to ?? '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
159
160
|
const bad = to.find((x) => !EMAIL.test(x));
|
|
160
|
-
if (bad) throw new Error(
|
|
161
|
-
if (to.join(', ').length > 4000) throw new Error('
|
|
161
|
+
if (bad) throw new Error(tx('feat.not_email', { address: bad.slice(0, 100) }));
|
|
162
|
+
if (to.join(', ').length > 4000) throw new Error(tx('feat.too_many'));
|
|
162
163
|
await c.query(
|
|
163
164
|
`insert into meta.app_setting (app_id, name, value, description) values ($1, 'EMAIL_REPORT_TO', $2, 'E-mail reporting: the addresses of the weekly activity report (comma separated)')
|
|
164
165
|
on conflict (app_id, name) do nothing`,
|
|
@@ -177,7 +178,7 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
177
178
|
label: 'Feedback',
|
|
178
179
|
icon: 'message-square',
|
|
179
180
|
help: 'Users send feedback from the header; you read it under the application\'s Activity → Feedback.',
|
|
180
|
-
adds: () => [
|
|
181
|
+
adds: () => [tx('feat.a.feedback_1'), tx('feat.a.feedback_2')],
|
|
181
182
|
installed: async (_c, a) => a.feedback,
|
|
182
183
|
install: async (c, a) => {
|
|
183
184
|
await c.query('update meta.app set feedback = true, updated_at = now() where id = $1', [a.id]);
|
|
@@ -189,7 +190,7 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
189
190
|
label: 'About page',
|
|
190
191
|
icon: 'info',
|
|
191
192
|
help: 'An About page in a Help menu of the header.',
|
|
192
|
-
adds: () => [
|
|
193
|
+
adds: () => [tx('feat.a.about_1', { page: ABOUT_PAGE }), tx('feat.a.about_2')],
|
|
193
194
|
installed: (c, a) => pageExists(c, a.id, ABOUT_PAGE),
|
|
194
195
|
install: async (c, a) => {
|
|
195
196
|
await addAboutPage(c, a.id);
|
|
@@ -201,9 +202,9 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
201
202
|
label: 'Login page',
|
|
202
203
|
icon: 'log-in',
|
|
203
204
|
help: 'The application\'s own login page instead of the built-in sign-in page, editable in the Page Designer.',
|
|
204
|
-
adds: () => ['
|
|
205
|
+
adds: () => [tx('feat.a.login_1'), tx('feat.a.login_2')],
|
|
205
206
|
installed: async (_c, a) => a.login_page !== null,
|
|
206
|
-
blocked: async (_c, a) => (a.authentication === 'none' ? '
|
|
207
|
+
blocked: async (_c, a) => (a.authentication === 'none' ? tx('feat.no_signin') : null),
|
|
207
208
|
install: async (c, a) => (await createLoginPage(c, a)).page,
|
|
208
209
|
},
|
|
209
210
|
{
|
|
@@ -211,7 +212,7 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
211
212
|
label: 'Theme style selection',
|
|
212
213
|
icon: 'palette',
|
|
213
214
|
help: 'Users choose the light or the dark theme in their user menu (Appearance).',
|
|
214
|
-
adds: () => ['
|
|
215
|
+
adds: () => [tx('feat.a.theme_1')],
|
|
215
216
|
installed: async (_c, a) => a.theme?.user_choice !== false,
|
|
216
217
|
install: async (c, a) => {
|
|
217
218
|
await c.query(`update meta.app set theme = coalesce(theme, '{}') - 'user_choice', updated_at = now() where id = $1`, [a.id]);
|
|
@@ -223,9 +224,9 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
223
224
|
label: 'Push notifications',
|
|
224
225
|
icon: 'bell',
|
|
225
226
|
help: 'Notifications to the users\' devices, from processes and SQL (meta.send_push); the application becomes installable.',
|
|
226
|
-
adds: () => ['
|
|
227
|
+
adds: () => [tx('feat.a.push_1'), tx('feat.a.push_2'), tx('feat.a.push_3')],
|
|
227
228
|
installed: async (_c, a) => a.pwa_push,
|
|
228
|
-
blocked: async () => (secretKeyConfigured() ? null : '
|
|
229
|
+
blocked: async () => (secretKeyConfigured() ? null : tx('feat.push_needs_key')),
|
|
229
230
|
install: async (c, a) => {
|
|
230
231
|
await c.query('update meta.app set pwa = true, pwa_push = true, updated_at = now() where id = $1', [a.id]);
|
|
231
232
|
return null;
|
|
@@ -235,6 +236,9 @@ export const FEATURE_LIST: Feature[] = [
|
|
|
235
236
|
|
|
236
237
|
const CODE_EMAIL = `select meta.send_activity_report(meta.get_app_setting('EMAIL_REPORT_TO'), 7)`;
|
|
237
238
|
|
|
239
|
+
/** A feature's label and help in the builder language (FEATURE_LIST stays English). */
|
|
240
|
+
export const featLabel = (f: Feature) => tx.dyn(`feat.f.${f.key}`, f.label);
|
|
241
|
+
export const featHelp = (f: Feature) => tx.dyn(`feat.h.${f.key}`, f.help);
|
|
238
242
|
const FEATURES_BY_KEY = new Map(FEATURE_LIST.map((f) => [f.key, f]));
|
|
239
243
|
|
|
240
244
|
async function appRow(id: string | number, c?: Client): Promise<AppRow | null> {
|
|
@@ -269,20 +273,20 @@ export async function featureRoutes(app: FastifyInstance) {
|
|
|
269
273
|
return { installed, blocked: installed ? null : ((await f.blocked?.(c, a)) ?? null) };
|
|
270
274
|
});
|
|
271
275
|
const main = html`${appHeader(a0, 'pages', s)}
|
|
272
|
-
${region(f
|
|
273
|
-
<p class="muted u-mt0">${icon(f.icon)} ${f
|
|
276
|
+
${region(featLabel(f), html`
|
|
277
|
+
<p class="muted u-mt0">${icon(f.icon)} ${featHelp(f)}</p>
|
|
274
278
|
${installed
|
|
275
|
-
? html`<p class="alert alert-info" role="status">${
|
|
279
|
+
? html`<p class="alert alert-info" role="status">${tx('feat.installed_already', { feature: featLabel(f) })}</p>`
|
|
276
280
|
: blocked ? html`<p class="alert alert-error" role="status">${blocked}</p>` : ''}
|
|
277
|
-
<h3>${installed ? '
|
|
281
|
+
<h3>${installed ? tx('feat.it_added') : tx('feat.it_adds')}</h3>
|
|
278
282
|
<ul class="cp-adds">${f.adds(a).map((x) => html`<li>${x}</li>`)}</ul>
|
|
279
283
|
${installed || blocked
|
|
280
|
-
? html`<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page?tab=feature"
|
|
284
|
+
? html`<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page?tab=feature">${tx('feat.back_to_the_features')}</a></div>`
|
|
281
285
|
: html`<form method="post" action="${BASE}/apps/${a.id}/features/${f.key}">${csrf(s)}
|
|
282
286
|
${f.fields ? await f.fields(a) : ''}
|
|
283
|
-
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page?tab=feature"
|
|
287
|
+
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page?tab=feature">${tx('feat.cancel')}</a><button class="btn btn-hot">${tx('feat.add', { feature: featLabel(f).toLowerCase() })}</button></div>
|
|
284
288
|
</form>`}`)}`;
|
|
285
|
-
return send(reply, s, shell(s,
|
|
289
|
+
return send(reply, s, shell(s, tx('feat.title', { feature: featLabel(f), app: a.name }), [[tx('feat.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('feat.create_page'), `${BASE}/apps/${a.id}/create-page?tab=feature`], [featLabel(f)]], main));
|
|
286
290
|
});
|
|
287
291
|
|
|
288
292
|
app.post(`${BASE}/apps/:id/features/:key`, async (req: Req, reply) => {
|
|
@@ -296,14 +300,14 @@ export async function featureRoutes(app: FastifyInstance) {
|
|
|
296
300
|
// one at a time per application, and never twice
|
|
297
301
|
await c.query('select 1 from meta.app where id = $1 for update', [req.params.id]);
|
|
298
302
|
const a = (await appRow(req.params.id, c))!;
|
|
299
|
-
if (await f.installed(c, a)) throw new Error(
|
|
303
|
+
if (await f.installed(c, a)) throw new Error(tx('feat.installed_already', { feature: featLabel(f) }));
|
|
300
304
|
const blocked = await f.blocked?.(c, a);
|
|
301
305
|
if (blocked) throw new Error(blocked);
|
|
302
306
|
return f.install(c, a, b);
|
|
303
307
|
});
|
|
304
308
|
// the push key pair after the commit (as the Create Application wizard does)
|
|
305
309
|
if (f.key === 'push') await appPublicKey(Number(req.params.id)).catch(() => undefined);
|
|
306
|
-
flash(s,
|
|
310
|
+
flash(s, tx('feat.added', { feature: featLabel(f) }));
|
|
307
311
|
return back(reply, s, page ? `${BASE}/pages/${page}` : `${BASE}/apps/${req.params.id}/create-page?tab=feature`);
|
|
308
312
|
} catch (e) {
|
|
309
313
|
flash(s, (e as Error).message, 'error');
|