pgkiln 0.44.0 → 0.46.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/102_app_computations.sql +345 -0
- package/db/migrations/103_shortcuts.sql +341 -0
- package/db/migrations/104_report_queries.sql +413 -0
- package/db/migrations/105_component_settings_groups.sql +367 -0
- package/db/migrations/106_form_designer.sql +102 -0
- package/db/migrations/107_app_version.sql +9 -0
- package/docs/apex-feature-parity.md +15 -12
- package/docs/guide/02-concepts.md +24 -0
- package/docs/guide/03-builder.md +69 -13
- package/docs/guide/04-pages-and-regions.md +84 -3
- package/docs/guide/05-items.md +14 -2
- package/docs/guide/06-processing.md +22 -0
- package/docs/guide/09-reference.md +14 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +8 -2
- package/docs/guide/16-files.md +23 -0
- package/docs/guide/18-cli.md +4 -0
- package/examples/hr/hr_61_form_designer.sql +80 -0
- package/package.json +1 -1
- package/public/app.css +79 -2
- package/public/app.js +307 -35
- package/public/builder.css +21 -1
- package/public/builder.js +67 -0
- package/public/sw.js +2 -1
- package/src/app.ts +9 -1
- package/src/appfiles.ts +18 -2
- package/src/builder/advisor.ts +7 -2
- package/src/builder/appwizard.ts +22 -6
- package/src/builder/arrange.ts +33 -12
- package/src/builder/code-editor.ts +7 -2
- package/src/builder/components.ts +86 -4
- package/src/builder/designer.ts +44 -17
- package/src/builder/errors.ts +65 -0
- package/src/builder/form-designer.ts +229 -0
- package/src/builder/forms.ts +1 -1
- package/src/builder/locks.ts +6 -0
- package/src/builder/newapp.ts +2 -1
- package/src/builder/object-browser.ts +81 -18
- package/src/builder/region-help.ts +2 -0
- package/src/builder/region-settings.ts +2 -0
- package/src/builder/reporter.ts +1 -1
- package/src/builder/routes.ts +7 -5
- package/src/builder/search.ts +3 -3
- package/src/builder/shared.ts +41 -8
- package/src/builder/subscriptions.ts +30 -4
- package/src/builder/ui.ts +2 -0
- package/src/cli/replace.ts +16 -2
- package/src/i18n/ar.ts +62 -0
- package/src/i18n/cs.ts +62 -0
- package/src/i18n/da.ts +62 -0
- package/src/i18n/de.ts +62 -0
- package/src/i18n/el.ts +62 -0
- package/src/i18n/es.ts +62 -0
- package/src/i18n/fi.ts +62 -0
- package/src/i18n/fr.ts +62 -0
- package/src/i18n/he.ts +62 -0
- package/src/i18n/it.ts +62 -0
- package/src/i18n/ja.ts +62 -0
- package/src/i18n/ko.ts +62 -0
- package/src/i18n/nb.ts +62 -0
- package/src/i18n/pl.ts +62 -0
- package/src/i18n/pt.ts +62 -0
- package/src/i18n/ru.ts +62 -0
- package/src/i18n/sv.ts +62 -0
- package/src/i18n/tr.ts +62 -0
- package/src/i18n/uk.ts +62 -0
- package/src/i18n/zh.ts +62 -0
- package/src/i18n.ts +124 -0
- package/src/metadata.ts +18 -3
- package/src/runtime/component-settings.ts +42 -0
- package/src/runtime/context.ts +4 -0
- package/src/runtime/devbar.ts +1 -1
- package/src/runtime/documents.ts +29 -3
- package/src/runtime/engine.ts +2 -2
- package/src/runtime/facet-state.ts +10 -5
- package/src/runtime/facets.ts +29 -5
- package/src/runtime/form-designer.ts +676 -0
- package/src/runtime/grid.ts +36 -11
- package/src/runtime/items.ts +47 -32
- package/src/runtime/lists.ts +1 -1
- package/src/runtime/logic.ts +12 -7
- package/src/runtime/pdf.ts +2 -1
- package/src/runtime/regions.ts +8 -3
- package/src/runtime/render.ts +8 -3
- package/src/runtime/report.ts +4 -3
- package/src/runtime/routes.ts +7 -3
- package/src/runtime/shortcuts.ts +43 -0
- package/src/runtime/smart-filters.ts +2 -1
- package/src/runtime/tasks.ts +6 -3
- package/src/security.ts +2 -1
- package/src/subscriptions.ts +67 -0
package/src/runtime/tasks.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Region } from '../metadata.ts';
|
|
|
6
6
|
import { logActivity, saveState } from '../session.ts';
|
|
7
7
|
import { pageAllowed } from './authz.ts';
|
|
8
8
|
import { publicError, stripSemicolon, type PageContext } from './context.ts';
|
|
9
|
+
import { appDateMask, appDateReader, dateField } from './items.ts';
|
|
9
10
|
import { linkAttrs } from './links.ts';
|
|
10
11
|
import { cell } from './report.ts';
|
|
11
12
|
import { loadContext, safeNext, txContext, type Req } from './routes.ts';
|
|
@@ -158,8 +159,8 @@ async function vacationPanel(ctx: PageContext, r: Region): Promise<Raw | ''> {
|
|
|
158
159
|
: html`<p class="muted">${t('tasks.vacation_help')}</p>
|
|
159
160
|
<div class="vacation-fields">
|
|
160
161
|
<label>${t('tasks.vacation_to')} <input name="to" form="${f}" maxlength="100" required autocomplete="off"></label>
|
|
161
|
-
<label>${t('tasks.vacation_from')}
|
|
162
|
-
<label>${t('tasks.vacation_until')}
|
|
162
|
+
<label>${t('tasks.vacation_from')} ${dateField(ctx, { id: `${f}_from`, iso: '', mask: appDateMask(ctx), attrs: html` name="from" form="${f}" required` })}</label>
|
|
163
|
+
<label>${t('tasks.vacation_until')} ${dateField(ctx, { id: `${f}_until`, iso: '', mask: appDateMask(ctx), attrs: html` name="until" form="${f}" required` })}</label>
|
|
163
164
|
<button class="btn" form="${f}" name="action" value="set">${t('tasks.vacation_save')}</button></div>`}
|
|
164
165
|
</details>`;
|
|
165
166
|
}
|
|
@@ -173,7 +174,9 @@ export async function taskRoutes(app: FastifyInstance) {
|
|
|
173
174
|
const next = safeNext(ctx.app, b.next);
|
|
174
175
|
if (b.__csrf !== ctx.session.csrf_token || !['set', 'clear'].includes(String(b.action))) return reply.redirect(next, 303);
|
|
175
176
|
const t = ctx.locale.t;
|
|
176
|
-
|
|
177
|
+
// (sprint 84) typed in the application's date format (or YYYY-MM-DD)
|
|
178
|
+
const readDate = appDateReader(ctx);
|
|
179
|
+
const date = (v: unknown) => (typeof v === 'string' && v.length <= 100 ? readDate(v) : null);
|
|
177
180
|
try {
|
|
178
181
|
await appTx(txContext(ctx), async (c) => {
|
|
179
182
|
if (b.action === 'clear') await c.query('select meta.clear_vacation_rule()');
|
package/src/security.ts
CHANGED
|
@@ -96,7 +96,8 @@ export function securityHeaders(app: FastifyInstance) {
|
|
|
96
96
|
// the app's own pages may use the camera (photos, scanning) and the position (location items); nothing else may
|
|
97
97
|
reply.header('Permissions-Policy', 'camera=(self), microphone=(), geolocation=(self)');
|
|
98
98
|
const type = String(reply.getHeader('content-type') ?? '');
|
|
99
|
-
|
|
99
|
+
// a file's own stricter policy (downloads, uploaded files: default-src 'none'; sandbox) is kept
|
|
100
|
+
if (type.startsWith('text/html') && !reply.hasHeader('content-security-policy')) {
|
|
100
101
|
// No inline scripts and no inline styles: behaviour lives in /static/app.js,
|
|
101
102
|
// styling in /static/app.css. The only <style> a page has (theme colours,
|
|
102
103
|
// chart geometry) carries this response's nonce; style="" attributes are refused.
|
package/src/subscriptions.ts
CHANGED
|
@@ -219,3 +219,70 @@ export async function publish(masterId: number, kind: string, name: string, user
|
|
|
219
219
|
|
|
220
220
|
export const unsubscribe = async (appId: number, kind: string, name: string) =>
|
|
221
221
|
((await owner.query('delete from meta.subscription where app_id = $1 and kind = $2 and name = $3', [appId, kind, name])).rowCount ?? 0) > 0;
|
|
222
|
+
|
|
223
|
+
// ------------------------------------------------------------------ (105) component groups
|
|
224
|
+
// A component group names some of an application's subscribable components
|
|
225
|
+
// ("kind:name"); another application subscribes to all of them at once.
|
|
226
|
+
|
|
227
|
+
/** The kinds a component group can hold: every subscribable kind but the theme. */
|
|
228
|
+
export const GROUP_KINDS = (Object.keys(KINDS) as Kind[]).filter((k) => k !== 'theme');
|
|
229
|
+
|
|
230
|
+
/** Members as the application has them ("lov:JOBS"), and a message for each that it doesn't have. */
|
|
231
|
+
export async function checkGroupMembers(appId: number, members: string[]) {
|
|
232
|
+
const ok: string[] = [];
|
|
233
|
+
const problems: string[] = [];
|
|
234
|
+
for (const m of members.map((x) => x.trim()).filter(Boolean)) {
|
|
235
|
+
const at = m.indexOf(':');
|
|
236
|
+
const kind = m.slice(0, at).trim();
|
|
237
|
+
const name = m.slice(at + 1).trim();
|
|
238
|
+
if (at < 1 || !isKind(kind) || kind === 'theme') {
|
|
239
|
+
problems.push(`${m}: write kind:name, the kind one of ${GROUP_KINDS.join(', ')}.`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const k = KINDS[kind];
|
|
243
|
+
const row = await owner.one<{ n: string }>(`select ${k.key} as n from ${k.table} where app_id = $1 and lower(${k.key}) = lower($2)`, [appId, name]);
|
|
244
|
+
if (!row) problems.push(`${m}: the application has no ${k.label.toLowerCase()} ${name}.`);
|
|
245
|
+
else if (!ok.includes(`${kind}:${row.n}`)) ok.push(`${kind}:${row.n}`);
|
|
246
|
+
}
|
|
247
|
+
return { members: ok, problems };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Component groups of the theme and library applications appId can subscribe to. */
|
|
251
|
+
export async function groupsOffered(appId: number) {
|
|
252
|
+
return (
|
|
253
|
+
await owner.query<{ master_id: number; master_name: string; name: string; description: string | null; members: string[] }>(
|
|
254
|
+
`select m.id as master_id, m.name as master_name, g.name, g.description, g.members
|
|
255
|
+
from meta.component_group g join meta.app m on m.id = g.app_id
|
|
256
|
+
where m.app_type in ('theme', 'library') and m.id <> $1 and meta.app_workspace(m.id) = meta.app_workspace($1)
|
|
257
|
+
order by lower(m.name), m.id, lower(g.name)`,
|
|
258
|
+
[appId],
|
|
259
|
+
)
|
|
260
|
+
).rows;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Subscribe appId to every member of a master's component group; members the master no longer has are skipped. */
|
|
264
|
+
export async function subscribeGroup(appId: number, masterId: number, group: string, username: string) {
|
|
265
|
+
const g = await owner.one<{ members: string[] }>('select members from meta.component_group where app_id = $1 and name = $2', [masterId, group]);
|
|
266
|
+
if (!g) throw new SubscriptionError('Choose a component group.');
|
|
267
|
+
const done: string[] = [];
|
|
268
|
+
const skipped: string[] = [];
|
|
269
|
+
for (const m of g.members) {
|
|
270
|
+
const at = m.indexOf(':');
|
|
271
|
+
const kind = m.slice(0, at);
|
|
272
|
+
const name = m.slice(at + 1);
|
|
273
|
+
if (!isKind(kind) || kind === 'theme') continue;
|
|
274
|
+
const k = KINDS[kind];
|
|
275
|
+
if (!(await owner.one(`select 1 as ok from ${k.table} where app_id = $1 and ${k.key} = $2`, [masterId, name]))) {
|
|
276
|
+
skipped.push(m);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
await subscribe(appId, masterId, kind, name, username);
|
|
281
|
+
done.push(m);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
if (!(e instanceof SubscriptionError)) throw e;
|
|
284
|
+
skipped.push(m);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { done, skipped };
|
|
288
|
+
}
|