pgkiln 0.86.0 → 0.88.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/156_pwa_prefetch.sql +13 -0
- package/docs/apex-feature-parity.md +4 -4
- package/docs/guide/04-pages-and-regions.md +23 -4
- package/docs/guide/05-items.md +20 -1
- package/docs/guide/07-dynamic-actions.md +1 -1
- package/docs/guide/08-security.md +6 -2
- package/docs/guide/09-reference.md +1 -1
- package/docs/guide/11-from-apex.md +3 -0
- package/docs/guide/17-mobile.md +35 -4
- package/examples/gallery/samples/sample-dynamic-actions.sql +7 -4
- package/examples/gallery/samples/sample-grids.sql +9 -29
- package/examples/gallery/samples/ux-patterns.sql +3 -3
- package/examples/hr/hr_82_pwa_prefetch.sql +11 -0
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/app.js +13 -0
- package/public/sw.js +51 -1
- package/src/builder/components.ts +1 -1
- package/src/builder/i18n/de.ts +6 -0
- package/src/builder/i18n/en.ts +6 -0
- package/src/builder/i18n/es.ts +6 -0
- package/src/builder/i18n/fr.ts +6 -0
- package/src/builder/i18n/it.ts +6 -0
- package/src/builder/i18n/ja.ts +6 -0
- package/src/builder/i18n/ko.ts +6 -0
- package/src/builder/i18n/nl.ts +6 -0
- package/src/builder/i18n/pt.ts +6 -0
- package/src/builder/i18n/zh-cn.ts +6 -0
- package/src/builder/i18n/zh-tw.ts +6 -0
- package/src/builder/pwa.ts +14 -3
- package/src/builder/region-help.ts +2 -2
- package/src/metadata.ts +3 -1
- package/src/runtime/authz.ts +7 -0
- package/src/runtime/grid.ts +69 -9
- package/src/runtime/items.ts +9 -1
- package/src/runtime/pwa.ts +8 -3
- package/src/runtime/regions.ts +16 -4
- package/src/runtime/routes.ts +25 -6
- package/src/runtime/signin.ts +5 -5
package/src/runtime/routes.ts
CHANGED
|
@@ -444,24 +444,37 @@ export async function runtimeRoutes(app: FastifyInstance) {
|
|
|
444
444
|
const normalised = normaliseReportParams(ctx.params);
|
|
445
445
|
if (normalised !== null) return reply.redirect(`${ctx.base}/${ctx.page.page_no}${normalised ? `?${normalised}` : ''}`);
|
|
446
446
|
|
|
447
|
+
// (168) the service worker fetching the page for offline use (pwa_prefetch_hours): the page runs read-only,
|
|
448
|
+
// its session state isn't kept and nothing waiting for the next page (a message) is taken; a page that runs
|
|
449
|
+
// processes when it opens is refused (kept only when the user opens it)
|
|
450
|
+
const prefetch = req.headers['x-pgkiln-prefetch'] === '1';
|
|
451
|
+
if (prefetch && (ctx.dialog || ctx.page.processes.some((p) => p.point === 'load')))
|
|
452
|
+
return reply.code(409).header('cache-control', 'no-store').send('Not kept for offline use: this page runs processes when it opens.');
|
|
453
|
+
|
|
447
454
|
if (!applyUrlItems(ctx)) return forbidden(ctx, reply, ctx.locale.t('error.checksum'), `checksum error: ${req.url}`);
|
|
448
455
|
// a master grid's selected row (signed link) into its item
|
|
449
456
|
applyRowSelection(ctx);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
457
|
+
if (!prefetch) {
|
|
458
|
+
const flash = takeFlash(ctx.session);
|
|
459
|
+
if (flash) ctx.messages.push(flash);
|
|
460
|
+
const flashError = takeFlash(ctx.session, '__FLASH_ERROR');
|
|
461
|
+
if (flashError) ctx.errors.page.push(flashError);
|
|
462
|
+
const saved = takeFlash(ctx.session, '__GRID_SAVED');
|
|
463
|
+
if (typeof saved === 'string') ctx.savedGrids = saved.split(',').map(Number).filter(Number.isInteger);
|
|
464
|
+
}
|
|
456
465
|
// Actions → Download CSV / Excel / PDF: r<region id>_csv=1, _xlsx=1 or _pdf=1
|
|
457
466
|
const downloadKey = [...ctx.params.keys()].find((k) => /^r\d+_(csv|xlsx|pdf)$/.test(k));
|
|
458
467
|
// a document template: ?doc=NAME (see documents.ts)
|
|
459
468
|
const docName = ctx.params.get('doc');
|
|
460
469
|
|
|
470
|
+
if (prefetch && (docName || downloadKey)) return reply.code(409).header('cache-control', 'no-store').send('Not kept for offline use.');
|
|
471
|
+
|
|
461
472
|
let result: { html?: string; streamed?: boolean; file?: Buffer; type?: string; name?: string; redirect?: string; download?: Download };
|
|
462
473
|
try {
|
|
463
474
|
result = await appTx(txContext(ctx), async (c) => {
|
|
464
475
|
ctx.client = c;
|
|
476
|
+
// (168) a prefetch writes nothing: an application process or region that tries fails
|
|
477
|
+
if (prefetch) await c.query('set local transaction_read_only = on');
|
|
465
478
|
await timed(ctx, 4, 'page', 'page authorization', () => checkPageAccess(ctx));
|
|
466
479
|
await timed(ctx, 4, 'page', 'application processes (before page)', () => runAppProcesses(ctx, 'before_page'));
|
|
467
480
|
// (086) items kept per user: their values from the user's preferences
|
|
@@ -516,6 +529,12 @@ export async function runtimeRoutes(app: FastifyInstance) {
|
|
|
516
529
|
if (e instanceof Forbidden) return forbidden(ctx, reply, e.message, `page ${ctx.page.page_no}`);
|
|
517
530
|
throw e;
|
|
518
531
|
}
|
|
532
|
+
if (prefetch) {
|
|
533
|
+
// a redirect (a branch, a sign-in) or anything but the page itself is not kept on the device
|
|
534
|
+
logActivity({ appId: ctx.app.id, pageNo: ctx.page.page_no, username: ctx.user, event: 'page_prefetch', ip: ctx.ip, elapsedMs: Math.round(performance.now() - started) });
|
|
535
|
+
if (result.html === undefined) return reply.code(409).header('cache-control', 'no-store').send('Not kept for offline use.');
|
|
536
|
+
return reply.type('text/html').header('cache-control', 'private, no-store').send(result.html);
|
|
537
|
+
}
|
|
519
538
|
await saveState(ctx.session);
|
|
520
539
|
if (result.redirect) return reply.redirect(result.redirect, 303);
|
|
521
540
|
logActivity({ appId: ctx.app.id, pageNo: ctx.page.page_no, username: ctx.user, event: 'page_view', ip: ctx.ip, elapsedMs: Math.round(performance.now() - started) });
|
package/src/runtime/signin.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const LOGIN_USER = '__LOGIN_USER';
|
|
|
34
34
|
|
|
35
35
|
export async function signInForm(
|
|
36
36
|
app: App, locale: Locale, session: Session, next: string,
|
|
37
|
-
o: { error?: string; username?: string; formId?: string } = {},
|
|
37
|
+
o: { error?: string; username?: string; formId?: string; autofocus?: boolean } = {},
|
|
38
38
|
): Promise<Raw> {
|
|
39
39
|
const t = locale.t;
|
|
40
40
|
// database accounts and custom authentication: the user name and password form only
|
|
@@ -48,7 +48,7 @@ export async function signInForm(
|
|
|
48
48
|
const fields = html`<input type="hidden" name="__csrf" value="${session.csrf_token}"${fa}>
|
|
49
49
|
<input type="hidden" name="next" value="${next}"${fa}>
|
|
50
50
|
${app.time_zone_auto ? html`<input type="hidden" name="__tz" value=""${fa}>` : ''}
|
|
51
|
-
<div class="field"><label class="label" for="username">${t('login.username')}</label><input id="username" name="username" value="${o.username ?? ''}" autocomplete="username" autofocus required maxlength="100"${fa}></div>
|
|
51
|
+
<div class="field"><label class="label" for="username">${t('login.username')}</label><input id="username" name="username" value="${o.username ?? ''}" autocomplete="username"${o.autofocus === false ? '' : raw(' autofocus')} required maxlength="100"${fa}></div>
|
|
52
52
|
<div class="field"><label class="label" for="password">${t('login.password')}</label><input id="password" name="password" type="password" autocomplete="current-password" required maxlength="200"${fa}></div>
|
|
53
53
|
${remember ? html`<label class="check"><input type="checkbox" name="remember" value="true"${fa}> ${t('login.remember', { days: remember })}</label>` : ''}
|
|
54
54
|
<button class="btn btn-hot"${fa}>${t('login.submit')}</button>
|
|
@@ -65,8 +65,8 @@ export async function signInForm(
|
|
|
65
65
|
: providers.length ? '' : html`<p>${t('login.none')}</p>`}`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** The "login" region of an application's own login page. */
|
|
69
|
-
export async function renderLoginRegion(ctx: PageContext): Promise<Raw> {
|
|
68
|
+
/** The "login" region of an application's own login page; (167) {"autofocus": false} leaves the focus where the page puts it. */
|
|
69
|
+
export async function renderLoginRegion(ctx: PageContext, config: { autofocus?: unknown } = {}): Promise<Raw> {
|
|
70
70
|
const s = ctx.session.state;
|
|
71
71
|
const error = s[LOGIN_ERROR] ?? undefined;
|
|
72
72
|
const username = s[LOGIN_USER] ?? undefined;
|
|
@@ -76,6 +76,6 @@ export async function renderLoginRegion(ctx: PageContext): Promise<Raw> {
|
|
|
76
76
|
ctx.detached.push(html`<form id="${formId}" method="post" action="/a/${ctx.app.alias}/login"></form>`);
|
|
77
77
|
const next = safeNext(ctx.app, ctx.params.get('next') ?? undefined);
|
|
78
78
|
const here = `${ctx.base}/${ctx.page.page_no}${ctx.params.get('next') ? `?next=${encodeURIComponent(next)}` : ''}`;
|
|
79
|
-
return html`<div class="login-region">${await signInForm(ctx.app, ctx.locale, ctx.session, next, { error, username, formId })}
|
|
79
|
+
return html`<div class="login-region">${await signInForm(ctx.app, ctx.locale, ctx.session, next, { error, username, formId, autofocus: config.autofocus !== false })}
|
|
80
80
|
${languagePicker(ctx.app, ctx.locale, here)}</div>`;
|
|
81
81
|
}
|