pgkiln 0.55.0 → 0.56.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.
@@ -1,5 +1,6 @@
1
1
  import { REPORT_CHART_KINDS } from './charts.ts';
2
2
  import { ComputeError, computeNameOk, computeSql } from './compute.ts';
3
+ import { facetFilters, facetKeys, facetParamNames, type FacetDef } from './facet-state.ts';
3
4
  import { AGGREGATES, HIGHLIGHT_COLORS, MAX_COMPUTATIONS, OPERATORS } from './report.ts';
4
5
 
5
6
  // (sprint 112) What "Ask in your own words" may do to an interactive report
@@ -15,7 +16,8 @@ import { AGGREGATES, HIGHLIGHT_COLORS, MAX_COMPUTATIONS, OPERATORS } from './rep
15
16
  //
16
17
  // The settings in force are shown as chips (chipsOf): each one is a URL
17
18
  // parameter value of the report, so a chip can be edited or removed
18
- // (editChip, removeChip) and nothing else is stored.
19
+ // (editChip, removeChip) and nothing else is stored. (sprint 122) The page's
20
+ // facets on the report are chips too (Remove only: the facet region edits them).
19
21
 
20
22
  export type Kind = 'number' | 'date' | 'text' | 'boolean';
21
23
 
@@ -243,12 +245,14 @@ export type Chip =
243
245
  | { kind: 'chart'; key: string; type: string; label: string; fn: string; value: string }
244
246
  | { kind: 'hide'; key: string; column: string }
245
247
  | { kind: 'area'; key: string }
246
- | { kind: 'near'; key: string };
248
+ | { kind: 'near'; key: string }
249
+ | { kind: 'facet'; key: string; column: string; value: string; exclude: boolean }
250
+ | { kind: 'facetrange'; key: string; column: string; from: string; to: string; custom: boolean; def: FacetDef };
247
251
 
248
252
  const MAX_CHIPS = 60;
249
253
 
250
254
  /** The report's settings in the URL as chips (what reportState() reads, in the same limits). */
251
- export function chipsOf(params: URLSearchParams, rid: number): Chip[] {
255
+ export function chipsOf(params: URLSearchParams, rid: number, facets?: Map<string, FacetDef>): Chip[] {
252
256
  const k = (x: string) => `r${rid}_${x}`;
253
257
  const out: Chip[] = [];
254
258
  const q = (params.get(k('q')) ?? '').trim();
@@ -287,6 +291,9 @@ export function chipsOf(params: URLSearchParams, rid: number): Chip[] {
287
291
  for (const x of params.getAll(k('x')).filter(Boolean).slice(0, 100)) out.push({ kind: 'hide', key: `x:${x}`, column: x });
288
292
  if (params.get(k('bb'))) out.push({ kind: 'area', key: 'bb' });
289
293
  if (params.get(k('near'))) out.push({ kind: 'near', key: 'near' });
294
+ for (const f of facets ? facetFilters(params, rid, facets) : [])
295
+ if (f.kind === 'values') for (const value of f.values) out.push({ kind: 'facet', key: `fx:${f.column}:${value}`, column: f.column, value, exclude: f.exclude });
296
+ else out.push({ kind: 'facetrange', key: `fr:${f.column}:${f.from}|${f.to}`, column: f.column, from: f.from, to: f.to, custom: f.custom, def: facets!.get(f.column)! });
290
297
  return out.slice(0, MAX_CHIPS);
291
298
  }
292
299
 
@@ -334,6 +341,13 @@ export function removeChip(params: URLSearchParams, rid: number, chip: Chip) {
334
341
  case 'hide': dropValue(params, k('x'), chip.column); break;
335
342
  case 'area': params.delete(k('bb')); break;
336
343
  case 'near': params.delete(k('near')); break;
344
+ case 'facet': {
345
+ const name = facetKeys(rid).values + chip.column;
346
+ dropValue(params, name, chip.value);
347
+ if (!params.getAll(name).length) params.delete(facetKeys(rid).exclude + chip.column);
348
+ break;
349
+ }
350
+ case 'facetrange': for (const n of facetParamNames(rid, chip.column)) params.delete(n); break;
337
351
  }
338
352
  params.delete(k('p'));
339
353
  params.delete(k('k'));
@@ -1,8 +1,13 @@
1
1
  import { appDateReader } from './items.ts';
2
+ import { valueLookup } from './ai-filter.ts';
3
+ import { applyFacetSettings, facetSettings } from './ask-facets.ts';
4
+ import { parseQuestion } from './ir-phrases.ts';
5
+ import { checkPlan, type Column } from './ir-plan.ts';
6
+ import { headingOf, isNumeric } from './report.ts';
2
7
  import { html, type Raw } from '../html.ts';
3
8
  import { icon } from '../icons.ts';
4
9
  import type { Region } from '../metadata.ts';
5
- import { publicError, type PageContext } from './context.ts';
10
+ import { dbg, publicError, type PageContext } from './context.ts';
6
11
  import { regionUrl } from './report.ts';
7
12
  import { describeFacetFilter, facetDefs, facetFilters, facetKeys, facetParamNames, rangeLabel, rangeValue, type FacetDef, type FacetFilter } from './facet-state.ts';
8
13
  import { facetLabel, facetSource, isFacetSource, rangeCounts, valueCounts } from './facets.ts';
@@ -14,6 +19,12 @@ import { facetLabel, facetSource, isFacetSource, rangeCounts, valueCounts } from
14
19
  // works without JavaScript and can be bookmarked.
15
20
  // config: {"report": <report region id>, "suggestions": 3, "placeholder": "Search or filter…",
16
21
  // "facets": [{"column": "job"}, {"column": "sal", "type": "range", "ranges": [...]}]}
22
+ //
23
+ // (sprint 122, WS-5; APEX 26.2) With "ask": true the typed text is also read
24
+ // as a phrase ("clerks in sales with salary between 1000 and 3000", in the
25
+ // application's language: ir-phrases.ts, no AI service): what this region's
26
+ // facets can hold (ask-facets.ts) is offered as the first suggestion, which
27
+ // sets those facets at once. Values are looked up in the report's own rows.
17
28
 
18
29
  const MAX_SUGGESTIONS = 10;
19
30
 
@@ -110,6 +121,28 @@ export async function renderSmartFilters(ctx: PageContext, r: Region): Promise<R
110
121
  }
111
122
  }
112
123
 
124
+ // (sprint 122) the search term read as a phrase: one suggestion that sets the facets it names
125
+ let understood: Raw | '' = '';
126
+ if (r.config.ask === true && fs.search && ctx.client)
127
+ try {
128
+ const columns: Column[] = defs.map((f) => {
129
+ const oid = fs.cols.get(f.column)!;
130
+ return { name: f.column, label: f.label ?? headingOf(report, f.column, ctx.locale.tr), kind: isNumeric(oid) ? 'number' : [1082, 1114, 1184].includes(oid) ? 'date' : oid === 16 ? 'boolean' : 'text', pos: 0 };
131
+ });
132
+ const parsed = await parseQuestion(fs.search, columns, ctx.locale.lang, valueLookup(ctx, columns, fs.src));
133
+ const settings = facetSettings(checkPlan(parsed.plan, columns), new Map(defs.map((f) => [f.column, f])), columns);
134
+ // a single value the matching suggestions already offer needs no second chip
135
+ const single = settings.length === 1 && settings[0].kind === 'values' && settings[0].values.length === 1 && !settings[0].exclude;
136
+ dbg(ctx, 6, 'smart', () => `"${fs.search}" as facets: ${JSON.stringify(settings)}; not understood: ${JSON.stringify(parsed.left)}`);
137
+ if (settings.length && !(single && !parsed.left.length && suggestions.some((x) => x.f.column === settings[0].column)))
138
+ understood = html`<div class="sf-suggestions sf-understood">
139
+ <span class="sf-suggest-title" id="${formId}_u">${t('smart.understood')}</span>
140
+ <ul class="sf-chips" aria-labelledby="${formId}_u"><li><a class="chip chip-suggest" href="${regionUrl(ctx, report, (p) => { applyFacetSettings(p, report.id, settings); p.delete(qName); })}">${settings.map((x, i) => html`${i ? ' · ' : ''}${describeFacetFilter(x, facetLabel(ctx, defs.find((d) => d.column === x.column)!), t, defs.find((d) => d.column === x.column))}`)}</a></li></ul>
141
+ </div>`;
142
+ } catch (e) {
143
+ dbg(ctx, 4, 'smart', () => `"${fs.search}" not read as a phrase: ${(e as Error).message}`);
144
+ }
145
+
113
146
  // GET form: the search term replaces the old one; every other parameter stays
114
147
  const keep = [...ctx.params.entries()].filter(([name]) => ![qName, page, 'clear', 'cs'].includes(name));
115
148
  ctx.detached.push(
@@ -128,12 +161,13 @@ export async function renderSmartFilters(ctx: PageContext, r: Region): Promise<R
128
161
  ? html`<a class="sf-clear" href="${regionUrl(ctx, report, (p) => { p.delete(qName); p.delete(page); for (const f of defs) for (const n of facetParamNames(report.id, f.column)) p.delete(n); })}">${t('facets.clear')}</a>`
129
162
  : ''}
130
163
  </div>
164
+ ${understood}
131
165
  ${suggestions.length
132
166
  ? html`<div class="sf-suggestions">
133
167
  <span class="sf-suggest-title" id="${formId}_s">${fs.search ? t('smart.matching') : t('smart.suggested')}</span>
134
168
  <ul class="sf-chips" aria-labelledby="${formId}_s">${suggestions.map((s) => html`<li><a class="chip chip-suggest" href="${s.href}">${s.label} <span class="facet-count">${s.n}</span></a></li>`)}</ul>
135
169
  </div>`
136
- : fs.search && defs.some((f) => f.type === 'checkbox')
170
+ : fs.search && !understood && defs.some((f) => f.type === 'checkbox')
137
171
  ? html`<p class="muted sf-none">${t('smart.no_match')}</p>`
138
172
  : ''}
139
173
  ${errors}