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
|
@@ -7,9 +7,10 @@ import { html } from '../html.ts';
|
|
|
7
7
|
import { icon } from '../icons.ts';
|
|
8
8
|
import { readMultipart } from '../runtime/files.ts';
|
|
9
9
|
import { clientIp, logActivity, type Session } from '../session.ts';
|
|
10
|
-
import { describeWorkspaceFile, exportWorkspace, importWorkspace,
|
|
10
|
+
import { describeWorkspaceFile, exportWorkspace, importWorkspace, PARTS, type Part } from '../workspace-export.ts';
|
|
11
11
|
import { back, BASE, csrf, developer, flash, region, send, shell, type Body, type Req } from './ui.ts';
|
|
12
12
|
import { currentWorkspace, workspaceAdmin } from './workspaces.ts';
|
|
13
|
+
import { scopedTx as tx, type BuilderKey } from './i18n.ts';
|
|
13
14
|
|
|
14
15
|
// Workspace utilities → Export / import (APEX: Workspace Utilities → Export):
|
|
15
16
|
// the current workspace's components as one file, and such a file imported
|
|
@@ -19,7 +20,14 @@ import { currentWorkspace, workspaceAdmin } from './workspaces.ts';
|
|
|
19
20
|
|
|
20
21
|
const RESULT = '__WS_IMPORT_RESULT';
|
|
21
22
|
const page = `${BASE}/workspace-export`;
|
|
22
|
-
const crumbs: [string, string?][]
|
|
23
|
+
const crumbs = (): [string, string?][] => [[tx('rail.app_builder'), BASE], [tx('wu.title'), `${BASE}/utilities`], [tx('wu.export')]];
|
|
24
|
+
/** The kinds of component in the developer's language (PART_LABELS in English: src/workspace-export.ts). */
|
|
25
|
+
const PART_KEYS: Record<Part, BuilderKey> = {
|
|
26
|
+
settings: 'wtr.part.settings', developers: 'wtr.part.developers', app_groups: 'wtr.part.app_groups', extension_links: 'wtr.part.extension_links',
|
|
27
|
+
rest_catalogs: 'wtr.part.rest_catalogs', sql_scripts: 'wtr.part.sql_scripts', blueprints: 'wtr.part.blueprints', data_generators: 'wtr.part.data_generators',
|
|
28
|
+
datasets: 'wtr.part.datasets', team: 'wtr.part.team', workspace_files: 'wtr.part.workspace_files', remote_servers: 'wtr.part.remote_servers',
|
|
29
|
+
web_credentials: 'wtr.part.web_credentials',
|
|
30
|
+
};
|
|
23
31
|
const MAX_MB = 20;
|
|
24
32
|
|
|
25
33
|
let version: string | null | undefined;
|
|
@@ -41,16 +49,16 @@ const chosen = (b: Body | undefined): Part[] => {
|
|
|
41
49
|
|
|
42
50
|
async function adminOr403(s: Session, reply: FastifyReply) {
|
|
43
51
|
if (s.workspace && (await workspaceAdmin(s.username, s.workspace.id))) return true;
|
|
44
|
-
await send(reply.code(403), s, shell(s, '
|
|
45
|
-
<div class="alert alert-error" role="alert"
|
|
52
|
+
await send(reply.code(403), s, shell(s, tx('wu.export'), crumbs(), html`<h1 class="u-mb1">${tx('wu.export')}</h1>
|
|
53
|
+
<div class="alert alert-error" role="alert">${tx('wtr.admins_only')}</div>`));
|
|
46
54
|
return false;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
const partBoxes = (counts?: Partial<Record<Part, number>>) =>
|
|
50
|
-
html`<fieldset class="field" data-wide><legend class="label"
|
|
58
|
+
html`<fieldset class="field" data-wide><legend class="label">${tx('wtr.components')}</legend>${PARTS.map((p) =>
|
|
51
59
|
counts && counts[p] === undefined
|
|
52
60
|
? ''
|
|
53
|
-
: html`<label class="check"><input type="checkbox" name="part" value="${p}" checked> ${
|
|
61
|
+
: html`<label class="check"><input type="checkbox" name="part" value="${p}" checked> ${tx(PART_KEYS[p])}${counts ? html` <span class="muted">(${p === 'settings' ? tx('wtr.in_file') : p === 'team' ? tx('wtr.issues', { n: counts[p] }) : counts[p]})</span>` : ''}</label>`)}</fieldset>`;
|
|
54
62
|
|
|
55
63
|
export async function workspaceTransferRoutes(app: FastifyInstance) {
|
|
56
64
|
app.get(page, async (req: Req, reply) => {
|
|
@@ -59,22 +67,22 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
|
|
|
59
67
|
const result = s.state[RESULT] ? (JSON.parse(s.state[RESULT]!) as string[]) : null;
|
|
60
68
|
delete s.state[RESULT];
|
|
61
69
|
const name = s.workspace!.name;
|
|
62
|
-
const main = html`<div class="title-row"><h1
|
|
63
|
-
<p class="muted u-mt0"
|
|
64
|
-
${result ? html`<div class="alert alert-success" role="status"><b
|
|
65
|
-
${region('
|
|
70
|
+
const main = html`<div class="title-row"><h1>${tx('wu.export')}</h1></div>
|
|
71
|
+
<p class="muted u-mt0">${tx('wtr.intro', { name })}</p>
|
|
72
|
+
${result ? html`<div class="alert alert-success" role="status"><b>${tx('wtr.imported_into', { name })}</b><ul>${result.map((l) => html`<li>${l}</li>`)}</ul></div>` : ''}
|
|
73
|
+
${region(tx('wtr.export'), html`<form method="post" action="${page}">${csrf(s)}
|
|
66
74
|
${partBoxes()}
|
|
67
|
-
<p class="muted"
|
|
68
|
-
<div class="buttons"><button class="btn btn-hot">${icon('download')}
|
|
69
|
-
${region('
|
|
70
|
-
<p class="muted u-mt0"
|
|
75
|
+
<p class="muted">${tx('wtr.not_in_file')}</p>
|
|
76
|
+
<div class="buttons"><button class="btn btn-hot">${icon('download')} ${tx('wtr.export')}</button></div></form>`)}
|
|
77
|
+
${region(tx('wtr.import'), html`<form method="post" action="${BASE}/workspace-import" enctype="multipart/form-data">${csrf(s)}
|
|
78
|
+
<p class="muted u-mt0">${tx.rich('wtr.import_intro', { name: html`<b>${name}</b>` })}</p>
|
|
71
79
|
<div class="form-grid">
|
|
72
|
-
<div class="field" data-wide><label class="label" for="f_file"
|
|
80
|
+
<div class="field" data-wide><label class="label" for="f_file">${tx('wtr.file')}</label><input id="f_file" type="file" name="file" accept=".json,application/json" required></div>
|
|
73
81
|
${partBoxes()}
|
|
74
|
-
<div class="field" data-wide><label class="check"><input type="checkbox" name="replace" value="true">
|
|
82
|
+
<div class="field" data-wide><label class="check"><input type="checkbox" name="replace" value="true"> ${tx('wtr.replace')}</label></div>
|
|
75
83
|
</div>
|
|
76
|
-
<div class="buttons"><button class="btn">${icon('upload')}
|
|
77
|
-
return send(reply, s, shell(s, '
|
|
84
|
+
<div class="buttons"><button class="btn">${icon('upload')} ${tx('wtr.import')}</button></div></form>`)}`;
|
|
85
|
+
return send(reply, s, shell(s, tx('wu.export'), crumbs(), main));
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
app.post(page, async (req: Req, reply) => {
|
|
@@ -82,7 +90,7 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
|
|
|
82
90
|
if (!s || !(await adminOr403(s, reply))) return;
|
|
83
91
|
const parts = chosen(req.body);
|
|
84
92
|
if (!parts.length) {
|
|
85
|
-
flash(s, '
|
|
93
|
+
flash(s, tx('wtr.choose_part'), 'error');
|
|
86
94
|
return back(reply, s, page);
|
|
87
95
|
}
|
|
88
96
|
const doc = await exportWorkspace(currentWorkspace(s), parts, { version: pgkilnVersion() ?? undefined, by: s.username });
|
|
@@ -104,23 +112,23 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
|
|
|
104
112
|
const s = await developer(req, reply);
|
|
105
113
|
if (!s || !(await adminOr403(s, reply))) return;
|
|
106
114
|
try {
|
|
107
|
-
if (tooBig) throw new Error(
|
|
108
|
-
if (!text.trim()) throw new Error('
|
|
115
|
+
if (tooBig) throw new Error(tx('wtr.too_big', { n: MAX_MB }));
|
|
116
|
+
if (!text.trim()) throw new Error(tx('wtr.choose_file'));
|
|
109
117
|
let doc: unknown;
|
|
110
118
|
try {
|
|
111
119
|
doc = JSON.parse(text);
|
|
112
120
|
} catch {
|
|
113
|
-
throw new Error('
|
|
121
|
+
throw new Error(tx('wtr.not_json'));
|
|
114
122
|
}
|
|
115
123
|
describeWorkspaceFile(doc);
|
|
116
124
|
const parts = chosen(req.body);
|
|
117
|
-
if (!parts.length) throw new Error('
|
|
125
|
+
if (!parts.length) throw new Error(tx('wtr.choose_part'));
|
|
118
126
|
const ws = currentWorkspace(s);
|
|
119
127
|
const result = await owner.tx((c) => importWorkspace(c, ws, doc, parts, { replace: req.body?.replace === 'true', by: s.username }));
|
|
120
128
|
await logActivity({ appId: null, username: s.username, event: 'workspace', ip: clientIp(req), detail: `imported into workspace ${ws}: ${result.added} added, ${result.replaced} replaced, ${result.skipped} kept` });
|
|
121
129
|
s.state[RESULT] = JSON.stringify(result.lines);
|
|
122
130
|
} catch (e) {
|
|
123
|
-
flash(s,
|
|
131
|
+
flash(s, tx('wtr.failed', { reason: friendly(e) }), 'error');
|
|
124
132
|
}
|
|
125
133
|
return back(reply, s, page);
|
|
126
134
|
});
|
|
@@ -129,8 +137,8 @@ export async function workspaceTransferRoutes(app: FastifyInstance) {
|
|
|
129
137
|
/** A database refusal in words (the transaction rolled back). */
|
|
130
138
|
function friendly(e: unknown) {
|
|
131
139
|
const err = e as { message: string; constraint?: string; table?: string; code?: string };
|
|
132
|
-
if (err.code === '23514' || err.code === '23502' || err.code === '22P02') return
|
|
133
|
-
if (err.code === '23505') return
|
|
140
|
+
if (err.code === '23514' || err.code === '23502' || err.code === '22P02') return tx('wtr.refused_value', { detail: `${err.table ?? ''}${err.constraint ? `: ${err.constraint}` : ''}` });
|
|
141
|
+
if (err.code === '23505') return tx('wtr.duplicate', { detail: err.constraint ?? err.table ?? '' });
|
|
134
142
|
return err.message;
|
|
135
143
|
}
|
|
136
144
|
|
package/src/runtime/engine.ts
CHANGED
|
@@ -301,9 +301,10 @@ export async function validate(ctx: PageContext) {
|
|
|
301
301
|
};
|
|
302
302
|
try {
|
|
303
303
|
const outcome = await validationPasses(ctx, v, value);
|
|
304
|
-
|
|
304
|
+
// (166) &ITEM. in the message, as in APEX (plain text: the message is escaped where it is shown)
|
|
305
|
+
if (outcome !== true) report(typeof outcome === 'string' ? outcome : substitute(v.message, ctx, (x) => x));
|
|
305
306
|
} catch (e) {
|
|
306
|
-
report(`${v.message} (${await publicError(ctx, e, `validation ${v.name}`)})`);
|
|
307
|
+
report(`${substitute(v.message, ctx, (x) => x)} (${await publicError(ctx, e, `validation ${v.name}`)})`);
|
|
307
308
|
}
|
|
308
309
|
}
|
|
309
310
|
|
|
@@ -683,7 +684,9 @@ export async function runProcesses(ctx: PageContext, point: 'submit' | 'load') {
|
|
|
683
684
|
continue;
|
|
684
685
|
}
|
|
685
686
|
const msg = await runOne(ctx, p, names, 0);
|
|
686
|
-
|
|
687
|
+
// (166) &ITEM. in a success message, with the values after the process (a new row's key, say), as in APEX;
|
|
688
|
+
// once here, so a chain's messages (joined by runOne) are filled in once
|
|
689
|
+
if (msg) messages.push(substitute(msg, ctx, (x) => x));
|
|
687
690
|
}
|
|
688
691
|
return messages;
|
|
689
692
|
}
|
package/src/runtime/lists.ts
CHANGED
|
@@ -231,9 +231,9 @@ export function navbarMarkup(nodes: ListNode[], label: string, live?: string): R
|
|
|
231
231
|
if (!nodes.length) return raw('');
|
|
232
232
|
return html`<nav class="t-navbar" aria-label="${label}"><ul>${nodes.map((n) =>
|
|
233
233
|
n.children.length
|
|
234
|
-
? html`<li><details class="menu"><summary${hasIcon(n) ? html` class="nb-icon"
|
|
234
|
+
? html`<li><details class="menu"><summary${hasIcon(n) ? html` class="nb-icon"` : ''}>${icon(n.icon)}<span>${n.label}</span>${badge(n, live)}</summary><div class="menu-panel align-right"><ul class="t-navbar-menu">${
|
|
235
235
|
[...(n.href ? [n] : []), ...n.children].map((c) => html`<li><a ${linkAttrs(c)}>${icon(c.icon)}<span>${c.label}</span>${badge(c, live)}</a></li>`)}</ul></div></details></li>`
|
|
236
|
-
: html`<li><a ${linkAttrs(n, hasIcon(n) ? 'nb-icon' : undefined)}
|
|
236
|
+
: html`<li><a ${linkAttrs(n, hasIcon(n) ? 'nb-icon' : undefined)}>${icon(n.icon)}<span>${n.label}</span>${badge(n, live)}</a></li>`)}</ul></nav>`;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
function linksMarkup(nodes: ListNode[]): Raw {
|
package/src/runtime/push.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { loadContext, type Req } from './routes.ts';
|
|
|
12
12
|
// action push_subscribe: both after a click, as browsers require) and posts
|
|
13
13
|
// it here. The application's public key reaches the page as data-push.
|
|
14
14
|
|
|
15
|
+
let pushKeyWarned = false;
|
|
16
|
+
|
|
15
17
|
/** Whether the app offers notifications. */
|
|
16
18
|
export const pushOn = (a: Pick<App, 'pwa' | 'pwa_push'>) => a.pwa && a.pwa_push;
|
|
17
19
|
|
|
@@ -22,7 +24,11 @@ export async function pushBody(a: Pick<App, 'id' | 'pwa' | 'pwa_push'>, username
|
|
|
22
24
|
return { 'data-push': await appPublicKey(a.id), 'data-push-user': username.toLowerCase() };
|
|
23
25
|
} catch (e) {
|
|
24
26
|
// no key yet and no PGKILN_SECRET_KEY to store one: the page works, without notifications
|
|
25
|
-
|
|
27
|
+
// (166) said once per server, not on every page
|
|
28
|
+
if (!pushKeyWarned) {
|
|
29
|
+
pushKeyWarned = true;
|
|
30
|
+
console.error('push notifications:', (e as Error).message);
|
|
31
|
+
}
|
|
26
32
|
return {};
|
|
27
33
|
}
|
|
28
34
|
}
|
package/src/runtime/regions.ts
CHANGED
|
@@ -481,9 +481,12 @@ async function regionShell(ctx: PageContext, r: Region, hidden: Set<string>, bod
|
|
|
481
481
|
</dialog>`;
|
|
482
482
|
|
|
483
483
|
const header = html`<h2 id="${titleId}">${r.title}</h2>${buttonsOnTop ? buttons : ''}`;
|
|
484
|
+
// (166) the header buttons sit beside the summary, not in it: a summary is the toggle, and a button inside it is
|
|
485
|
+
// nested interactive content (axe); they come first so they are reached before the region's body
|
|
484
486
|
if (r.template === 'collapsible')
|
|
485
|
-
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr} aria-labelledby="${titleId}">
|
|
486
|
-
|
|
487
|
+
return html`<section class="${cls}${buttonsOnTop && buttons ? ' has-header-buttons' : ''}" id="R${r.id}"${staticAttr}${hiddenAttr} aria-labelledby="${titleId}">
|
|
488
|
+
${buttonsOnTop && buttons ? html`<div class="region-header-buttons">${buttons}</div>` : ''}
|
|
489
|
+
<details open><summary class="region-header"><h2 id="${titleId}">${r.title}</h2></summary>
|
|
487
490
|
<div class="region-body"${scrollable(r)}>${body}</div>
|
|
488
491
|
${!buttonsOnTop && buttons ? html`<footer class="region-footer">${buttons}</footer>` : ''}
|
|
489
492
|
</details></section>`;
|
|
@@ -518,9 +521,11 @@ async function slottedShell(ctx: PageContext, r: Region, hidden: Set<string>, bo
|
|
|
518
521
|
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr} aria-label="${r.title}">
|
|
519
522
|
${top ? html`<div class="region-toolbar">${top}</div>` : ''}${body}${footer}</section>`;
|
|
520
523
|
const header = html`<h2 id="${titleId}">${r.title}</h2>${top}`;
|
|
524
|
+
// (166) as above: the header buttons beside the summary, not in it
|
|
521
525
|
if (r.template === 'collapsible')
|
|
522
|
-
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr} aria-labelledby="${titleId}">
|
|
523
|
-
|
|
526
|
+
return html`<section class="${cls}${top ? ' has-header-buttons' : ''}" id="R${r.id}"${staticAttr}${hiddenAttr} aria-labelledby="${titleId}">
|
|
527
|
+
${top ? html`<div class="region-header-buttons">${top}</div>` : ''}
|
|
528
|
+
<details open><summary class="region-header"><h2 id="${titleId}">${r.title}</h2></summary><div class="region-body"${scrollable(r)}>${body}</div>${footer}</details></section>`;
|
|
524
529
|
return html`<section class="${cls}" id="R${r.id}"${staticAttr}${hiddenAttr}${r.title ? raw(` aria-labelledby="${titleId}"`) : ''}>
|
|
525
530
|
${r.title || top ? html`<header class="region-header">${header}</header>` : ''}
|
|
526
531
|
<div class="region-body"${scrollable(r)}>${body}</div>${footer}
|