pgkiln 0.83.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/db/migrations/154_notifications.sql +186 -0
- package/docs/apex-feature-parity.md +3 -2
- package/docs/guide/03-builder.md +16 -5
- package/docs/guide/04-pages-and-regions.md +42 -0
- package/docs/guide/06-processing.md +22 -1
- package/docs/guide/07-dynamic-actions.md +1 -0
- package/docs/guide/09-reference.md +15 -0
- package/docs/guide/11-from-apex.md +1 -0
- package/docs/guide/12-development.md +13 -1
- package/package.json +1 -1
- package/public/app.css +29 -0
- package/public/app.js +78 -0
- package/public/sw.js +5 -1
- package/src/app.ts +11 -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/code-editor.ts +1 -1
- package/src/builder/components.ts +10 -6
- 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 +3461 -0
- package/src/builder/i18n/en.ts +3461 -0
- package/src/builder/i18n/es.ts +3461 -0
- package/src/builder/i18n/fr.ts +3461 -0
- package/src/builder/i18n/it.ts +3461 -0
- package/src/builder/i18n/ja.ts +3461 -0
- package/src/builder/i18n/ko.ts +3461 -0
- package/src/builder/i18n/nl.ts +3461 -0
- package/src/builder/i18n/pt.ts +3461 -0
- package/src/builder/i18n/zh-cn.ts +3461 -0
- package/src/builder/i18n/zh-tw.ts +3461 -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/notification-settings.ts +27 -0
- 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 +30 -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/cli/replace.ts +2 -0
- package/src/i18n/ar.ts +9 -0
- package/src/i18n/cs.ts +9 -0
- package/src/i18n/da.ts +9 -0
- package/src/i18n/de.ts +9 -0
- package/src/i18n/el.ts +9 -0
- package/src/i18n/es.ts +9 -0
- package/src/i18n/fi.ts +9 -0
- package/src/i18n/fr.ts +9 -0
- package/src/i18n/he.ts +9 -0
- package/src/i18n/it.ts +9 -0
- package/src/i18n/ja.ts +9 -0
- package/src/i18n/ko.ts +9 -0
- package/src/i18n/nb.ts +9 -0
- package/src/i18n/pl.ts +9 -0
- package/src/i18n/pt.ts +9 -0
- package/src/i18n/ru.ts +9 -0
- package/src/i18n/sv.ts +9 -0
- package/src/i18n/tr.ts +9 -0
- package/src/i18n/uk.ts +9 -0
- package/src/i18n/zh.ts +9 -0
- package/src/i18n.ts +18 -0
- package/src/metadata.ts +9 -3
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/feedback.ts +1 -1
- package/src/runtime/lists.ts +43 -11
- package/src/runtime/notifications.ts +237 -0
- package/src/runtime/processes.ts +45 -1
- package/src/runtime/regions.ts +11 -15
- package/src/runtime/render.ts +12 -3
- package/src/runtime/routes.ts +12 -4
- package/src/runtime/signin.ts +2 -1
- package/src/session.ts +13 -4
|
@@ -2,10 +2,11 @@ import type { FastifyInstance } from 'fastify';
|
|
|
2
2
|
import { owner } from '../db.ts';
|
|
3
3
|
import { html, raw, type Raw } from '../html.ts';
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
|
-
import { featureStates } from './features.ts';
|
|
5
|
+
import { featHelp, featLabel, featureStates } from './features.ts';
|
|
6
6
|
import { appOr404 } from './forms.ts';
|
|
7
7
|
import { appHeader, back, BASE, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
|
|
8
|
-
import { NO_TABLE, PAGE_KINDS, wizardTables } from './wizards.ts';
|
|
8
|
+
import { kindHelp, kindLabel, NO_TABLE, PAGE_KINDS, wizardTables } from './wizards.ts';
|
|
9
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
9
10
|
|
|
10
11
|
// (sprint 137, AP-3) Create Page like APEX's: a grid of page types with small previews (tab Component), the
|
|
11
12
|
// features (tab Feature, features.ts) and Create page as copy (tab Copy), at /builder/apps/:id/create-page.
|
|
@@ -14,12 +15,12 @@ import { NO_TABLE, PAGE_KINDS, wizardTables } from './wizards.ts';
|
|
|
14
15
|
// forms: no JavaScript needed. The previews are inline SVG drawn with classes (public/builder.css), no styles.
|
|
15
16
|
|
|
16
17
|
/** The grid's order (APEX's names), each with the step it starts with. */
|
|
17
|
-
const GRID: { key: string; label: string; help: string; table: boolean }[]
|
|
18
|
-
{ key: 'blank', label: '
|
|
18
|
+
const GRID = (): { key: string; label: string; help: string; table: boolean }[] => [
|
|
19
|
+
{ key: 'blank', label: tx('cpg.blank_page'), help: tx('cpg.an_empty_page_to_build'), table: false },
|
|
19
20
|
...['calendar', 'cards', 'chart', 'comments', 'content_row', 'dashboard', 'data_load', 'facets', 'form', 'grid', 'report', 'map',
|
|
20
21
|
'master_detail', 'media_list', 'report_form', 'search', 'smart_filters', 'timeline', 'tree', 'tasks', 'wizard', 'workflows'].map((k) => {
|
|
21
22
|
const d = PAGE_KINDS.find((x) => x[0] === k)!;
|
|
22
|
-
return { key: k, label: d[1], help: d[2], table: !NO_TABLE.has(k) };
|
|
23
|
+
return { key: k, label: kindLabel(k, d[1]), help: kindHelp(k, d[2]), table: !NO_TABLE.has(k) };
|
|
23
24
|
}),
|
|
24
25
|
];
|
|
25
26
|
|
|
@@ -66,7 +67,7 @@ const PREVIEWS: Record<string, string> = {
|
|
|
66
67
|
export const preview = (key: string): Raw =>
|
|
67
68
|
raw(`<svg class="cp-svg" viewBox="0 0 80 50" aria-hidden="true" focusable="false">${PREVIEWS[key] ?? PREVIEWS.blank}</svg>`);
|
|
68
69
|
|
|
69
|
-
const TABS: [string, string][] = [['component', '
|
|
70
|
+
const TABS: [string, string][] = [['component', tx('cpg.component')], ['feature', tx('cpg.feature')], ['copy', tx('cpg.copy')]];
|
|
70
71
|
|
|
71
72
|
export async function createPageRoutes(app: FastifyInstance) {
|
|
72
73
|
app.get(`${BASE}/apps/:id/create-page`, async (req: Req, reply) => {
|
|
@@ -76,8 +77,8 @@ export async function createPageRoutes(app: FastifyInstance) {
|
|
|
76
77
|
if (!a) return reply.code(404).send('Not found');
|
|
77
78
|
const q = req.query as Record<string, string | undefined>;
|
|
78
79
|
const tab = TABS.some(([t]) => t === q.tab) ? q.tab! : 'component';
|
|
79
|
-
const type = GRID.find((g) => g.key === q.type);
|
|
80
|
-
const crumbs: [string, string?][] = [['
|
|
80
|
+
const type = GRID().find((g) => g.key === q.type);
|
|
81
|
+
const crumbs: [string, string?][] = [[tx('cpg.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], ['Create page', type ? `${BASE}/apps/${a.id}/create-page` : undefined]];
|
|
81
82
|
const nextPage = async () => {
|
|
82
83
|
const used = new Set((await owner.query('select page_no from meta.page where app_id = $1', [a.id])).rows.map((r) => r.page_no as number));
|
|
83
84
|
let n = Math.max(0, ...[...used].filter((x) => x < 9000)) + 1;
|
|
@@ -91,49 +92,49 @@ export async function createPageRoutes(app: FastifyInstance) {
|
|
|
91
92
|
if (type.key === 'blank') {
|
|
92
93
|
body = html`<form method="post" action="${BASE}/apps/${a.id}/pages">${csrf(s)}
|
|
93
94
|
<div class="form-grid">
|
|
94
|
-
${input('page_no', '
|
|
95
|
-
${input('name', '
|
|
96
|
-
${select('mode', '
|
|
97
|
-
${input('parent_page', '
|
|
95
|
+
${input('page_no', tx('cpg.page_number'), await nextPage(), { type: 'number', required: true })}
|
|
96
|
+
${input('name', tx('cpg.name'), '', { required: true })}
|
|
97
|
+
${select('mode', tx('cpg.page_mode'), 'normal', [['normal', tx('cpg.normal_page')], ['modal', tx('cpg.modal_dialog')]])}
|
|
98
|
+
${input('parent_page', tx('cpg.breadcrumb_parent'), a.home_page ?? '', { type: 'number' })}
|
|
98
99
|
</div>
|
|
99
|
-
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page"
|
|
100
|
+
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page">${tx('cpg.back')}</a><button class="btn btn-hot">${tx('cpg.create_page')}</button></div>
|
|
100
101
|
</form>`;
|
|
101
102
|
} else {
|
|
102
103
|
const tables = await wizardTables(a.db_role);
|
|
103
104
|
body = html`<form method="get" action="${BASE}/apps/${a.id}/wizard">
|
|
104
105
|
<input type="hidden" name="kind" value="${type.key}">
|
|
105
106
|
<div class="form-grid">
|
|
106
|
-
${select('table', '
|
|
107
|
+
${select('table', tx('cpg.table_or_view'), '', tables.map((t): [string, string] => [t.t, t.access ? t.t : `${t.t} (no access for ${a.db_role})`]),
|
|
107
108
|
'The next step proposes the columns, keys and foreign keys from the database catalog.')}
|
|
108
109
|
</div>
|
|
109
|
-
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page"
|
|
110
|
+
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page">${tx('cpg.back')}</a><button class="btn btn-hot">${tx('cpg.next')}</button></div>
|
|
110
111
|
</form>`;
|
|
111
112
|
}
|
|
112
113
|
const main = html`${appHeader(a, 'pages', s)}
|
|
113
114
|
${region(type.label, html`<div class="cp-step"><span class="cp-preview cp-preview-lg">${preview(type.key)}</span><p class="muted">${type.help}</p></div>${body}`)}`;
|
|
114
|
-
return send(reply, s, shell(s,
|
|
115
|
+
return send(reply, s, shell(s, tx('cpg.page_title', { app: a.name }), [...crumbs, [type.label]], main));
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
const tabs = html`<nav class="gal-tabs segmented cp-tabs" aria-label="
|
|
118
|
+
const tabs = html`<nav class="gal-tabs segmented cp-tabs" aria-label="${tx('cpg.create_page')}">${TABS.map(([t, l]) =>
|
|
118
119
|
html`<a href="${BASE}/apps/${a.id}/create-page?tab=${t}"${t === tab ? raw(' aria-current="page"') : ''}>${l}</a>`)}</nav>`;
|
|
119
120
|
let body: Raw;
|
|
120
121
|
if (tab === 'component') {
|
|
121
|
-
const href = (g:
|
|
122
|
+
const href = (g: ReturnType<typeof GRID>[number]) =>
|
|
122
123
|
g.key !== 'blank' && !g.table ? `${BASE}/apps/${a.id}/wizard?kind=${g.key}` : `${BASE}/apps/${a.id}/create-page?type=${g.key}`;
|
|
123
|
-
body = html`<p class="muted u-mt0"
|
|
124
|
-
<ul class="cp-grid">${GRID.map((g) => html`<li><a class="cp-type" href="${href(g)}"><span class="cp-preview">${preview(g.key)}</span><b>${g.label}</b><small>${g.help}</small></a></li>`)}
|
|
125
|
-
<li><a class="cp-type" href="${BASE}/apps/${a.id}/ai-pages"><span class="cp-preview">${preview('ai')}</span><b
|
|
124
|
+
body = html`<p class="muted u-mt0">${tx('cpg.choose_a_page_type_types')}</p>
|
|
125
|
+
<ul class="cp-grid">${GRID().map((g) => html`<li><a class="cp-type" href="${href(g)}"><span class="cp-preview">${preview(g.key)}</span><b>${g.label}</b><small>${g.help}</small></a></li>`)}
|
|
126
|
+
<li><a class="cp-type" href="${BASE}/apps/${a.id}/ai-pages"><span class="cp-preview">${preview('ai')}</span><b>${tx('cpg.create_pages_with_ai')}</b><small>${tx('cpg.describe_the_pages_in_your')}</small></a></li></ul>`;
|
|
126
127
|
} else if (tab === 'feature') {
|
|
127
128
|
const states = await featureStates(a.id);
|
|
128
|
-
body = html`<p class="muted u-mt0"
|
|
129
|
+
body = html`<p class="muted u-mt0">${tx('cpg.features_add_pages_and_settings')}</p>
|
|
129
130
|
<ul class="cp-grid">${states.map(({ f, installed, blocked }) => html`<li><a class="cp-type${installed || blocked ? ' cp-done' : ''}" href="${BASE}/apps/${a.id}/features/${f.key}">
|
|
130
|
-
<span class="cp-feature-icon">${icon(f.icon)}</span><b>${f
|
|
131
|
-
${installed ? html`<span class="tag"
|
|
131
|
+
<span class="cp-feature-icon">${icon(f.icon)}</span><b>${featLabel(f)}</b><small>${featHelp(f)}</small>
|
|
132
|
+
${installed ? html`<span class="tag">${tx('cpg.installed')}</span>` : blocked ? html`<span class="tag">${tx('cpg.not_available')}</span>` : ''}</a></li>`)}</ul>`;
|
|
132
133
|
} else {
|
|
133
134
|
body = await copyStep1(a, s.username!);
|
|
134
135
|
}
|
|
135
|
-
const main = html`${appHeader(a, 'pages', s)}${region('
|
|
136
|
-
return send(reply, s, shell(s,
|
|
136
|
+
const main = html`${appHeader(a, 'pages', s)}${region(tx('cpg.create_page'), html`${tabs}${body}`)}`;
|
|
137
|
+
return send(reply, s, shell(s, tx('cpg.page_title', { app: a.name }), crumbs, main));
|
|
137
138
|
});
|
|
138
139
|
|
|
139
140
|
// ---------------------------------------------------------------- Create page as copy
|
|
@@ -146,17 +147,17 @@ export async function createPageRoutes(app: FastifyInstance) {
|
|
|
146
147
|
const q = req.query as Record<string, string | undefined>;
|
|
147
148
|
const from = await sourceApp(a.id, q.from);
|
|
148
149
|
if (!from) {
|
|
149
|
-
flash(s, '
|
|
150
|
+
flash(s, tx('cpg.choose_an_application_of_this'), 'error');
|
|
150
151
|
return back(reply, s, `${BASE}/apps/${a.id}/create-page?tab=copy`);
|
|
151
152
|
}
|
|
152
153
|
const pages = (await owner.query<{ id: number; page_no: number; name: string }>('select id, page_no, name from meta.page where app_id = $1 order by page_no', [from.id])).rows;
|
|
153
154
|
const page = pages.find((p) => String(p.page_no) === q.page);
|
|
154
|
-
const crumbs: [string, string?][] = [['
|
|
155
|
+
const crumbs: [string, string?][] = [[tx('cpg.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('cpg.create_page'), `${BASE}/apps/${a.id}/create-page?tab=copy`], [tx('cpg.copy')]];
|
|
155
156
|
let body: Raw;
|
|
156
157
|
if (!page) {
|
|
157
158
|
body = html`<form method="get" action="${BASE}/apps/${a.id}/copy-page"><input type="hidden" name="from" value="${from.id}">
|
|
158
|
-
<div class="form-grid">${select('page',
|
|
159
|
-
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page?tab=copy"
|
|
159
|
+
<div class="form-grid">${select('page', tx('cpg.page_of', { app: from.name }), '', pages.map((p): [string, string] => [String(p.page_no), `${p.page_no} ${p.name}`]))}</div>
|
|
160
|
+
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/create-page?tab=copy">${tx('cpg.back')}</a><button class="btn btn-hot">${tx('cpg.next')}</button></div></form>`;
|
|
160
161
|
} else {
|
|
161
162
|
const refs = (await owner.query<{ kind: string; name: string; in_target: boolean }>('select * from meta.page_shared_refs($1, $2)', [page.id, a.id])).rows;
|
|
162
163
|
const used = new Set((await owner.query('select page_no from meta.page where app_id = $1', [a.id])).rows.map((r) => r.page_no as number));
|
|
@@ -166,27 +167,27 @@ export async function createPageRoutes(app: FastifyInstance) {
|
|
|
166
167
|
const options = async (kind: string) => (await owner.query<{ n: string }>(SHARED_NAMES[kind], [a.id])).rows.map((r) => r.n);
|
|
167
168
|
const mapRows = await Promise.all(refs.map(async (r) => {
|
|
168
169
|
const names = await options(r.kind);
|
|
169
|
-
const opts: [string, string][] = [...(same || r.in_target ? [] : [['', '
|
|
170
|
-
return html`<tr><td>${
|
|
171
|
-
<select name="map:${r.kind}:${r.name}" aria-label="${
|
|
170
|
+
const opts: [string, string][] = [...(same || r.in_target ? [] : [['', tx('cpg.copy_it_into_this_application')] as [string, string]]), ...names.map((n): [string, string] => [n, tx('cpg.use_name', { name: n })])];
|
|
171
|
+
return html`<tr><td>${kindName(r.kind)}</td><td><code>${r.name}</code></td><td>
|
|
172
|
+
<select name="map:${r.kind}:${r.name}" aria-label="${kindName(r.kind)} ${r.name}">${opts.map(([v, l]) => html`<option value="${v}"${v === (r.in_target || same ? r.name : '') ? raw(' selected') : ''}>${l}</option>`)}</select></td></tr>`;
|
|
172
173
|
}));
|
|
173
|
-
body = html`<p class="muted u-mt0"
|
|
174
|
+
body = html`<p class="muted u-mt0">${tx.rich('cpg.copy_intro', { n: page.page_no, name: html`<b>${page.name}</b>`, app: from.name, prefix: `P${page.page_no}_` })}</p>
|
|
174
175
|
<form method="post" action="${BASE}/apps/${a.id}/copy-page">${csrf(s)}
|
|
175
176
|
<input type="hidden" name="from" value="${from.id}"><input type="hidden" name="page" value="${page.page_no}">
|
|
176
177
|
<div class="form-grid">
|
|
177
|
-
${input('new_page', '
|
|
178
|
-
${input('name', '
|
|
178
|
+
${input('new_page', tx('cpg.new_page_number'), no, { type: 'number', required: true })}
|
|
179
|
+
${input('name', tx('cpg.name'), page.name, { required: true })}
|
|
179
180
|
</div>
|
|
180
181
|
${refs.length
|
|
181
|
-
? html`<h3
|
|
182
|
-
<div class="table-wrap"><table class="report"><thead><tr><th
|
|
183
|
-
: html`<p class="muted"
|
|
184
|
-
${same ? '' : html`<p class="muted small"
|
|
185
|
-
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/copy-page?from=${from.id}"
|
|
182
|
+
? html`<h3>${tx('cpg.shared_components')}</h3><p class="muted small">${tx('cpg.refers_by_name')} ${same ? tx('cpg.in_the_same_application_they') : tx('cpg.use_one_of_this_application')}</p>
|
|
183
|
+
<div class="table-wrap"><table class="report"><thead><tr><th>${tx('cpg.kind')}</th><th>${tx('cpg.name')}</th><th>${tx('cpg.in_this_application')}</th></tr></thead><tbody>${mapRows}</tbody></table></div>`
|
|
184
|
+
: html`<p class="muted">${tx('cpg.the_page_uses_no_shared')}</p>`}
|
|
185
|
+
${same ? '' : html`<p class="muted small">${tx.rich('cpg.sql_runs_as', { role: html`<code>${a.db_role ?? tx('cpg.none')}</code>`, app: from.name })}</p>`}
|
|
186
|
+
<div class="buttons"><a class="btn" href="${BASE}/apps/${a.id}/copy-page?from=${from.id}">${tx('cpg.back')}</a><button class="btn btn-hot">${tx('cpg.copy_page')}</button></div>
|
|
186
187
|
</form>`;
|
|
187
188
|
}
|
|
188
|
-
const main = html`${appHeader(a, 'pages', s)}${region('
|
|
189
|
-
return send(reply, s, shell(s,
|
|
189
|
+
const main = html`${appHeader(a, 'pages', s)}${region(tx('cpg.create_page_as_copy'), body)}`;
|
|
190
|
+
return send(reply, s, shell(s, tx('cpg.copy_title', { app: a.name }), crumbs, main));
|
|
190
191
|
});
|
|
191
192
|
|
|
192
193
|
app.post(`${BASE}/apps/:id/copy-page`, async (req: Req, reply) => {
|
|
@@ -198,15 +199,15 @@ export async function createPageRoutes(app: FastifyInstance) {
|
|
|
198
199
|
const from = await sourceApp(a.id, String(b.from ?? ''));
|
|
199
200
|
const retry = `${BASE}/apps/${a.id}/copy-page?from=${encodeURIComponent(String(b.from ?? ''))}&page=${encodeURIComponent(String(b.page ?? ''))}`;
|
|
200
201
|
if (!from) {
|
|
201
|
-
flash(s, '
|
|
202
|
+
flash(s, tx('cpg.choose_an_application_of_this'), 'error');
|
|
202
203
|
return back(reply, s, `${BASE}/apps/${a.id}/create-page?tab=copy`);
|
|
203
204
|
}
|
|
204
205
|
try {
|
|
205
206
|
const page = /^\d{1,6}$/.test(String(b.page ?? '')) ? Number(b.page) : NaN;
|
|
206
207
|
const no = /^\d{1,6}$/.test(String(b.new_page ?? '').trim()) ? Number(String(b.new_page).trim()) : NaN;
|
|
207
|
-
if (!(no >= 1)) throw new Error('
|
|
208
|
+
if (!(no >= 1)) throw new Error(tx('cpg.enter_a_page_number'));
|
|
208
209
|
const name = String(b.name ?? '').trim().slice(0, 200);
|
|
209
|
-
if (!name) throw new Error('
|
|
210
|
+
if (!name) throw new Error(tx('cpg.enter_a_name'));
|
|
210
211
|
// map:<kind>:<NAME> → a name of the target's (empty: copy); only known kinds pass
|
|
211
212
|
const map: Record<string, string> = {};
|
|
212
213
|
for (const [k, v] of Object.entries(b)) {
|
|
@@ -214,7 +215,7 @@ export async function createPageRoutes(app: FastifyInstance) {
|
|
|
214
215
|
if (m && Object.hasOwn(SHARED_NAMES, m[1]) && typeof v === 'string') map[`${m[1]}:${m[2]}`] = v.slice(0, 100);
|
|
215
216
|
}
|
|
216
217
|
const id = await owner.tx(async (c) => (await c.query('select meta.copy_page($1, $2, $3, $4, $5, $6::jsonb) as id', [from.id, page, a.id, no, name, JSON.stringify(map)])).rows[0].id as number);
|
|
217
|
-
flash(s,
|
|
218
|
+
flash(s, from.id === a.id ? tx('cpg.copied', { n: no, name, page }) : tx('cpg.copied_from', { n: no, name, page, app: from.name }));
|
|
218
219
|
return back(reply, s, `${BASE}/pages/${id}`);
|
|
219
220
|
} catch (e) {
|
|
220
221
|
flash(s, (e as Error).message, 'error');
|
|
@@ -227,6 +228,7 @@ const KIND_LABELS: Record<string, string> = {
|
|
|
227
228
|
authz_scheme: 'Authorization scheme', build_option: 'Build option', lov: 'List of values', list: 'List',
|
|
228
229
|
template_component: 'Template component', search_config: 'Search configuration', data_load_def: 'Data load definition',
|
|
229
230
|
};
|
|
231
|
+
const kindName = (k: string) => (Object.hasOwn(KIND_LABELS, k) ? tx.dyn(`cpg.k.${k}`, KIND_LABELS[k]) : k);
|
|
230
232
|
/** The target application's names of each kind (the choices of the mapping). */
|
|
231
233
|
const SHARED_NAMES: Record<string, string> = {
|
|
232
234
|
authz_scheme: 'select name as n from meta.authz_scheme where app_id = $1 order by 1',
|
|
@@ -251,9 +253,9 @@ async function copyStep1(a: { id: number; name: string }, _user: string): Promis
|
|
|
251
253
|
const apps = (await owner.query<{ id: number; name: string; alias: string }>(
|
|
252
254
|
'select id, name, alias from meta.app where meta.app_workspace(id) = meta.app_workspace($1) order by id <> $1, lower(name)', [a.id],
|
|
253
255
|
)).rows;
|
|
254
|
-
return html`<p class="muted u-mt0"
|
|
256
|
+
return html`<p class="muted u-mt0">${tx('cpg.copy_a_page_of_this')}</p>
|
|
255
257
|
<form method="get" action="${BASE}/apps/${a.id}/copy-page">
|
|
256
|
-
<div class="form-grid">${select('from', '
|
|
257
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
258
|
+
<div class="form-grid">${select('from', tx('cpg.copy_from_application'), String(a.id), apps.map((x): [string, string] => [String(x.id), x.id === a.id ? `${x.name} (this application)` : `${x.name} (${x.alias})`]))}</div>
|
|
259
|
+
<div class="buttons"><button class="btn btn-hot">${tx('cpg.next')}</button></div>
|
|
258
260
|
</form>`;
|
|
259
261
|
}
|
package/src/builder/cross-app.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { html, raw } from '../html.ts';
|
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
5
|
import { BASE, developer, region, send, shell, type Req } from './ui.ts';
|
|
6
6
|
import { currentWorkspace } from './workspaces.ts';
|
|
7
|
+
import { scopedTx as tx, type BuilderKey } from './i18n.ts';
|
|
7
8
|
|
|
8
9
|
// Cross-application reports (sprint 61; APEX: Workspace Utilities → Cross
|
|
9
10
|
// Application Reports): one read-only report per topic over every application
|
|
@@ -11,9 +12,10 @@ import { currentWorkspace } from './workspaces.ts';
|
|
|
11
12
|
// returns app_id and app (the name) first; those become a link.
|
|
12
13
|
|
|
13
14
|
interface Report {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/** Message keys (src/builder/i18n): the title, the introduction and the column labels. */
|
|
16
|
+
title: BuilderKey;
|
|
17
|
+
about: BuilderKey;
|
|
18
|
+
columns: [key: string, label: BuilderKey, cls?: 'num'][];
|
|
17
19
|
sql: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -22,56 +24,56 @@ const IN_WS = 'meta.app_workspace(a.id) = $1';
|
|
|
22
24
|
|
|
23
25
|
export const REPORTS: Record<string, Report> = {
|
|
24
26
|
attributes: {
|
|
25
|
-
title: '
|
|
26
|
-
about: '
|
|
27
|
-
columns: [['alias', '
|
|
27
|
+
title: 'xap.attributes.title',
|
|
28
|
+
about: 'xap.attributes.about',
|
|
29
|
+
columns: [['alias', 'xap.col.alias'], ['app_type', 'xap.col.type'], ['authentication', 'xap.col.sign_in'], ['availability', 'xap.col.availability'], ['build_status', 'xap.col.build_status'], ['debug_level', 'xap.col.debug', 'num'], ['language', 'xap.col.language'], ['db_role', 'xap.col.database_role'], ['updated', 'xap.col.updated']],
|
|
28
30
|
sql: `select a.id as app_id, a.name as app, a.alias, a.app_type, a.authentication, a.availability, a.build_status, a.debug_level,
|
|
29
31
|
a.language, coalesce(a.db_role, '(runtime connection)') as db_role, to_char(a.updated_at, 'YYYY-MM-DD HH24:MI') as updated
|
|
30
32
|
from meta.app a where ${IN_WS} order by lower(a.name)`,
|
|
31
33
|
},
|
|
32
34
|
'build-options': {
|
|
33
|
-
title: '
|
|
34
|
-
about: '
|
|
35
|
-
columns: [['name', '
|
|
35
|
+
title: 'xap.build_options.title',
|
|
36
|
+
about: 'xap.build_options.about',
|
|
37
|
+
columns: [['name', 'xap.col.build_option'], ['status', 'xap.col.status'], ['description', 'xap.col.description']],
|
|
36
38
|
sql: `select a.id as app_id, a.name as app, b.name, b.status, b.description
|
|
37
39
|
from meta.app a join meta.build_option b on b.app_id = a.id where ${IN_WS} order by lower(a.name), b.name`,
|
|
38
40
|
},
|
|
39
41
|
comments: {
|
|
40
|
-
title: '
|
|
41
|
-
about: '
|
|
42
|
-
columns: [['page_no', '
|
|
42
|
+
title: 'xap.comments.title',
|
|
43
|
+
about: 'xap.comments.about',
|
|
44
|
+
columns: [['page_no', 'xap.col.page', 'num'], ['author', 'xap.col.author'], ['created', 'xap.col.written'], ['body', 'xap.col.comment']],
|
|
43
45
|
sql: `select a.id as app_id, a.name as app, c.page_no, c.author, to_char(c.created_at, 'YYYY-MM-DD HH24:MI') as created, left(c.body, 500) as body
|
|
44
46
|
from meta.app a join meta.dev_comment c on c.app_id = a.id where ${IN_WS} order by c.created_at desc limit 500`,
|
|
45
47
|
},
|
|
46
48
|
authentication: {
|
|
47
|
-
title: '
|
|
48
|
-
about: '
|
|
49
|
-
columns: [['authentication', '
|
|
49
|
+
title: 'xap.authentication.title',
|
|
50
|
+
about: 'xap.authentication.about',
|
|
51
|
+
columns: [['authentication', 'xap.col.sign_in'], ['local_login', 'xap.col.passwords'], ['providers', 'xap.col.identity_providers'], ['ldap', 'xap.col.ldap_directories'], ['access_control', 'xap.col.access'], ['remember_me_days', 'xap.col.keep_me_signed_in_days', 'num'], ['login_page', 'xap.col.login_page', 'num'], ['password_reset', 'xap.col.forgot_password']],
|
|
50
52
|
sql: `select a.id as app_id, a.name as app, a.authentication, case when a.local_login then 'yes' else 'no' end as local_login,
|
|
51
53
|
array_to_string(a.sso_providers, ', ') as providers, array_to_string(a.ldap_directories, ', ') as ldap,
|
|
52
54
|
a.access_control, a.remember_me_days, a.login_page, case when a.password_reset then 'yes' else 'no' end as password_reset
|
|
53
55
|
from meta.app a where ${IN_WS} order by lower(a.name)`,
|
|
54
56
|
},
|
|
55
57
|
authorization: {
|
|
56
|
-
title: '
|
|
57
|
-
about: '
|
|
58
|
-
columns: [['name', '
|
|
58
|
+
title: 'xap.authorization.title',
|
|
59
|
+
about: 'xap.authorization.about',
|
|
60
|
+
columns: [['name', 'xap.col.scheme'], ['type', 'xap.col.type'], ['value', 'xap.col.checks'], ['error_message', 'xap.col.error_message']],
|
|
59
61
|
sql: `select a.id as app_id, a.name as app, s.name, s.type, left(s.value, 300) as value, s.error_message
|
|
60
62
|
from meta.app a join meta.authz_scheme s on s.app_id = a.id where ${IN_WS} order by lower(a.name), s.name`,
|
|
61
63
|
},
|
|
62
64
|
'password-items': {
|
|
63
|
-
title: '
|
|
64
|
-
about: '
|
|
65
|
-
columns: [['page_no', '
|
|
65
|
+
title: 'xap.password_items.title',
|
|
66
|
+
about: 'xap.password_items.about',
|
|
67
|
+
columns: [['page_no', 'xap.col.page', 'num'], ['name', 'xap.col.item'], ['label', 'xap.col.label'], ['reveal', 'xap.col.show_button'], ['required', 'xap.col.required']],
|
|
66
68
|
sql: `select a.id as app_id, a.name as app, p.page_no, i.name, i.label,
|
|
67
69
|
case when (i.config ->> 'reveal')::boolean then 'yes' else 'no' end as reveal, case when i.required then 'yes' else 'no' end as required
|
|
68
70
|
from meta.app a join meta.page p on p.app_id = a.id join meta.item i on i.page_id = p.id
|
|
69
71
|
where ${IN_WS} and i.type = 'password' order by lower(a.name), p.page_no, i.seq`,
|
|
70
72
|
},
|
|
71
73
|
theme: {
|
|
72
|
-
title: '
|
|
73
|
-
about: '
|
|
74
|
-
columns: [['base', '
|
|
74
|
+
title: 'xap.theme.title',
|
|
75
|
+
about: 'xap.theme.about',
|
|
76
|
+
columns: [['base', 'xap.col.base_style'], ['style', 'xap.col.style'], ['nav', 'xap.col.navigation'], ['colors', 'xap.col.colours_set', 'num'], ['pwa', 'xap.col.installable_pwa']],
|
|
75
77
|
sql: `select a.id as app_id, a.name as app, coalesce(a.theme ->> 'base', 'default') as base, coalesce(a.theme ->> 'style', '-') as style,
|
|
76
78
|
coalesce(a.theme ->> 'nav', 'side') as nav,
|
|
77
79
|
(select count(*) from jsonb_object_keys(coalesce(a.theme -> 'colors', '{}'::jsonb)))::int as colors,
|
|
@@ -82,7 +84,7 @@ export const REPORTS: Record<string, Report> = {
|
|
|
82
84
|
|
|
83
85
|
/** The links in Workspace utilities' side column. */
|
|
84
86
|
export const crossAppLinks = () =>
|
|
85
|
-
Object.entries(REPORTS).map(([k, r]) => html`<li><a href="${BASE}/utilities/reports/${k}">${r.title}${icon('chevron')}</a></li>`);
|
|
87
|
+
Object.entries(REPORTS).map(([k, r]) => html`<li><a href="${BASE}/utilities/reports/${k}">${tx(r.title)}${icon('chevron')}</a></li>`);
|
|
86
88
|
|
|
87
89
|
export async function crossAppRoutes(app: FastifyInstance) {
|
|
88
90
|
app.get(`${BASE}/utilities/reports/:kind`, async (req: Req, reply) => {
|
|
@@ -92,15 +94,15 @@ export async function crossAppRoutes(app: FastifyInstance) {
|
|
|
92
94
|
if (!r) return reply.code(404).send('Not found');
|
|
93
95
|
const rows = (await owner.query(r.sql, [currentWorkspace(s)])).rows;
|
|
94
96
|
const cell = (v: unknown) => (v === null || v === undefined || v === '' ? html`<span class="muted">-</span>` : String(v));
|
|
95
|
-
const main = html`<div class="title-row"><h1>${r.title}</h1></div>
|
|
96
|
-
<p class="muted u-mt0">${r.about}
|
|
97
|
-
<nav class="ide-tabs u-mb1" aria-label="
|
|
98
|
-
html`<a class="ide-tab" href="${BASE}/utilities/reports/${k}"${k === req.params.kind ? raw(' aria-current="page"') : ''}><span>${x.title}</span></a>`)}</nav>
|
|
99
|
-
${region(r.title, rows.length
|
|
100
|
-
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th
|
|
101
|
-
<tbody>${rows.map((row) => html`<tr><td data-label="
|
|
102
|
-
${r.columns.map(([k, l, c]) => html`<td data-label="${l}"${c ? raw(` class="${c}"`) : ''}>${cell(row[k])}</td>`)}</tr>`)}</tbody></table></div>`
|
|
103
|
-
: html`<p class="muted"
|
|
104
|
-
return send(reply, s, shell(s, r.title, [['
|
|
97
|
+
const main = html`<div class="title-row"><h1>${tx(r.title)}</h1></div>
|
|
98
|
+
<p class="muted u-mt0">${tx(r.about)} ${tx('xap.of_workspace', { name: s.workspace?.name ?? '' })}</p>
|
|
99
|
+
<nav class="ide-tabs u-mb1" aria-label="${tx('xap.nav')}">${Object.entries(REPORTS).map(([k, x]) =>
|
|
100
|
+
html`<a class="ide-tab" href="${BASE}/utilities/reports/${k}"${k === req.params.kind ? raw(' aria-current="page"') : ''}><span>${tx(x.title)}</span></a>`)}</nav>
|
|
101
|
+
${region(tx(r.title), rows.length
|
|
102
|
+
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('xap.application')}</th>${r.columns.map(([, l, c]) => html`<th${c ? raw(` class="${c}"`) : ''}>${tx(l)}</th>`)}</tr></thead>
|
|
103
|
+
<tbody>${rows.map((row) => html`<tr><td data-label="${tx('xap.application')}"><a href="${BASE}/apps/${row.app_id}">${row.app}</a></td>
|
|
104
|
+
${r.columns.map(([k, l, c]) => html`<td data-label="${tx(l)}"${c ? raw(` class="${c}"`) : ''}>${cell(row[k])}</td>`)}</tr>`)}</tbody></table></div>`
|
|
105
|
+
: html`<p class="muted">${tx('xap.nothing')}</p>`, html`<span class="count">${rows.length}</span>`)}`;
|
|
106
|
+
return send(reply, s, shell(s, tx(r.title), [[tx('rail.app_builder'), BASE], [tx('wu.title'), `${BASE}/utilities`], [tx(r.title)]], main));
|
|
105
107
|
});
|
|
106
108
|
}
|
package/src/builder/dataload.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type { Session } from '../session.ts';
|
|
|
27
27
|
import type { ComponentSpec } from './components.ts';
|
|
28
28
|
import { back, BASE, csrf, developer, flash, region, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
|
|
29
29
|
import { appWorkspace, currentWorkspace, workshopDb } from './workspaces.ts';
|
|
30
|
-
import { tr, translatorFor, type BuilderTranslate } from './i18n.ts';
|
|
30
|
+
import { scopedTx, tr, translatorFor, type BuilderTranslate } from './i18n.ts';
|
|
31
31
|
|
|
32
32
|
// SQL Workshop → Load Data: upload a CSV/TSV/XLSX/JSON/XML file, preview it,
|
|
33
33
|
// and load it into a new table (column types inferred) or an existing one
|
|
@@ -73,7 +73,7 @@ export const DATA_LOAD_DEF_SPEC: ComponentSpec = {
|
|
|
73
73
|
|
|
74
74
|
/** "Load a file with it" on a definition's Shared Components page. */
|
|
75
75
|
export const dataLoadDefExtras = (row: { id: number }) =>
|
|
76
|
-
html`<p class="u-mt1"><a class="btn" href="${BASE}/sql/load?definition=${row.id}">${icon('upload')}
|
|
76
|
+
html`<p class="u-mt1"><a class="btn" href="${BASE}/sql/load?definition=${row.id}">${icon('upload')} ${scopedTx('dl.load_with_def')}</a></p>`;
|
|
77
77
|
|
|
78
78
|
/** The data load definitions of the current workspace's applications. */
|
|
79
79
|
async function definitions(s: Session) {
|