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.
Files changed (91) hide show
  1. package/db/migrations/102_app_computations.sql +345 -0
  2. package/db/migrations/103_shortcuts.sql +341 -0
  3. package/db/migrations/104_report_queries.sql +413 -0
  4. package/db/migrations/105_component_settings_groups.sql +367 -0
  5. package/db/migrations/106_form_designer.sql +102 -0
  6. package/db/migrations/107_app_version.sql +9 -0
  7. package/docs/apex-feature-parity.md +15 -12
  8. package/docs/guide/02-concepts.md +24 -0
  9. package/docs/guide/03-builder.md +69 -13
  10. package/docs/guide/04-pages-and-regions.md +84 -3
  11. package/docs/guide/05-items.md +14 -2
  12. package/docs/guide/06-processing.md +22 -0
  13. package/docs/guide/09-reference.md +14 -1
  14. package/docs/guide/11-from-apex.md +2 -1
  15. package/docs/guide/12-development.md +8 -2
  16. package/docs/guide/16-files.md +23 -0
  17. package/docs/guide/18-cli.md +4 -0
  18. package/examples/hr/hr_61_form_designer.sql +80 -0
  19. package/package.json +1 -1
  20. package/public/app.css +79 -2
  21. package/public/app.js +307 -35
  22. package/public/builder.css +21 -1
  23. package/public/builder.js +67 -0
  24. package/public/sw.js +2 -1
  25. package/src/app.ts +9 -1
  26. package/src/appfiles.ts +18 -2
  27. package/src/builder/advisor.ts +7 -2
  28. package/src/builder/appwizard.ts +22 -6
  29. package/src/builder/arrange.ts +33 -12
  30. package/src/builder/code-editor.ts +7 -2
  31. package/src/builder/components.ts +86 -4
  32. package/src/builder/designer.ts +44 -17
  33. package/src/builder/errors.ts +65 -0
  34. package/src/builder/form-designer.ts +229 -0
  35. package/src/builder/forms.ts +1 -1
  36. package/src/builder/locks.ts +6 -0
  37. package/src/builder/newapp.ts +2 -1
  38. package/src/builder/object-browser.ts +81 -18
  39. package/src/builder/region-help.ts +2 -0
  40. package/src/builder/region-settings.ts +2 -0
  41. package/src/builder/reporter.ts +1 -1
  42. package/src/builder/routes.ts +7 -5
  43. package/src/builder/search.ts +3 -3
  44. package/src/builder/shared.ts +41 -8
  45. package/src/builder/subscriptions.ts +30 -4
  46. package/src/builder/ui.ts +2 -0
  47. package/src/cli/replace.ts +16 -2
  48. package/src/i18n/ar.ts +62 -0
  49. package/src/i18n/cs.ts +62 -0
  50. package/src/i18n/da.ts +62 -0
  51. package/src/i18n/de.ts +62 -0
  52. package/src/i18n/el.ts +62 -0
  53. package/src/i18n/es.ts +62 -0
  54. package/src/i18n/fi.ts +62 -0
  55. package/src/i18n/fr.ts +62 -0
  56. package/src/i18n/he.ts +62 -0
  57. package/src/i18n/it.ts +62 -0
  58. package/src/i18n/ja.ts +62 -0
  59. package/src/i18n/ko.ts +62 -0
  60. package/src/i18n/nb.ts +62 -0
  61. package/src/i18n/pl.ts +62 -0
  62. package/src/i18n/pt.ts +62 -0
  63. package/src/i18n/ru.ts +62 -0
  64. package/src/i18n/sv.ts +62 -0
  65. package/src/i18n/tr.ts +62 -0
  66. package/src/i18n/uk.ts +62 -0
  67. package/src/i18n/zh.ts +62 -0
  68. package/src/i18n.ts +124 -0
  69. package/src/metadata.ts +18 -3
  70. package/src/runtime/component-settings.ts +42 -0
  71. package/src/runtime/context.ts +4 -0
  72. package/src/runtime/devbar.ts +1 -1
  73. package/src/runtime/documents.ts +29 -3
  74. package/src/runtime/engine.ts +2 -2
  75. package/src/runtime/facet-state.ts +10 -5
  76. package/src/runtime/facets.ts +29 -5
  77. package/src/runtime/form-designer.ts +676 -0
  78. package/src/runtime/grid.ts +36 -11
  79. package/src/runtime/items.ts +47 -32
  80. package/src/runtime/lists.ts +1 -1
  81. package/src/runtime/logic.ts +12 -7
  82. package/src/runtime/pdf.ts +2 -1
  83. package/src/runtime/regions.ts +8 -3
  84. package/src/runtime/render.ts +8 -3
  85. package/src/runtime/report.ts +4 -3
  86. package/src/runtime/routes.ts +7 -3
  87. package/src/runtime/shortcuts.ts +43 -0
  88. package/src/runtime/smart-filters.ts +2 -1
  89. package/src/runtime/tasks.ts +6 -3
  90. package/src/security.ts +2 -1
  91. package/src/subscriptions.ts +67 -0
@@ -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')} <input type="date" name="from" form="${f}" required></label>
162
- <label>${t('tasks.vacation_until')} <input type="date" name="until" form="${f}" required></label>
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
- const date = (v: unknown) => (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : null);
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
- if (type.startsWith('text/html')) {
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.
@@ -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
+ }