pgkiln 0.72.0 → 0.73.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/140_item_region_types.sql +27 -0
- package/docs/apex-feature-parity.md +84 -8
- package/docs/guide/04-pages-and-regions.md +41 -1
- package/docs/guide/05-items.md +43 -3
- package/docs/guide/11-from-apex.md +3 -0
- package/docs/guide/12-development.md +10 -2
- package/examples/hr/hr_75_item_region_types.sql +81 -0
- package/package.json +1 -1
- package/public/app.css +48 -0
- package/public/app.js +377 -1
- package/src/builder/advisor.ts +5 -1
- package/src/builder/arrange.ts +7 -0
- package/src/builder/components.ts +5 -5
- package/src/builder/designer.ts +8 -2
- 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/region-help.ts +8 -2
- package/src/i18n/ar.ts +13 -0
- package/src/i18n/cs.ts +13 -0
- package/src/i18n/da.ts +13 -0
- package/src/i18n/de.ts +13 -0
- package/src/i18n/el.ts +13 -0
- package/src/i18n/es.ts +13 -0
- package/src/i18n/fi.ts +13 -0
- package/src/i18n/fr.ts +13 -0
- package/src/i18n/he.ts +13 -0
- package/src/i18n/it.ts +13 -0
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/nb.ts +13 -0
- package/src/i18n/pl.ts +13 -0
- package/src/i18n/pt.ts +13 -0
- package/src/i18n/ru.ts +13 -0
- package/src/i18n/sv.ts +13 -0
- package/src/i18n/tr.ts +13 -0
- package/src/i18n/uk.ts +13 -0
- package/src/i18n/zh.ts +13 -0
- package/src/i18n.ts +26 -0
- package/src/metadata.ts +5 -2
- package/src/runtime/items.ts +32 -3
- package/src/runtime/page-regions.ts +50 -0
- package/src/runtime/region-cache.ts +1 -1
- package/src/runtime/regions.ts +11 -0
- package/src/runtime/render.ts +3 -17
- package/src/runtime/report.ts +55 -3
package/src/i18n/uk.ts
CHANGED
|
@@ -424,6 +424,19 @@ export const uk: Record<MessageKey, string> = {
|
|
|
424
424
|
'lov.loading': "Завантаження…",
|
|
425
425
|
'da.wait': "Зачекайте…",
|
|
426
426
|
'lov.no_rows': "Нічого не знайдено.",
|
|
427
|
+
'shuttle.available': "Доступні",
|
|
428
|
+
'shuttle.chosen': "Вибрані",
|
|
429
|
+
'shuttle.move': "Перемістити",
|
|
430
|
+
'shuttle.move_all': "Перемістити всі",
|
|
431
|
+
'shuttle.remove': "Прибрати",
|
|
432
|
+
'shuttle.remove_all': "Прибрати всі",
|
|
433
|
+
'shuttle.up': "Вгору",
|
|
434
|
+
'shuttle.down': "Вниз",
|
|
435
|
+
'shuttle.top': "На початок",
|
|
436
|
+
'shuttle.bottom': "У кінець",
|
|
437
|
+
'shuttle.filter': "Фільтр",
|
|
438
|
+
'region.breadcrumb': "Навігаційний ланцюжок",
|
|
439
|
+
'region.no_help': "Для цієї сторінки немає довідки.",
|
|
427
440
|
'search.did_you_mean': "Можливо, ви мали на увазі:",
|
|
428
441
|
'error.lov_value': "{label}: виберіть значення зі списку.",
|
|
429
442
|
'item.yes': "Так",
|
package/src/i18n/zh.ts
CHANGED
|
@@ -424,6 +424,19 @@ export const zh: Record<MessageKey, string> = {
|
|
|
424
424
|
'lov.loading': "正在加载更多…",
|
|
425
425
|
'da.wait': "请稍候…",
|
|
426
426
|
'lov.no_rows': "未找到任何内容。",
|
|
427
|
+
'shuttle.available': "可选",
|
|
428
|
+
'shuttle.chosen': "已选",
|
|
429
|
+
'shuttle.move': "移入",
|
|
430
|
+
'shuttle.move_all': "全部移入",
|
|
431
|
+
'shuttle.remove': "移除",
|
|
432
|
+
'shuttle.remove_all': "全部移除",
|
|
433
|
+
'shuttle.up': "上移",
|
|
434
|
+
'shuttle.down': "下移",
|
|
435
|
+
'shuttle.top': "移到顶部",
|
|
436
|
+
'shuttle.bottom': "移到底部",
|
|
437
|
+
'shuttle.filter': "筛选",
|
|
438
|
+
'region.breadcrumb': "面包屑导航",
|
|
439
|
+
'region.no_help': "此页面没有帮助信息。",
|
|
427
440
|
'search.did_you_mean': "您是不是要找:",
|
|
428
441
|
'error.lov_value': "{label}:请从列表中选择一个值。",
|
|
429
442
|
'item.yes': "是",
|
package/src/i18n.ts
CHANGED
|
@@ -451,6 +451,19 @@ const en = {
|
|
|
451
451
|
'lov.loading': 'Loading more…',
|
|
452
452
|
'da.wait': 'Please wait…',
|
|
453
453
|
'lov.no_rows': 'Nothing found.',
|
|
454
|
+
'shuttle.available': 'Available',
|
|
455
|
+
'shuttle.chosen': 'Selected',
|
|
456
|
+
'shuttle.move': 'Move',
|
|
457
|
+
'shuttle.move_all': 'Move all',
|
|
458
|
+
'shuttle.remove': 'Remove',
|
|
459
|
+
'shuttle.remove_all': 'Remove all',
|
|
460
|
+
'shuttle.up': 'Move up',
|
|
461
|
+
'shuttle.down': 'Move down',
|
|
462
|
+
'shuttle.top': 'Move to top',
|
|
463
|
+
'shuttle.bottom': 'Move to bottom',
|
|
464
|
+
'shuttle.filter': 'Filter',
|
|
465
|
+
'region.breadcrumb': 'Breadcrumb',
|
|
466
|
+
'region.no_help': 'No help is available for this page.',
|
|
454
467
|
'search.did_you_mean': 'Did you mean:',
|
|
455
468
|
'error.lov_value': '{label}: choose a value from the list.',
|
|
456
469
|
'item.yes': 'Yes',
|
|
@@ -1319,6 +1332,19 @@ const nl: Record<MessageKey, string> = {
|
|
|
1319
1332
|
'lov.loading': 'Meer laden…',
|
|
1320
1333
|
'da.wait': 'Even geduld…',
|
|
1321
1334
|
'lov.no_rows': 'Niets gevonden.',
|
|
1335
|
+
'shuttle.available': 'Beschikbaar',
|
|
1336
|
+
'shuttle.chosen': 'Gekozen',
|
|
1337
|
+
'shuttle.move': 'Verplaatsen',
|
|
1338
|
+
'shuttle.move_all': 'Alles verplaatsen',
|
|
1339
|
+
'shuttle.remove': 'Verwijderen',
|
|
1340
|
+
'shuttle.remove_all': 'Alles verwijderen',
|
|
1341
|
+
'shuttle.up': 'Omhoog',
|
|
1342
|
+
'shuttle.down': 'Omlaag',
|
|
1343
|
+
'shuttle.top': 'Bovenaan',
|
|
1344
|
+
'shuttle.bottom': 'Onderaan',
|
|
1345
|
+
'shuttle.filter': 'Filteren',
|
|
1346
|
+
'region.breadcrumb': 'Kruimelpad',
|
|
1347
|
+
'region.no_help': 'Er is geen help voor deze pagina.',
|
|
1322
1348
|
'search.did_you_mean': 'Bedoelde u:',
|
|
1323
1349
|
'error.lov_value': '{label}: kies een waarde uit de lijst.',
|
|
1324
1350
|
'item.yes': 'Ja',
|
package/src/metadata.ts
CHANGED
|
@@ -159,7 +159,7 @@ export interface Region {
|
|
|
159
159
|
title: string | null;
|
|
160
160
|
/** (0.31, 073) optional, unique on the page */
|
|
161
161
|
static_id?: string | null;
|
|
162
|
-
type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search' | 'form_designer';
|
|
162
|
+
type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search' | 'form_designer' | 'classic_report' | 'breadcrumb' | 'help_text';
|
|
163
163
|
source: string | null;
|
|
164
164
|
table_name: string | null;
|
|
165
165
|
pk_column: string | null;
|
|
@@ -182,6 +182,7 @@ export type ItemType =
|
|
|
182
182
|
| 'checkbox' | 'switch' | 'hidden' | 'display' | 'password'
|
|
183
183
|
| 'checkbox_group' | 'multiselect' | 'popup_lov' | 'email' | 'tel' | 'url' | 'color' | 'file' | 'location'
|
|
184
184
|
| 'richtext' | 'markdown' | 'rating' | 'combobox' | 'daterange' | 'qrcode' | 'plugin'
|
|
185
|
+
| 'shuttle' | 'select_one' | 'select_many'
|
|
185
186
|
// (086) display image, geocoded address
|
|
186
187
|
| 'image' | 'address';
|
|
187
188
|
|
|
@@ -363,6 +364,8 @@ export interface Page extends PageSummary {
|
|
|
363
364
|
warn_unsaved: boolean;
|
|
364
365
|
/** (087) 'minimal': no header and navigation */
|
|
365
366
|
page_template: 'standard' | 'minimal' | 'left_side_column' | 'right_side_column';
|
|
367
|
+
/** (140) the page's help text (Markdown), shown by a help_text region */
|
|
368
|
+
help: string | null;
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
/** The static files a list of names refers to, in that order, with their version. */
|
|
@@ -406,7 +409,7 @@ export async function loadApp(alias: string) {
|
|
|
406
409
|
export async function loadPage(appId: number, pageNo: number) {
|
|
407
410
|
return runtime.one<Page>(
|
|
408
411
|
`select p.id, p.app_id, p.page_no, p.name, p.title, p.requires_auth, p.parent_page, p.mode,
|
|
409
|
-
p.protection, p.authz, p.warn_unsaved, p.page_template,
|
|
412
|
+
p.protection, p.authz, p.warn_unsaved, p.page_template, p.help,
|
|
410
413
|
${agg('meta.region', 'page_id', 'p', 'p.app_id')} as regions,
|
|
411
414
|
${agg('meta.item', 'page_id', 'p', 'p.app_id')} as items,
|
|
412
415
|
${agg('meta.button', 'page_id', 'p', 'p.app_id')} as buttons,
|
package/src/runtime/items.ts
CHANGED
|
@@ -159,7 +159,7 @@ export async function lovContains(ctx: PageContext, lov: string | null, values:
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/** Item types whose posted value must come from their list of values (a combobox takes free text by design). */
|
|
162
|
-
export const LOV_CHECKED = new Set(['select', 'radio', 'checkbox_group', 'multiselect', 'popup_lov']);
|
|
162
|
+
export const LOV_CHECKED = new Set(['select', 'radio', 'checkbox_group', 'multiselect', 'popup_lov', 'shuttle', 'select_one', 'select_many']);
|
|
163
163
|
|
|
164
164
|
/** Rows a list of values reads when its item sets no config.max_rows (1 to 50,000). */
|
|
165
165
|
export const LOV_MAX_ROWS = 5000;
|
|
@@ -168,9 +168,10 @@ export const lovMax = (item: Item, settings?: ComponentSettings) => {
|
|
|
168
168
|
return Number.isFinite(n) && n >= 1 ? Math.min(n, 50_000) : settings ? componentSetting(settings, 'lov_max_rows') : LOV_MAX_ROWS;
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
/** Item types that show a list of values (the Page Designer's "List of values"). */
|
|
172
|
+
export const LOV_TYPES = new Set(['select', 'radio', 'checkbox_group', 'multiselect', 'popup_lov', 'combobox', 'shuttle', 'select_one', 'select_many']);
|
|
172
173
|
const hasLov = (item: Item) => LOV_TYPES.has(item.type) || (item.type === 'display' && !!item.lov);
|
|
173
|
-
export const MULTI_VALUE = new Set(['checkbox_group', 'multiselect']);
|
|
174
|
+
export const MULTI_VALUE = new Set(['checkbox_group', 'multiselect', 'shuttle', 'select_many']);
|
|
174
175
|
/** Multi-value items store their values colon-separated, as in APEX. */
|
|
175
176
|
export const splitValues = (v: string) => (v ? v.split(':') : []);
|
|
176
177
|
|
|
@@ -324,6 +325,34 @@ export async function renderItem(ctx: PageContext, item: Item, hiddenByDa = fals
|
|
|
324
325
|
</select>`;
|
|
325
326
|
break;
|
|
326
327
|
}
|
|
328
|
+
case 'shuttle': {
|
|
329
|
+
// (140; APEX: Shuttle) a <select multiple> that works without JavaScript; app.js turns it into two lists
|
|
330
|
+
// (available, chosen) with move and order buttons. The chosen values keep their order: they come first.
|
|
331
|
+
const chosen = splitValues(value);
|
|
332
|
+
const set = new Set(chosen);
|
|
333
|
+
const indexed = options.map((o, i) => ({ ...o, i }));
|
|
334
|
+
const ordered = [...chosen.flatMap((v) => indexed.filter((o) => o.value === v).slice(0, 1)), ...indexed.filter((o) => !set.has(o.value))];
|
|
335
|
+
const t = ctx.locale.t;
|
|
336
|
+
control = html`<div class="shuttle" data-shuttle="${id}" data-filter-label="${t('shuttle.filter')}" data-available-label="${t('shuttle.available')}" data-chosen-label="${t('shuttle.chosen')}" data-move-label="${t('shuttle.move')}" data-move-all-label="${t('shuttle.move_all')}" data-remove-label="${t('shuttle.remove')}" data-remove-all-label="${t('shuttle.remove_all')}" data-up-label="${t('shuttle.up')}" data-down-label="${t('shuttle.down')}" data-top-label="${t('shuttle.top')}" data-bottom-label="${t('shuttle.bottom')}">
|
|
337
|
+
<select id="${id}" name="${id}" multiple size="${Math.min(Math.max(options.length, 4), 10)}"${aria}>
|
|
338
|
+
${ordered.map((o) => html`<option value="${o.value}" data-i="${o.i}"${set.has(o.value) ? raw(' selected') : ''}>${o.display}</option>`)}
|
|
339
|
+
</select></div>`;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
case 'select_one':
|
|
343
|
+
case 'select_many': {
|
|
344
|
+
// (140; APEX 24.2: Select One, Select Many) a <select> (or <select multiple>) that works without JavaScript;
|
|
345
|
+
// app.js adds a search field with a list box that filters the options as one types, and chips for the chosen values
|
|
346
|
+
const many = item.type === 'select_many';
|
|
347
|
+
const chosen = new Set(many ? splitValues(value) : value ? [value] : []);
|
|
348
|
+
const t = ctx.locale.t;
|
|
349
|
+
control = html`<div class="select-pick" data-select-pick="${many ? 'many' : 'one'}" data-search-label="${t('lov.search')}" data-none-label="${t('lov.no_rows')}" data-remove-label="${t('item.tag_remove')}">
|
|
350
|
+
<select id="${id}" name="${id}"${many ? html` multiple size="${Math.min(Math.max(options.length, 3), 8)}"` : ''}${aria}>
|
|
351
|
+
${many || item.config?.null_label === false ? '' : html`<option value="">${item.config?.null_label ?? t('lov.none')}</option>`}
|
|
352
|
+
${options.map((o) => html`<option value="${o.value}"${chosen.has(o.value) ? raw(' selected') : ''}>${o.display}</option>`)}
|
|
353
|
+
</select></div>`;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
327
356
|
case 'popup_lov': {
|
|
328
357
|
// a select list (the LOV's first max_rows rows) that works without JavaScript; app.js
|
|
329
358
|
// turns it into a field with a button that searches the whole LOV on the server
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// (140) Two region types about the page itself: breadcrumb (APEX: Breadcrumb) shows the page's
|
|
2
|
+
// breadcrumb trail, the pages above it by "Breadcrumb parent" (else Home), and help_text (APEX: Help
|
|
3
|
+
// Text) shows the page's help text. The page's title bar shows the trail too, unless the page has a
|
|
4
|
+
// breadcrumb region of its own.
|
|
5
|
+
|
|
6
|
+
import { html, raw, type Raw } from '../html.ts';
|
|
7
|
+
import type { Region } from '../metadata.ts';
|
|
8
|
+
import { markdownHtml } from '../richtext.ts';
|
|
9
|
+
import { pageAllowed } from './authz.ts';
|
|
10
|
+
import { substitute, type PageContext } from './context.ts';
|
|
11
|
+
import { expandShortcuts } from './shortcuts.ts';
|
|
12
|
+
|
|
13
|
+
/** The pages above the current one (at most six), as links the user may follow. */
|
|
14
|
+
export async function breadcrumbLinks(ctx: PageContext): Promise<Raw[]> {
|
|
15
|
+
const trail: { page_no: number; name: string }[] = [];
|
|
16
|
+
let p = ctx.app.pages.find((x) => x.page_no === ctx.page.parent_page);
|
|
17
|
+
for (let guard = 0; p && guard < 6; guard++) {
|
|
18
|
+
trail.unshift(p);
|
|
19
|
+
p = ctx.app.pages.find((x) => x.page_no === p!.parent_page);
|
|
20
|
+
}
|
|
21
|
+
if (!trail.length && ctx.page.page_no !== ctx.app.home_page) {
|
|
22
|
+
const home = ctx.app.pages.find((x) => x.page_no === ctx.app.home_page);
|
|
23
|
+
if (home) trail.push(home);
|
|
24
|
+
}
|
|
25
|
+
const links = [];
|
|
26
|
+
for (const t of trail) if (await pageAllowed(ctx, t.page_no)) links.push(html`<li><a href="${ctx.base}/${t.page_no}">${t.name}</a></li>`);
|
|
27
|
+
return links;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether the page shows a breadcrumb region (then its title bar leaves the trail out). */
|
|
31
|
+
export const hasBreadcrumbRegion = (ctx: PageContext) => ctx.page.regions.some((r) => r.type === 'breadcrumb' && ctx.vis?.regions.has(r.id));
|
|
32
|
+
|
|
33
|
+
/** A breadcrumb region: the trail and ({"current": false} leaves it out) the current page. */
|
|
34
|
+
export async function renderBreadcrumbRegion(ctx: PageContext, r: Region): Promise<Raw> {
|
|
35
|
+
const links = await breadcrumbLinks(ctx);
|
|
36
|
+
const title = substitute(ctx.page.title ?? ctx.page.name, ctx, (v) => v);
|
|
37
|
+
const current = r.config?.current === false ? '' : html`<li><span aria-current="page">${title}</span></li>`;
|
|
38
|
+
return html`<nav class="region-crumbs" aria-label="${r.title || ctx.locale.t('region.breadcrumb')}"><ol class="crumbs">${links}${current}</ol></nav>`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Markdown's punctuation, escaped in item values put into a help text (they stay text). */
|
|
42
|
+
const mdEscape = (v: string) => v.replace(/[\\`*_{}[\]()#+\-.!~>|]/g, '\\$&');
|
|
43
|
+
|
|
44
|
+
/** A help text region: the page's help text (Markdown with &ITEM. substitutions), else {"empty": "…"} or "No help…". */
|
|
45
|
+
export async function renderHelpTextRegion(ctx: PageContext, r: Region): Promise<Raw> {
|
|
46
|
+
const help = ctx.page.help?.trim();
|
|
47
|
+
if (!help) return html`<p class="muted help-text-none">${typeof r.config?.empty === 'string' ? r.config.empty : ctx.locale.t('region.no_help')}</p>`;
|
|
48
|
+
const text = substitute(await expandShortcuts(ctx, help, 'text'), ctx, mdEscape);
|
|
49
|
+
return html`<div class="help-text rich-text">${raw(markdownHtml(text))}</div>`;
|
|
50
|
+
}
|
|
@@ -23,7 +23,7 @@ import { bindValues, type PageContext } from './context.ts';
|
|
|
23
23
|
// the placeholder is a link that renders the page with the region (r<id>_load=1).
|
|
24
24
|
|
|
25
25
|
/** Region types that can be cached or loaded lazily (no form state of their own). */
|
|
26
|
-
export const DEFERRABLE = new Set<Region['type']>(['report', 'chart', 'cards', 'dynamic', 'tree', 'template_component']);
|
|
26
|
+
export const DEFERRABLE = new Set<Region['type']>(['report', 'classic_report', 'chart', 'cards', 'dynamic', 'tree', 'template_component']);
|
|
27
27
|
|
|
28
28
|
export interface CacheSetting {
|
|
29
29
|
scope: 'user' | 'session' | 'all';
|
package/src/runtime/regions.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { renderPluginRegion } from './plugins.ts';
|
|
|
37
37
|
import { renderUrlRegion } from './url-region.ts';
|
|
38
38
|
import { renderSearch } from './search.ts';
|
|
39
39
|
import { renderLoginRegion } from './signin.ts';
|
|
40
|
+
import { renderBreadcrumbRegion, renderHelpTextRegion } from './page-regions.ts';
|
|
40
41
|
|
|
41
42
|
// ---------------------------------------------------------------- buttons
|
|
42
43
|
|
|
@@ -335,6 +336,16 @@ async function renderBody(ctx: PageContext, r: Region, hidden: Set<string>): Pro
|
|
|
335
336
|
case 'report':
|
|
336
337
|
body = html`${await renderAiFilter(ctx, r)}${await renderReport(ctx, r, await renderItems(ctx, items, hidden))}`;
|
|
337
338
|
break;
|
|
339
|
+
case 'classic_report':
|
|
340
|
+
// (140) the report renderer without the interactive features (report.ts: classic)
|
|
341
|
+
body = await renderReport(ctx, r, await renderItems(ctx, items, hidden));
|
|
342
|
+
break;
|
|
343
|
+
case 'breadcrumb':
|
|
344
|
+
body = await renderBreadcrumbRegion(ctx, r);
|
|
345
|
+
break;
|
|
346
|
+
case 'help_text':
|
|
347
|
+
body = await renderHelpTextRegion(ctx, r);
|
|
348
|
+
break;
|
|
338
349
|
case 'form':
|
|
339
350
|
body = html`<div class="form-grid">${await renderItems(ctx, items, hidden)}</div>`;
|
|
340
351
|
break;
|
package/src/runtime/render.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { buttonsFor, renderRegion } from './regions.ts';
|
|
|
20
20
|
import { listTree, navbarMarkup, navMarkup } from './lists.ts';
|
|
21
21
|
import { aiInputs, aiOutputs, aiProcessOf } from './ai.ts';
|
|
22
22
|
import { frameOrigins } from './url-region.ts';
|
|
23
|
+
import { breadcrumbLinks, hasBreadcrumbRegion } from './page-regions.ts';
|
|
23
24
|
import { devbarHtml } from './devbar.ts';
|
|
24
25
|
import { feedbackNode } from './feedback.ts';
|
|
25
26
|
import { sessionLimits } from '../session.ts';
|
|
@@ -121,22 +122,6 @@ async function navTree(ctx: PageContext, topNav = false) {
|
|
|
121
122
|
return render(null);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
async function breadcrumb(ctx: PageContext) {
|
|
125
|
-
const trail: { page_no: number; name: string }[] = [];
|
|
126
|
-
let p = ctx.app.pages.find((x) => x.page_no === ctx.page.parent_page);
|
|
127
|
-
for (let guard = 0; p && guard < 6; guard++) {
|
|
128
|
-
trail.unshift(p);
|
|
129
|
-
p = ctx.app.pages.find((x) => x.page_no === p!.parent_page);
|
|
130
|
-
}
|
|
131
|
-
if (!trail.length && ctx.page.page_no !== ctx.app.home_page) {
|
|
132
|
-
const home = ctx.app.pages.find((x) => x.page_no === ctx.app.home_page);
|
|
133
|
-
if (home) trail.push(home);
|
|
134
|
-
}
|
|
135
|
-
const links = [];
|
|
136
|
-
for (const t of trail) if (await pageAllowed(ctx, t.page_no)) links.push(html`<li><a href="${ctx.base}/${t.page_no}">${t.name}</a></li>`);
|
|
137
|
-
return links;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
125
|
// ---------------------------------------------------------------- theme
|
|
141
126
|
|
|
142
127
|
/** Per-app colours (Theme Roller) and the style variant in use. Only checked values reach the CSS (styles.ts). */
|
|
@@ -392,7 +377,8 @@ export async function renderPage(ctx: PageContext) {
|
|
|
392
377
|
// (078) the origins of the page's URL regions (also those a refresh or lazy load shows later)
|
|
393
378
|
for (const o of frameOrigins(ctx.page.regions.filter((r) => ctx.vis!.regions.has(r.id)))) ctx.frameSrc?.add(o);
|
|
394
379
|
const title = substitute(ctx.page.title ?? ctx.page.name, ctx, (v) => v);
|
|
395
|
-
|
|
380
|
+
// (140) a page with a breadcrumb region shows the trail there instead
|
|
381
|
+
const crumbs = ctx.dialog || hasBreadcrumbRegion(ctx) ? [] : await breadcrumbLinks(ctx);
|
|
396
382
|
const das = dynamicActionsJson(ctx);
|
|
397
383
|
|
|
398
384
|
const main = html`
|
package/src/runtime/report.ts
CHANGED
|
@@ -239,7 +239,10 @@ export function selectionOf(page: { items: { name: string }[] }, r: Region) {
|
|
|
239
239
|
|
|
240
240
|
export function reportState(ctx: PageContext, r: Region): ReportState {
|
|
241
241
|
const p = ctx.params;
|
|
242
|
-
|
|
242
|
+
// (140) a classic report: no rows-per-page choice; "pagination": "none" shows its rows on one page
|
|
243
|
+
const classic = r.type === 'classic_report';
|
|
244
|
+
const size = classic && r.config.pagination === 'none' ? Math.min(maxRows(r, 500) ?? 500, 500)
|
|
245
|
+
: (!classic && parseInt(p.get(key(r, 'n')) ?? '', 10)) || Number(r.config.page_size) || componentSetting(ctx.app.component_settings, 'report_rows');
|
|
243
246
|
const st: ReportState = {
|
|
244
247
|
search: (p.get(key(r, 'q')) ?? '').trim(),
|
|
245
248
|
searchCol: (p.get(key(r, 'qc')) ?? '').trim() || null,
|
|
@@ -288,6 +291,13 @@ export function reportState(ctx: PageContext, r: Region): ReportState {
|
|
|
288
291
|
seek: null as Seek | null,
|
|
289
292
|
};
|
|
290
293
|
st.seek = parseSeek(ctx, r, st);
|
|
294
|
+
// (140) a classic report has none of an interactive report's settings: sorting, paging and (when searchable) the search only
|
|
295
|
+
if (classic) {
|
|
296
|
+
Object.assign(st, { searchCol: null, hideCols: [], filters: [], breakCol: null, aggregates: [], highlights: [], computations: [], view: 'report',
|
|
297
|
+
groupBy: { columns: [], functions: [] }, pivot: null, chart: null, area: null, near: null });
|
|
298
|
+
if (r.config.searchable !== true) st.search = '';
|
|
299
|
+
if (r.config.pagination === 'none') st.page = 1;
|
|
300
|
+
}
|
|
291
301
|
return st;
|
|
292
302
|
}
|
|
293
303
|
|
|
@@ -814,8 +824,10 @@ export async function renderReport(ctx: PageContext, r: Region, filterItems: Raw
|
|
|
814
824
|
const t = ctx.locale.t;
|
|
815
825
|
const c = ctx.client!;
|
|
816
826
|
const st = reportState(ctx, r);
|
|
817
|
-
|
|
818
|
-
const
|
|
827
|
+
// (140; APEX: Classic Report) no Actions menu, column menus or saved reports; a search field only with {"searchable": true}
|
|
828
|
+
const classic = r.type === 'classic_report';
|
|
829
|
+
const searchable = classic ? r.config.searchable === true : r.config.searchable !== false;
|
|
830
|
+
const interactive = !classic && r.config.interactive !== false && searchable;
|
|
819
831
|
// (sprint 78; APEX: a faceted search's results have no search bar) one search field: the facets'
|
|
820
832
|
const searchBox = !ctx.page.regions.some((x) => x.type === 'facets' && x.config.search === true && Number(x.config.report) === r.id && ctx.vis?.regions.has(x.id) !== false);
|
|
821
833
|
|
|
@@ -1193,6 +1205,17 @@ export async function renderReport(ctx: PageContext, r: Region, filterItems: Raw
|
|
|
1193
1205
|
? html`<p class="sel-count muted" data-sel-count="${r.id}" aria-live="polite">${selected.size ? t('report.selected_count', { n: selected.size }) : ''}</p>
|
|
1194
1206
|
${elsewhere.map((v) => html`<input type="hidden" name="${selection!.item}" value="${v}" data-sel-other>`)}`
|
|
1195
1207
|
: '';
|
|
1208
|
+
if (classic) {
|
|
1209
|
+
const pager = r.config.pagination === 'none'
|
|
1210
|
+
? (info.more ? html`<p class="region-limit">${t('region.first_rows', { n: st.size })}</p>` : '')
|
|
1211
|
+
: info.more || info.page > 1 || searchable ? pagerNav(ctx, r, info) : '';
|
|
1212
|
+
const shaped = classicTemplate(ctx, r, cols, info.rows, fmtOf, linkIdx >= 0 ? (row) => linkAttrs(ctx, link!.page, rowItems(row)) : null);
|
|
1213
|
+
if (shaped) return html`${toolbar}${shaped}${pager}`;
|
|
1214
|
+
return html`${toolbar}<div class="table-wrap"><table class="report report-classic${r.config.mobile === 'scroll' ? '' : ' report-reflow'}">
|
|
1215
|
+
<thead><tr>${header}</tr></thead>
|
|
1216
|
+
<tbody>${body.length ? body : html`<tr><td colspan="${cols.length + lead || 1}" class="empty">${empty}</td></tr>`}</tbody>
|
|
1217
|
+
</table></div>${pager}`;
|
|
1218
|
+
}
|
|
1196
1219
|
return html`${toolbar}${switcher}${errors}${selectionNote}
|
|
1197
1220
|
<div class="table-wrap"><table class="report${r.config.mobile === 'scroll' ? '' : ' report-reflow'}"${selIdx >= 0 ? raw(` data-sel-region="${r.id}"`) : ''}>
|
|
1198
1221
|
<thead><tr>${header}</tr></thead>
|
|
@@ -1201,3 +1224,32 @@ export async function renderReport(ctx: PageContext, r: Region, filterItems: Raw
|
|
|
1201
1224
|
</table></div>
|
|
1202
1225
|
${info.more || info.page > 1 || searchable ? pagerNav(ctx, r, info) : ''}`;
|
|
1203
1226
|
}
|
|
1227
|
+
|
|
1228
|
+
/** (140) A classic report's templates besides the table ({"report_template": …}). */
|
|
1229
|
+
export const CLASSIC_TEMPLATES = ['standard', 'value_pairs', 'badges', 'media_list'] as const;
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* (140; APEX: the classic report templates Value Attribute Pairs, Badge List and Media List) The rows in another shape
|
|
1233
|
+
* than a table; null for the table ("standard"). value_pairs: each row as headings and values; badges: the first column
|
|
1234
|
+
* as the label and the second as the value; media_list: a title (column title, else the first) and a text (column
|
|
1235
|
+
* description, text or body, else the second), the title a link when the report has one.
|
|
1236
|
+
*/
|
|
1237
|
+
function classicTemplate(ctx: PageContext, r: Region, cols: { f: pg.FieldDef; i: number }[], rows: unknown[][], fmtOf: (name: string) => Formatter,
|
|
1238
|
+
linkOf: ((row: unknown[]) => Raw) | null): Raw | null {
|
|
1239
|
+
const tpl = r.config.report_template;
|
|
1240
|
+
if (!tpl || tpl === 'standard' || !(CLASSIC_TEMPLATES as readonly string[]).includes(tpl)) return null;
|
|
1241
|
+
const text = (row: unknown[], c: { f: pg.FieldDef; i: number } | undefined) => (c ? cell(row[c.i], c.f.dataTypeID, fmtOf(c.f.name)) : '');
|
|
1242
|
+
if (!rows.length) return html`<p class="empty">${r.config.empty ?? ctx.locale.t('report.no_data')}</p>`;
|
|
1243
|
+
if (tpl === 'value_pairs')
|
|
1244
|
+
return html`<div class="cr-pairs">${rows.map((row) => html`<dl>${cols.map((c) => html`<dt>${headingOf(r, c.f.name, ctx.locale.tr)}</dt><dd>${text(row, c)}</dd>`)}</dl>`)}</div>`;
|
|
1245
|
+
if (tpl === 'badges')
|
|
1246
|
+
return html`<ul class="cr-badges">${rows.map((row) => {
|
|
1247
|
+
const inner = html`<span class="cr-badge-value">${text(row, cols[1] ?? cols[0])}</span><span class="cr-badge-label">${cols[1] ? text(row, cols[0]) : ''}</span>`;
|
|
1248
|
+
return html`<li>${linkOf ? html`<a ${linkOf(row)}>${inner}</a>` : inner}</li>`;
|
|
1249
|
+
})}</ul>`;
|
|
1250
|
+
const named = (...names: string[]) => cols.find((c) => names.includes(c.f.name.toLowerCase()));
|
|
1251
|
+
const titleCol = named('title') ?? cols[0];
|
|
1252
|
+
const textCol = named('description', 'text', 'body') ?? cols.find((c) => c !== titleCol);
|
|
1253
|
+
return html`<ul class="cr-media">${rows.map((row) => html`<li><div class="cr-media-title">${linkOf ? html`<a ${linkOf(row)}>${text(row, titleCol)}</a>` : text(row, titleCol)}</div>${
|
|
1254
|
+
textCol ? html`<p class="cr-media-text">${text(row, textCol)}</p>` : ''}</li>`)}</ul>`;
|
|
1255
|
+
}
|