pgkiln 0.84.0 → 0.86.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/155_mail_available.sql +23 -0
- package/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +14 -5
- package/docs/guide/04-pages-and-regions.md +1 -1
- package/docs/guide/06-processing.md +7 -3
- package/docs/guide/07-dynamic-actions.md +3 -3
- package/docs/guide/09-reference.md +1 -0
- package/docs/guide/12-development.md +10 -1
- package/examples/gallery/README.md +23 -6
- package/examples/gallery/gallery.json +4 -0
- package/examples/gallery/samples/sample-approvals.sql +102 -11
- package/examples/gallery/samples/sample-calendar.sql +40 -5
- package/examples/gallery/samples/sample-dynamic-actions.sql +237 -9
- package/examples/gallery/samples/sample-grids.sql +35 -3
- package/examples/gallery/samples/sample-maps.sql +65 -7
- package/examples/gallery/samples/sample-processes.sql +466 -0
- package/examples/gallery/samples/sample-validations.sql +407 -0
- package/examples/gallery/samples/ux-patterns.sql +154 -14
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_80_notifications.sql +125 -0
- package/examples/hr/hr_81_vector_search_type.sql +33 -0
- package/package.json +1 -1
- package/public/app.css +8 -1
- package/public/app.js +28 -9
- 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/runtime/engine.ts +6 -3
- package/src/runtime/lists.ts +2 -2
- package/src/runtime/push.ts +7 -1
- package/src/runtime/regions.ts +9 -4
|
@@ -5,6 +5,7 @@ import { icon } from '../icons.ts';
|
|
|
5
5
|
import { APPEARANCE_DEFAULT, appStyles, BASE_STYLES, baseStyleOf, FONT_SIZES, FONTS, HEX, ITEM_REF, MAX_STYLES, NAV_APPEARANCES, navAppearance, parseStyle, RADII, STYLE_NAME, type StyleVariant } from '../runtime/styles.ts';
|
|
6
6
|
import { REGION_OPTIONS, BUTTON_OPTIONS } from '../runtime/template-options.ts';
|
|
7
7
|
import { appHeader, back, BASE, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
|
|
8
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
8
9
|
|
|
9
10
|
// App → Settings → Theme Roller: style variants (APEX: theme styles). The
|
|
10
11
|
// styles live in meta.app.theme ("styles", "style", "style_choice"), so they
|
|
@@ -27,12 +28,12 @@ async function updateTheme(id: string, change: (theme: Theme) => Theme | string)
|
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const label = (list: Record<string, { label: string }>, k: string | undefined) => (k && list[k] ? list[k].label : '
|
|
31
|
+
const label = (list: Record<string, { label: string }>, k: string | undefined) => (k && list[k] ? list[k].label : tx('thr.as_base'));
|
|
31
32
|
/** (sprint 117) a style's navigation appearance as shown in the list */
|
|
32
|
-
const appearanceLabel = (k: string | undefined) => (k === APPEARANCE_DEFAULT ? '
|
|
33
|
+
const appearanceLabel = (k: string | undefined) => (k === APPEARANCE_DEFAULT ? tx('thr.default') : k && NAV_APPEARANCES[k] ? NAV_APPEARANCES[k].label : tx('thr.as_app'));
|
|
33
34
|
const appearanceChoices = (forStyle: boolean): [string, string][] => [
|
|
34
|
-
forStyle ? ['', '
|
|
35
|
-
...(forStyle ? [[APPEARANCE_DEFAULT,
|
|
35
|
+
forStyle ? ['', tx('thr.as_the_application')] : ['', tx('thr.default_the_base_style_s')],
|
|
36
|
+
...(forStyle ? [[APPEARANCE_DEFAULT, tx('thr.default_base_style')] as [string, string]] : []),
|
|
36
37
|
...Object.entries(NAV_APPEARANCES).map(([k, v]): [string, string] => [k, v.label]),
|
|
37
38
|
];
|
|
38
39
|
const APPEARANCE_HELP = 'APEX 26.2: Pillar Light puts the header and menu on the page\'s own light surfaces, Pillar Dark makes them one dark column in the header colour; both have a header pillar strip and show the current page as a filled pill.';
|
|
@@ -44,13 +45,13 @@ function styleForm(appId: number, s: Parameters<typeof csrf>[0], st: StyleVarian
|
|
|
44
45
|
const value = (own ? st?.[name] : undefined) ?? (typeof base[name] === 'string' && HEX.test(base[name] as string) ? (base[name] as string) : fallback);
|
|
45
46
|
return html`<div class="field"><label class="label" for="f_${name}">${title}</label>
|
|
46
47
|
<input id="f_${name}" name="${name}" type="color" value="${value}">
|
|
47
|
-
<label class="check"><input type="checkbox" name="${name}_own" value="true"${own ? raw(' checked') : ''}>
|
|
48
|
-
<label class="sub" for="f_${name}_item"
|
|
48
|
+
<label class="check"><input type="checkbox" name="${name}_own" value="true"${own ? raw(' checked') : ''}> ${tx('thr.use_this_colour_else_the')}</label>
|
|
49
|
+
<label class="sub" for="f_${name}_item">${tx('thr.or_the_value_of_an')}</label>
|
|
49
50
|
<input id="f_${name}_item" name="${name}_item" value="${ref}" placeholder="&APP_BRAND_COLOUR." autocomplete="off" spellcheck="false">
|
|
50
|
-
<small class="help"
|
|
51
|
+
<small class="help">${tx('thr.a_dynamic_colour_the_item')}</small></div>`;
|
|
51
52
|
};
|
|
52
53
|
const baseOf = BASE_STYLES[baseStyleOf(base)];
|
|
53
|
-
const choices = (list: Record<string, { label: string }>) => [['', '
|
|
54
|
+
const choices = (list: Record<string, { label: string }>) => [['', tx('thr.as_the_base')] as [string, string], ...Object.entries(list).map(([k, v]): [string, string] => [k, v.label])];
|
|
54
55
|
// the live preview (builder.js): the style form's values as CSS variables on a sample of the application's look
|
|
55
56
|
const lists = JSON.stringify({
|
|
56
57
|
fonts: Object.fromEntries(Object.entries(FONTS).map(([k, v]) => [k, v.css])),
|
|
@@ -59,40 +60,40 @@ function styleForm(appId: number, s: Parameters<typeof csrf>[0], st: StyleVarian
|
|
|
59
60
|
});
|
|
60
61
|
// (sprint 117) the preview has a menu: the current page, its parent and the appearance in use
|
|
61
62
|
const appearance = navAppearance(base as never, st) ?? '';
|
|
62
|
-
const preview = html`<div class="tr-preview tr-preview-${baseStyleOf(base)}" data-tr-preview="${lists}" data-tr-app-appearance="${navAppearance(base as never, null) ?? ''}" data-appearance="${appearance}" aria-label="
|
|
63
|
-
<div class="tr-preview-header">${icon('menu')}<span
|
|
63
|
+
const preview = html`<div class="tr-preview tr-preview-${baseStyleOf(base)}" data-tr-preview="${lists}" data-tr-app-appearance="${navAppearance(base as never, null) ?? ''}" data-appearance="${appearance}" aria-label="${tx('thr.preview_of_the_style')}">
|
|
64
|
+
<div class="tr-preview-header">${icon('menu')}<span>${tx('thr.preview')}</span></div>
|
|
64
65
|
<div class="tr-preview-main">
|
|
65
|
-
<ul class="tr-preview-nav" aria-label="
|
|
66
|
-
<li><span class="tr-nav-parent">${icon('users')}<span
|
|
67
|
-
<ul><li><span class="tr-nav-current"
|
|
68
|
-
<li><span>${icon('chart')}<span
|
|
66
|
+
<ul class="tr-preview-nav" aria-label="${tx('thr.sample_menu')}">
|
|
67
|
+
<li><span class="tr-nav-parent">${icon('users')}<span>${tx('thr.employees')}</span></span>
|
|
68
|
+
<ul><li><span class="tr-nav-current">${tx('thr.employee_list')}</span></li><li><span>${tx('thr.directory')}</span></li></ul></li>
|
|
69
|
+
<li><span>${icon('chart')}<span>${tx('thr.reports')}</span></span></li>
|
|
69
70
|
</ul>
|
|
70
71
|
<div class="tr-preview-body">
|
|
71
|
-
<section class="region region-standard"><header class="region-header"><h2
|
|
72
|
-
<div class="region-body"><p class="u-mt0"
|
|
73
|
-
<div class="field"><label class="label" for="tr-preview-input"
|
|
74
|
-
<div class="buttons"><button type="button" class="btn"
|
|
72
|
+
<section class="region region-standard"><header class="region-header"><h2>${tx('thr.a_region')}</h2></header>
|
|
73
|
+
<div class="region-body"><p class="u-mt0">${tx.rich('thr.preview_text', { link: html`<a href="#tr-preview-link" id="tr-preview-link">${tx('thr.link')}</a>`, badge: html`<span class="tc-badge tc-badge-info">${tx('thr.badge')}</span>` })}</p>
|
|
74
|
+
<div class="field"><label class="label" for="tr-preview-input">${tx('thr.a_field')}</label><input id="tr-preview-input" value="${tx('thr.value')}" readonly></div>
|
|
75
|
+
<div class="buttons"><button type="button" class="btn">${tx('thr.cancel')}</button><button type="button" class="btn btn-hot">${tx('thr.save')}</button></div></div></section>
|
|
75
76
|
</div></div></div>`;
|
|
76
77
|
return html`${preview}<form method="post" action="${BASE}/apps/${appId}/theme/styles" data-tr-form>${csrf(s)}
|
|
77
78
|
${st ? html`<input type="hidden" name="original" value="${st.name}">` : ''}
|
|
78
79
|
<div class="form-grid">
|
|
79
|
-
${input('name', '
|
|
80
|
-
${colour('accent', '
|
|
81
|
-
${colour('header', '
|
|
82
|
-
${colour('accent_dark', '
|
|
83
|
-
${colour('header_dark', '
|
|
84
|
-
${select('appearance', '
|
|
85
|
-
${colour('strip', '
|
|
86
|
-
${colour('strip_dark', '
|
|
87
|
-
${select('font', '
|
|
88
|
-
${select('font_size', '
|
|
89
|
-
${select('radius', '
|
|
90
|
-
<div class="field" data-wide><label class="label" for="f_condition"
|
|
80
|
+
${input('name', tx('thr.name'), st?.name ?? '', { required: true, placeholder: tx('thr.e_g_ocean'), help: tx('thr.shown_to_users_who_may') })}
|
|
81
|
+
${colour('accent', tx('thr.c_accent'), baseOf.accent)}
|
|
82
|
+
${colour('header', tx('thr.c_header'), baseOf.header)}
|
|
83
|
+
${colour('accent_dark', tx('thr.c_accent_dark'), '#a59cff')}
|
|
84
|
+
${colour('header_dark', tx('thr.c_header_dark'), '#0d0c1a')}
|
|
85
|
+
${select('appearance', tx('thr.navigation_appearance'), st?.appearance ?? '', appearanceChoices(true), APPEARANCE_HELP)}
|
|
86
|
+
${colour('strip', tx('thr.c_strip'), baseOf.accent)}
|
|
87
|
+
${colour('strip_dark', tx('thr.c_strip_dark'), '#a59cff')}
|
|
88
|
+
${select('font', tx('thr.font'), st?.font ?? '', choices(FONTS))}
|
|
89
|
+
${select('font_size', tx('thr.font_size'), st?.font_size ?? '', choices(FONT_SIZES))}
|
|
90
|
+
${select('radius', tx('thr.corners'), st?.radius ?? '', choices(RADII))}
|
|
91
|
+
<div class="field" data-wide><label class="label" for="f_condition">${tx('thr.condition_sql')}</label>
|
|
91
92
|
<textarea id="f_condition" name="condition" class="code" rows="2" spellcheck="false" data-code="sql" placeholder=":APP_TENANT = 'north'">${st?.condition ?? ''}</textarea>
|
|
92
|
-
<small class="help"
|
|
93
|
+
<small class="help">${tx('thr.optional_a_boolean_expression_with')}</small></div>
|
|
93
94
|
</div>
|
|
94
|
-
<div class="buttons"><button class="btn btn-hot">${st ? '
|
|
95
|
-
${st ? html`<a class="btn" href="${BASE}/apps/${appId}/theme"
|
|
95
|
+
<div class="buttons"><button class="btn btn-hot">${st ? tx('thr.save_style') : tx('thr.add_style')}</button>
|
|
96
|
+
${st ? html`<a class="btn" href="${BASE}/apps/${appId}/theme">${tx('thr.cancel')}</a>` : ''}</div>
|
|
96
97
|
</form>`;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -110,46 +111,42 @@ export async function themeRollerRoutes(app: FastifyInstance) {
|
|
|
110
111
|
const v = typeof theme[k] === 'string' && HEX.test(theme[k] as string) ? (theme[k] as string) : '';
|
|
111
112
|
return html`<div class="field"><label class="label" for="f_app_${k}">${title}</label>
|
|
112
113
|
<input id="f_app_${k}" name="${k}" type="color" value="${v || fallback}">
|
|
113
|
-
<label class="check"><input type="checkbox" name="${k}_own" value="true"${v ? raw(' checked') : ''}>
|
|
114
|
+
<label class="check"><input type="checkbox" name="${k}_own" value="true"${v ? raw(' checked') : ''}> ${tx('thr.use_this_colour_else_the_2')}</label></div>`;
|
|
114
115
|
};
|
|
115
116
|
const swatch = (hex: string | undefined) =>
|
|
116
117
|
!hex ? html`<span class="muted">base</span>` : ITEM_REF.test(hex) ? html`<code>${hex}</code>` : html`<input type="color" value="${hex}" disabled aria-label="${hex}" class="swatch"> <code>${hex}</code>`;
|
|
117
118
|
const main = html`${appHeader(a, 'settings', s)}
|
|
118
119
|
<div class="ide-body">
|
|
119
|
-
${region('
|
|
120
|
-
<p class="muted u-mt0"
|
|
121
|
-
(${BASE_STYLES[baseStyleOf(a.theme)].label}) and colours under <a href="${BASE}/apps/${a.id}/settings">Settings → Theme</a>
|
|
122
|
-
are what users see without a style; each style below changes the colours and may set a font, font size and corners.
|
|
123
|
-
The colours apply to the light theme, the dark-mode colours to the dark theme (pick light colours there); fonts, sizes and corners to both.</p>
|
|
120
|
+
${region(tx('thr.theme_roller_style_variants'), html`
|
|
121
|
+
<p class="muted u-mt0">${tx.rich('thr.variants_intro', { base: BASE_STYLES[baseStyleOf(a.theme)].label, settings: html`<a href="${BASE}/apps/${a.id}/settings">${tx('thr.settings_theme')}</a>` })}</p>
|
|
124
122
|
${styles.length
|
|
125
|
-
? html`<div class="table-wrap"><table class="report"><thead><tr><th
|
|
123
|
+
? html`<div class="table-wrap"><table class="report"><thead><tr><th>${tx('thr.name')}</th><th>${tx('thr.accent')}</th><th>${tx('thr.header')}</th><th>${tx('thr.dark_accent')}</th><th>${tx('thr.dark_header')}</th><th>${tx('thr.navigation')}</th><th>${tx('thr.strip')}</th><th>${tx('thr.font')}</th><th>${tx('thr.font_size')}</th><th>${tx('thr.corners')}</th><th><span class="sr-only">${tx('thr.actions')}</span></th></tr></thead><tbody>
|
|
126
124
|
${styles.map((x) => html`<tr><td>${x.name}${theme.style === x.name ? html` <span class="badge">default</span>` : ''}${x.condition ? html`<br><small class="muted">when <code>${x.condition.length > 60 ? `${x.condition.slice(0, 60)}…` : x.condition}</code></small>` : ''}</td>
|
|
127
125
|
<td>${swatch(x.accent)}</td><td>${swatch(x.header)}</td><td>${swatch(x.accent_dark)}</td><td>${swatch(x.header_dark)}</td><td>${appearanceLabel(x.appearance)}</td><td>${swatch(x.strip)}</td><td>${label(FONTS, x.font)}</td><td>${label(FONT_SIZES, x.font_size)}</td><td>${label(RADII, x.radius)}</td>
|
|
128
|
-
<td><a class="btn btn-sm" href="${BASE}/apps/${a.id}/theme?edit=${encodeURIComponent(x.name)}">${icon('edit')}
|
|
126
|
+
<td><a class="btn btn-sm" href="${BASE}/apps/${a.id}/theme?edit=${encodeURIComponent(x.name)}">${icon('edit')} ${tx('thr.edit')}</a>
|
|
129
127
|
<form method="post" action="${BASE}/apps/${a.id}/theme/styles/delete" class="u-inline">${csrf(s)}<input type="hidden" name="name" value="${x.name}">
|
|
130
|
-
<button class="btn btn-sm btn-danger" data-confirm="
|
|
128
|
+
<button class="btn btn-sm btn-danger" data-confirm="${tx('thr.delete_confirm', { name: x.name })}">${tx('thr.delete')}</button></form></td></tr>`)}
|
|
131
129
|
</tbody></table></div>`
|
|
132
|
-
: html`<p class="muted"
|
|
130
|
+
: html`<p class="muted">${tx('thr.no_styles_yet_the_application')}</p>`}
|
|
133
131
|
<form method="post" action="${BASE}/apps/${a.id}/theme/settings" class="u-mt1">${csrf(s)}
|
|
134
132
|
<div class="form-grid">
|
|
135
|
-
${select('style', '
|
|
136
|
-
${select('appearance', '
|
|
137
|
-
${appColour('strip', '
|
|
138
|
-
${appColour('strip_dark', '
|
|
133
|
+
${select('style', tx('thr.default_style'), typeof theme.style === 'string' ? theme.style : '', [['', tx('thr.none_the_base_style_and')], ...styles.map((x): [string, string] => [x.name, x.name])], tx('thr.what_everyone_sees_unless_they'))}
|
|
134
|
+
${select('appearance', tx('thr.navigation_appearance'), navAppearance(theme as never, null) ?? '', appearanceChoices(false), `${APPEARANCE_HELP} A style may choose its own.`)}
|
|
135
|
+
${appColour('strip', tx('thr.c_strip'), BASE_STYLES[baseStyleOf(a.theme)].accent)}
|
|
136
|
+
${appColour('strip_dark', tx('thr.c_strip_dark'), '#a59cff')}
|
|
139
137
|
</div>
|
|
140
|
-
<div class="field"><label class="check"><input type="checkbox" name="style_choice" value="true"${theme.style_choice === true ? raw(' checked') : ''}>
|
|
141
|
-
<small class="help"
|
|
142
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
138
|
+
<div class="field"><label class="check"><input type="checkbox" name="style_choice" value="true"${theme.style_choice === true ? raw(' checked') : ''}> ${tx('thr.users_may_choose_a_style')}</label>
|
|
139
|
+
<small class="help">${tx('thr.adds_the_styles_to_the')}</small></div>
|
|
140
|
+
<div class="buttons"><button class="btn btn-hot">${tx('thr.save')}</button></div>
|
|
143
141
|
</form>`)}
|
|
144
|
-
${region(editing ?
|
|
145
|
-
? html`<p class="muted"
|
|
142
|
+
${region(editing ? tx('thr.edit_style', { name: editing.name }) : tx('thr.add_a_style'), styles.length >= MAX_STYLES && !editing
|
|
143
|
+
? html`<p class="muted">${tx('thr.max_styles', { n: MAX_STYLES })}</p>`
|
|
146
144
|
: styleForm(a.id, s, editing, theme))}
|
|
147
|
-
${region('
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<li>Buttons: ${BUTTON_OPTIONS.map((o, i) => html`${i ? ', ' : ''}<code>${o.cls}</code>`)}</li></ul>`)}
|
|
145
|
+
${region(tx('thr.template_options'), html`<p class="muted u-mt0">${tx.rich('thr.options_intro', { options: html`<em>${tx('thr.template_options')}</em>` })}</p>
|
|
146
|
+
<ul class="u-mt0"><li>${tx.rich('thr.regions_list', { list: html`${REGION_OPTIONS.map((o, i) => html`${i ? ', ' : ''}<code>${o.cls}</code>`)}` })}</li>
|
|
147
|
+
<li>${tx.rich('thr.buttons_list', { list: html`${BUTTON_OPTIONS.map((o, i) => html`${i ? ', ' : ''}<code>${o.cls}</code>`)}` })}</li></ul>`)}
|
|
151
148
|
</div>`;
|
|
152
|
-
return send(reply, s, shell(s,
|
|
149
|
+
return send(reply, s, shell(s, tx('thr.title', { app: a.name }), [[tx('thr.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('thr.settings'), `${BASE}/apps/${a.id}/settings`], [tx('thr.theme_roller')]], main));
|
|
153
150
|
});
|
|
154
151
|
|
|
155
152
|
// add or change a style
|
|
@@ -180,9 +177,9 @@ export async function themeRollerRoutes(app: FastifyInstance) {
|
|
|
180
177
|
const problem = await updateTheme(id, (t) => {
|
|
181
178
|
const styles = appStyles(t as never);
|
|
182
179
|
const at = original ? styles.findIndex((x) => x.name === original) : -1;
|
|
183
|
-
if (original && at < 0) return
|
|
184
|
-
if (styles.some((x, i) => i !== at && x.name.toLowerCase() === parsed.name.toLowerCase())) return
|
|
185
|
-
if (at < 0 && styles.length >= MAX_STYLES) return
|
|
180
|
+
if (original && at < 0) return tx('thr.gone', { name: original });
|
|
181
|
+
if (styles.some((x, i) => i !== at && x.name.toLowerCase() === parsed.name.toLowerCase())) return tx('thr.exists', { name: parsed.name });
|
|
182
|
+
if (at < 0 && styles.length >= MAX_STYLES) return tx('thr.max_styles', { n: MAX_STYLES });
|
|
186
183
|
if (at >= 0) styles[at] = parsed;
|
|
187
184
|
else styles.push(parsed);
|
|
188
185
|
const next: Theme = { ...t, styles };
|
|
@@ -196,7 +193,7 @@ export async function themeRollerRoutes(app: FastifyInstance) {
|
|
|
196
193
|
// users who chose the renamed style keep it
|
|
197
194
|
if (original && original !== parsed.name)
|
|
198
195
|
await owner.query('update meta.account_style set style = $3 where app_id = $1 and style = $2', [id, original, parsed.name]);
|
|
199
|
-
flash(s,
|
|
196
|
+
flash(s, tx('thr.saved', { name: parsed.name }));
|
|
200
197
|
return back(reply, s, target);
|
|
201
198
|
});
|
|
202
199
|
|
|
@@ -214,7 +211,7 @@ export async function themeRollerRoutes(app: FastifyInstance) {
|
|
|
214
211
|
return next;
|
|
215
212
|
});
|
|
216
213
|
if (!problem) await owner.query('delete from meta.account_style where app_id = $1 and style = $2', [id, name]);
|
|
217
|
-
flash(s, problem ??
|
|
214
|
+
flash(s, problem ?? tx('thr.deleted', { name }), problem ? 'error' : 'ok');
|
|
218
215
|
return back(reply, s, `${BASE}/apps/${id}/theme`);
|
|
219
216
|
});
|
|
220
217
|
|
|
@@ -242,7 +239,7 @@ export async function themeRollerRoutes(app: FastifyInstance) {
|
|
|
242
239
|
else return 'Choose one of the styles.';
|
|
243
240
|
return next;
|
|
244
241
|
});
|
|
245
|
-
flash(s, problem ?? '
|
|
242
|
+
flash(s, problem ?? tx('thr.theme_roller_settings_saved'), problem ? 'error' : 'ok');
|
|
246
243
|
return back(reply, s, `${BASE}/apps/${id}/theme`);
|
|
247
244
|
});
|
|
248
245
|
}
|
|
@@ -2,6 +2,7 @@ import type { FastifyInstance } from 'fastify';
|
|
|
2
2
|
import { owner } from '../db.ts';
|
|
3
3
|
import { html, raw } from '../html.ts';
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
5
6
|
import { back, BASE, csrf, developer, flash, region, send, shell, type Body, type Req } from './ui.ts';
|
|
6
7
|
import { usableSchemas, workshopDb } from './workspaces.ts';
|
|
7
8
|
|
|
@@ -72,33 +73,33 @@ export async function uiDefaultRoutes(app: FastifyInstance) {
|
|
|
72
73
|
const t = schema ? await findTable(s, schema, q.table) : null;
|
|
73
74
|
const counts = (await owner.query<{ schema_name: string; n: number }>('select schema_name, count(*)::int as n from meta.ui_default group by 1')).rows;
|
|
74
75
|
const pick = html`<form method="get" class="util-filters">
|
|
75
|
-
<div class="field"><label class="label" for="f_schema"
|
|
76
|
+
<div class="field"><label class="label" for="f_schema">${tx('uid.schema')}</label><select id="f_schema" name="schema"><option value="">${tx('uid.choose_schema')}</option>
|
|
76
77
|
${schemas.map((n) => html`<option value="${n}"${n === schema ? raw(' selected') : ''}>${n}${counts.find((c) => c.schema_name === n) ? ` (${counts.find((c) => c.schema_name === n)!.n})` : ''}</option>`)}</select></div>
|
|
77
|
-
${schema ? html`<div class="field"><label class="label" for="f_table"
|
|
78
|
+
${schema ? html`<div class="field"><label class="label" for="f_table">${tx('uid.table_or_view')}</label><select id="f_table" name="table"><option value="">${tx('uid.choose_table')}</option>
|
|
78
79
|
${tables.map((x) => html`<option value="${x.name}"${x.name === t?.table ? raw(' selected') : ''}>${x.name}${x.n ? ` (${x.n})` : ''}</option>`)}</select></div>` : ''}
|
|
79
|
-
<div class="buttons"><button class="btn btn-sm">${icon('table')}
|
|
80
|
+
<div class="buttons"><button class="btn btn-sm">${icon('table')} ${tx('common.show')}</button></div></form>`;
|
|
80
81
|
let body = html`${pick}`;
|
|
81
82
|
if (t) {
|
|
82
83
|
const cols = await columnsOf(t.oid, t.schema, t.table);
|
|
83
84
|
body = html`${pick}
|
|
84
85
|
<form method="post" action="${BASE}/ui-defaults">${csrf(s)}<input type="hidden" name="schema" value="${t.schema}"><input type="hidden" name="table" value="${t.table}">
|
|
85
|
-
<div class="table-wrap"><table class="table ui-defaults report-reflow"><thead><tr><th scope="col"
|
|
86
|
+
<div class="table-wrap"><table class="table ui-defaults report-reflow"><thead><tr><th scope="col">${tx('uid.col_column')}</th><th scope="col">${tx('uid.col_label')}</th><th scope="col">${tx('uid.col_mask')}</th><th scope="col">${tx('uid.col_alignment')}</th><th scope="col">${tx('uid.col_help')}</th><th scope="col">${tx('uid.col_lov')}</th></tr></thead><tbody>
|
|
86
87
|
${cols.map((c, i) => html`<tr>
|
|
87
|
-
<td data-label="
|
|
88
|
-
<td data-label="
|
|
89
|
-
<td data-label="
|
|
90
|
-
<td data-label="
|
|
91
|
-
<td data-label="
|
|
92
|
-
<td data-label="
|
|
88
|
+
<td data-label="${tx('uid.col_column')}"><code>${c.name}</code><br><span class="muted small">${c.type}</span><input type="hidden" name="c.${i}.name" value="${c.name}"></td>
|
|
89
|
+
<td data-label="${tx('uid.col_label')}"><input id="f_l${i}" aria-label="${tx('uid.label_of', { column: c.name })}" name="c.${i}.label" value="${c.d?.label ?? ''}" maxlength="200" placeholder="${c.wizard}"></td>
|
|
90
|
+
<td data-label="${tx('uid.col_mask')}"><input id="f_m${i}" aria-label="${tx('uid.mask_of', { column: c.name })}" name="c.${i}.format_mask" value="${c.d?.format_mask ?? ''}" maxlength="60" placeholder="${/numeric|integer|bigint|smallint|real|double/.test(c.type) ? '999G990D00' : /date|time/.test(c.type) ? 'DD.MM.YYYY' : ''}"></td>
|
|
91
|
+
<td data-label="${tx('uid.col_alignment')}"><select id="f_a${i}" aria-label="${tx('uid.alignment_of', { column: c.name })}" name="c.${i}.alignment">${ALIGNMENTS.map((x) => html`<option value="${x}"${(c.d?.alignment ?? '') === x ? raw(' selected') : ''}>${x || '–'}</option>`)}</select></td>
|
|
92
|
+
<td data-label="${tx('uid.col_help')}"><textarea id="f_h${i}" aria-label="${tx('uid.help_of', { column: c.name })}" name="c.${i}.help" rows="1" maxlength="4000">${c.d?.help ?? ''}</textarea></td>
|
|
93
|
+
<td data-label="${tx('uid.col_lov')}"><input id="f_v${i}" aria-label="${tx('uid.lov_of', { column: c.name })}" name="c.${i}.lov" value="${c.d?.lov ?? ''}" maxlength="4000" placeholder="LOV:NAME"></td>
|
|
93
94
|
</tr>`)}</tbody></table></div>
|
|
94
|
-
<div class="buttons"><button class="btn btn-hot" name="mode" value="save">${icon('save')}
|
|
95
|
-
<button class="btn" name="mode" value="seed">${icon('edit')}
|
|
96
|
-
<p class="muted small"
|
|
95
|
+
<div class="buttons"><button class="btn btn-hot" name="mode" value="save">${icon('save')} ${tx('common.save')}</button>
|
|
96
|
+
<button class="btn" name="mode" value="seed">${icon('edit')} ${tx('uid.seed')}</button></div>
|
|
97
|
+
<p class="muted small">${tx.code('uid.help')}</p>
|
|
97
98
|
</form>`;
|
|
98
99
|
}
|
|
99
|
-
const main = html`<h1 class="u-mb1"
|
|
100
|
-
${region('
|
|
101
|
-
return send(reply, s, shell(s, '
|
|
100
|
+
const main = html`<h1 class="u-mb1">${tx('util.c_ui_defaults')}</h1>
|
|
101
|
+
${region(tx('uid.per_column'), html`<p class="muted u-mt0">${tx('uid.intro')}</p>${body}`)}`;
|
|
102
|
+
return send(reply, s, shell(s, tx('util.c_ui_defaults'), [[tx('rail.app_builder'), BASE], [tx('admin.utilities'), `${BASE}/utilities`], [tx('util.c_ui_defaults')]], main));
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
app.post(`${BASE}/ui-defaults`, async (req: Req, reply) => {
|
|
@@ -123,7 +124,7 @@ export async function uiDefaultRoutes(app: FastifyInstance) {
|
|
|
123
124
|
[t.schema, t.table, c.name, short ? text : null, short ? null : text.slice(0, 4000), s.username]);
|
|
124
125
|
n++;
|
|
125
126
|
}
|
|
126
|
-
flash(s, n ?
|
|
127
|
+
flash(s, n ? tx(n === 1 ? 'uid.seeded_one' : 'uid.seeded', { n }) : tx('uid.no_comments'));
|
|
127
128
|
return back(reply, s, to);
|
|
128
129
|
}
|
|
129
130
|
const rows: { name: string; label: string | null; mask: string | null; help: string | null; align: string | null; lov: string | null }[] = [];
|
|
@@ -131,9 +132,9 @@ export async function uiDefaultRoutes(app: FastifyInstance) {
|
|
|
131
132
|
const name = b[`c.${i}.name`];
|
|
132
133
|
if (typeof name !== 'string' || !known.has(name)) continue;
|
|
133
134
|
const mask = trim(b[`c.${i}.format_mask`], 60);
|
|
134
|
-
if (mask && !MASK.test(mask)) throw new Error(
|
|
135
|
+
if (mask && !MASK.test(mask)) throw new Error(tx('uid.bad_mask', { column: name }));
|
|
135
136
|
const align = trim(b[`c.${i}.alignment`], 10);
|
|
136
|
-
if (align && !(ALIGNMENTS as readonly string[]).includes(align)) throw new Error(
|
|
137
|
+
if (align && !(ALIGNMENTS as readonly string[]).includes(align)) throw new Error(tx('uid.bad_alignment', { column: name }));
|
|
137
138
|
rows.push({ name, label: trim(b[`c.${i}.label`], 200), mask, help: trim(b[`c.${i}.help`], 4000), align, lov: trim(b[`c.${i}.lov`], 4000) });
|
|
138
139
|
}
|
|
139
140
|
await owner.tx(async (c) => {
|
|
@@ -152,7 +153,7 @@ export async function uiDefaultRoutes(app: FastifyInstance) {
|
|
|
152
153
|
[t.schema, t.table, r.name, r.label, r.mask, r.help, r.align, r.lov, s.username]);
|
|
153
154
|
}
|
|
154
155
|
});
|
|
155
|
-
flash(s,
|
|
156
|
+
flash(s, tx('uid.saved', { table: `${t.schema}.${t.table}` }));
|
|
156
157
|
} catch (e) {
|
|
157
158
|
flash(s, (e as Error).message, 'error');
|
|
158
159
|
}
|
package/src/builder/ui.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { getSession, saveState, takeFlash, type Session } from '../session.ts';
|
|
|
10
10
|
import { SQL_TOOLS, TOOL_GROUPS, toolLabel } from './sql-tools.ts';
|
|
11
11
|
import { ensureDevbar } from '../runtime/devbar.ts';
|
|
12
12
|
import { noteChange } from './app-updates.ts';
|
|
13
|
-
import { clientTexts, tr } from './i18n.ts';
|
|
13
|
+
import { clientTexts, setRequestTranslator, tr } from './i18n.ts';
|
|
14
14
|
import { languageMenu, loadLanguage } from './language.ts';
|
|
15
15
|
import { enrolmentDue, requirement } from '../mfa.ts';
|
|
16
16
|
|
|
@@ -322,6 +322,7 @@ export async function developer(req: Req, reply: FastifyReply) {
|
|
|
322
322
|
}
|
|
323
323
|
// (sprint 121) a session from before the developer's language was kept in it: the stored choice (language.ts)
|
|
324
324
|
if (s.state.__BLANG === undefined) await loadLanguage(s);
|
|
325
|
+
setRequestTranslator(tr(s)); // (sprint 164) scopedTx in this request's handler and helpers
|
|
325
326
|
// (sprint 126, SEC-2) the two-factor policy: a banner during the grace period, then only the Security page until they enrol
|
|
326
327
|
delete s.state.__MFA_DUE;
|
|
327
328
|
const admin = await isAdmin(s.username);
|
package/src/builder/upgrade.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { recordHistory } from '../history.ts';
|
|
|
5
5
|
import { html, raw } from '../html.ts';
|
|
6
6
|
import { icon } from '../icons.ts';
|
|
7
7
|
import { appHeader, back, BASE, csrf, designerUrl, developer, flash, region, send, shell, type Req } from './ui.ts';
|
|
8
|
+
import { scopedTx as tx, type BuilderKey } from './i18n.ts';
|
|
8
9
|
import { diffHtml } from './utilities.ts';
|
|
9
10
|
|
|
10
11
|
// (sprint 138, AP-5) App Builder → Utilities → Upgrade application (APEX:
|
|
@@ -39,10 +40,10 @@ export interface Candidate {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export type Rule = 'nav_appearance' | 'date_picker' | 'popup_lov';
|
|
42
|
-
export const RULES: Record<Rule, { title:
|
|
43
|
-
nav_appearance: { title: '
|
|
44
|
-
date_picker: { title: '
|
|
45
|
-
popup_lov: { title: '
|
|
43
|
+
export const RULES: Record<Rule, { title: BuilderKey; since: BuilderKey }> = {
|
|
44
|
+
nav_appearance: { title: 'upg.rule_nav_title', since: 'upg.rule_nav_since' },
|
|
45
|
+
date_picker: { title: 'upg.rule_date_title', since: 'upg.rule_date_since' },
|
|
46
|
+
popup_lov: { title: 'upg.rule_popup_title', since: 'upg.rule_popup_since' },
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
/** The relations a list of values query reads (FROM / JOIN names). */
|
|
@@ -57,8 +58,8 @@ export async function upgradeCandidates(appId: number): Promise<Candidate[]> {
|
|
|
57
58
|
const theme = app.theme && typeof app.theme === 'object' ? app.theme : {};
|
|
58
59
|
if (typeof theme.appearance !== 'string' || !theme.appearance) {
|
|
59
60
|
out.push({
|
|
60
|
-
key: 'nav_appearance:app', rule: 'nav_appearance', label: '
|
|
61
|
-
reason: '
|
|
61
|
+
key: 'nav_appearance:app', rule: 'nav_appearance', label: tx('upg.label_theme'), page: null, url: `${BASE}/apps/${appId}/theme`,
|
|
62
|
+
reason: tx('upg.reason_nav'),
|
|
62
63
|
before: { appearance: theme.appearance ?? null }, after: { appearance: 'pillar_light' },
|
|
63
64
|
sql: [`update meta.app set theme = coalesce(theme, '{}'::jsonb) || '{"appearance": "pillar_light"}' where id = $1 and coalesce(theme ->> 'appearance', '') = ''`, [appId]],
|
|
64
65
|
});
|
|
@@ -74,8 +75,8 @@ export async function upgradeCandidates(appId: number): Promise<Candidate[]> {
|
|
|
74
75
|
for (const r of dates) {
|
|
75
76
|
const to = r.is_date ? 'date' : 'datetime';
|
|
76
77
|
out.push({
|
|
77
|
-
key: `date_picker:${r.id}`, rule: 'date_picker', label:
|
|
78
|
-
reason: `${r.table_name}.${r.source_column}
|
|
78
|
+
key: `date_picker:${r.id}`, rule: 'date_picker', label: tx('upg.label_item', { page: r.page_no, name: r.name }), page: r.page_no, url: designerUrl(appId, r.page_no, `c=item-${r.id}`),
|
|
79
|
+
reason: tx(r.is_date ? 'upg.reason_date' : 'upg.reason_datetime', { column: `${r.table_name}.${r.source_column}`, type: r.coltype }),
|
|
79
80
|
before: { type: 'text' }, after: { type: to },
|
|
80
81
|
sql: [`update meta.item set type = $2 where id = $1 and type = 'text'`, [r.id, to]],
|
|
81
82
|
});
|
|
@@ -92,8 +93,8 @@ export async function upgradeCandidates(appId: number): Promise<Candidate[]> {
|
|
|
92
93
|
[names, POPUP_ROWS]).catch(() => ({ rows: [] }))).rows[0];
|
|
93
94
|
if (!big) continue;
|
|
94
95
|
out.push({
|
|
95
|
-
key: `popup_lov:${r.id}`, rule: 'popup_lov', label:
|
|
96
|
-
reason:
|
|
96
|
+
key: `popup_lov:${r.id}`, rule: 'popup_lov', label: tx('upg.label_item', { page: r.page_no, name: r.name }), page: r.page_no, url: designerUrl(appId, r.page_no, `c=item-${r.id}`),
|
|
97
|
+
reason: tx('upg.reason_popup', { table: big.name, n: big.n }),
|
|
97
98
|
before: { type: 'select' }, after: { type: 'popup_lov' },
|
|
98
99
|
sql: [`update meta.item set type = 'popup_lov' where id = $1 and type = 'select'`, [r.id]],
|
|
99
100
|
});
|
|
@@ -114,20 +115,20 @@ export async function upgradeRoutes(app: FastifyInstance) {
|
|
|
114
115
|
const u = `${BASE}/apps/${a.id}/utilities/upgrade`;
|
|
115
116
|
const sections = (Object.keys(RULES) as Rule[]).map((rule) => {
|
|
116
117
|
const list = all.filter((c) => c.rule === rule);
|
|
117
|
-
return html`<section class="util-upgrade" aria-labelledby="up-${rule}"><h3 id="up-${rule}">${RULES[rule].title} <span class="tag">${list.length}</span></h3>
|
|
118
|
-
<p class="muted small">${RULES[rule].since}</p>
|
|
119
|
-
${list.length ? html`<div class="table-wrap"><table class="table report-reflow"><thead><tr><th scope="col"
|
|
120
|
-
${list.map((c) => html`<tr${preview?.key === c.key ? raw(' aria-current="true"') : ''}><td data-label="
|
|
121
|
-
<td data-label="
|
|
122
|
-
<form method="post" action="${u}">${csrf(s)}<input type="hidden" name="key" value="${c.key}"><button class="btn btn-sm btn-hot">${icon('refresh')}
|
|
123
|
-
${list.length > 1 ? html`<form method="post" action="${u}" class="u-mt075">${csrf(s)}<input type="hidden" name="rule" value="${rule}"><button class="btn btn-sm">${icon('refresh')}
|
|
124
|
-
: html`<p class="muted"
|
|
118
|
+
return html`<section class="util-upgrade" aria-labelledby="up-${rule}"><h3 id="up-${rule}">${tx(RULES[rule].title)} <span class="tag">${list.length}</span></h3>
|
|
119
|
+
<p class="muted small">${tx(RULES[rule].since)}</p>
|
|
120
|
+
${list.length ? html`<div class="table-wrap"><table class="table report-reflow"><thead><tr><th scope="col">${tx('upg.col_component')}</th><th scope="col">${tx('upg.col_why')}</th><th scope="col">${tx('upg.col_actions')}</th></tr></thead><tbody>
|
|
121
|
+
${list.map((c) => html`<tr${preview?.key === c.key ? raw(' aria-current="true"') : ''}><td data-label="${tx('upg.col_component')}"><a href="${c.url}">${c.label}</a></td><td data-label="${tx('upg.col_why')}">${c.reason}</td>
|
|
122
|
+
<td data-label="${tx('upg.col_actions')}"><div class="buttons"><a class="btn btn-sm" href="${u}?preview=${encodeURIComponent(c.key)}#preview">${tx('upg.preview')}</a>
|
|
123
|
+
<form method="post" action="${u}">${csrf(s)}<input type="hidden" name="key" value="${c.key}"><button class="btn btn-sm btn-hot">${icon('refresh')} ${tx('upg.upgrade')}</button></form></div></td></tr>`)}</tbody></table></div>
|
|
124
|
+
${list.length > 1 ? html`<form method="post" action="${u}" class="u-mt075">${csrf(s)}<input type="hidden" name="rule" value="${rule}"><button class="btn btn-sm">${icon('refresh')} ${tx('upg.upgrade_all', { n: list.length })}</button></form>` : ''}`
|
|
125
|
+
: html`<p class="muted">${tx('upg.nothing')}</p>`}</section>`;
|
|
125
126
|
});
|
|
126
127
|
const main = html`${appHeader(a, 'utilities', s)}
|
|
127
|
-
${region('
|
|
128
|
-
${preview ? region(
|
|
129
|
-
<form method="post" action="${u}">${csrf(s)}<input type="hidden" name="key" value="${preview.key}"><div class="buttons"><button class="btn btn-hot">${icon('refresh')}
|
|
130
|
-
return send(reply, s, shell(s,
|
|
128
|
+
${region(tx('util.c_upgrade'), html`<p class="muted u-mt0">${tx('upg.intro')}</p>${sections}`)}
|
|
129
|
+
${preview ? region(tx('upg.preview_title', { label: preview.label }), html`<p class="muted u-mt0">${preview.reason}</p>${diffHtml('settings', stableJson(preview.before), stableJson(preview.after), ['now', 'upgraded'])}
|
|
130
|
+
<form method="post" action="${u}">${csrf(s)}<input type="hidden" name="key" value="${preview.key}"><div class="buttons"><button class="btn btn-hot">${icon('refresh')} ${tx('upg.upgrade')}</button></div></form>`, '', 'preview') : ''}`;
|
|
131
|
+
return send(reply, s, shell(s, tx('upg.title', { name: a.name }), [[tx('rail.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('app.tab.utilities'), `${BASE}/apps/${a.id}/utilities`], [tx('util.c_upgrade')]], main));
|
|
131
132
|
});
|
|
132
133
|
|
|
133
134
|
app.post(`${BASE}/apps/:id/utilities/upgrade`, async (req: Req, reply) => {
|
|
@@ -141,14 +142,14 @@ export async function upgradeRoutes(app: FastifyInstance) {
|
|
|
141
142
|
const chosen = key ? all.filter((c) => c.key === key) : all.filter((c) => c.rule === rule);
|
|
142
143
|
const to = `${BASE}/apps/${a.id}/utilities/upgrade`;
|
|
143
144
|
if (!chosen.length) {
|
|
144
|
-
flash(s, '
|
|
145
|
+
flash(s, tx('upg.gone'), 'error');
|
|
145
146
|
return back(reply, s, to);
|
|
146
147
|
}
|
|
147
148
|
await owner.tx(async (c) => {
|
|
148
149
|
for (const x of chosen) await c.query(x.sql[0], x.sql[1]);
|
|
149
150
|
});
|
|
150
151
|
await recordHistory(a.id, s.username, 'upgrade').catch((err) => req.log.warn({ err }, 'change history not recorded'));
|
|
151
|
-
flash(s, chosen.length === 1 ?
|
|
152
|
+
flash(s, chosen.length === 1 ? tx('upg.done_one', { label: chosen[0].label }) : tx('upg.done_n', { n: chosen.length }));
|
|
152
153
|
return back(reply, s, to);
|
|
153
154
|
});
|
|
154
155
|
}
|