@cube-nl/dmp-sdk 1.0.0-rc.4 → 1.1.0-rc.2

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 (96) hide show
  1. package/README.md +372 -9
  2. package/dist/app.d.ts +41 -4
  3. package/dist/app.js +252 -14
  4. package/dist/browser.js +14 -1
  5. package/dist/client.d.ts +2 -0
  6. package/dist/client.js +5 -1
  7. package/dist/cube-sdk.js +186 -2
  8. package/dist/cube-ui.manifest.json +971 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.js +4 -0
  11. package/dist/preview.d.ts +11 -0
  12. package/dist/preview.js +60 -7
  13. package/dist/size.json +6 -0
  14. package/dist/trial.d.ts +32 -0
  15. package/dist/trial.js +55 -0
  16. package/dist/ui/aggregate-block.d.ts +73 -0
  17. package/dist/ui/aggregate-block.js +139 -0
  18. package/dist/ui/attributes.d.ts +5 -0
  19. package/dist/ui/attributes.js +72 -0
  20. package/dist/ui/author-error.d.ts +3 -0
  21. package/dist/ui/author-error.js +3 -0
  22. package/dist/ui/block.d.ts +69 -0
  23. package/dist/ui/block.js +238 -0
  24. package/dist/ui/blocks/app-shell.d.ts +4 -0
  25. package/dist/ui/blocks/app-shell.js +167 -0
  26. package/dist/ui/blocks/board.d.ts +8 -0
  27. package/dist/ui/blocks/board.js +333 -0
  28. package/dist/ui/blocks/calc.d.ts +9 -0
  29. package/dist/ui/blocks/calc.js +263 -0
  30. package/dist/ui/blocks/chart.d.ts +7 -0
  31. package/dist/ui/blocks/chart.js +166 -0
  32. package/dist/ui/blocks/detail.d.ts +8 -0
  33. package/dist/ui/blocks/detail.js +270 -0
  34. package/dist/ui/blocks/field.d.ts +8 -0
  35. package/dist/ui/blocks/field.js +16 -0
  36. package/dist/ui/blocks/form.d.ts +8 -0
  37. package/dist/ui/blocks/form.js +153 -0
  38. package/dist/ui/blocks/gap-list.d.ts +4 -0
  39. package/dist/ui/blocks/gap-list.js +36 -0
  40. package/dist/ui/blocks/kpi.d.ts +4 -0
  41. package/dist/ui/blocks/kpi.js +75 -0
  42. package/dist/ui/blocks/period-picker.d.ts +4 -0
  43. package/dist/ui/blocks/period-picker.js +155 -0
  44. package/dist/ui/blocks/state.d.ts +4 -0
  45. package/dist/ui/blocks/state.js +135 -0
  46. package/dist/ui/blocks/table.d.ts +8 -0
  47. package/dist/ui/blocks/table.js +666 -0
  48. package/dist/ui/blocks/top.d.ts +4 -0
  49. package/dist/ui/blocks/top.js +67 -0
  50. package/dist/ui/chart-svg.d.ts +28 -0
  51. package/dist/ui/chart-svg.js +111 -0
  52. package/dist/ui/csv.d.ts +9 -0
  53. package/dist/ui/csv.js +55 -0
  54. package/dist/ui/define.d.ts +24 -0
  55. package/dist/ui/define.js +107 -0
  56. package/dist/ui/dom.d.ts +12 -0
  57. package/dist/ui/dom.js +58 -0
  58. package/dist/ui/fields.d.ts +39 -0
  59. package/dist/ui/fields.js +73 -0
  60. package/dist/ui/format.d.ts +44 -0
  61. package/dist/ui/format.js +215 -0
  62. package/dist/ui/inputs.d.ts +35 -0
  63. package/dist/ui/inputs.js +161 -0
  64. package/dist/ui/manifest.d.ts +40 -0
  65. package/dist/ui/manifest.js +880 -0
  66. package/dist/ui/period.d.ts +18 -0
  67. package/dist/ui/period.js +52 -0
  68. package/dist/ui/query.d.ts +26 -0
  69. package/dist/ui/query.js +99 -0
  70. package/dist/ui/schema.d.ts +23 -0
  71. package/dist/ui/schema.js +21 -0
  72. package/dist/ui/styles.d.ts +8 -0
  73. package/dist/ui/styles.js +51 -0
  74. package/dist/ui/texts.d.ts +146 -0
  75. package/dist/ui/texts.js +138 -0
  76. package/dist/ui/theme.d.ts +58 -0
  77. package/dist/ui/theme.js +311 -0
  78. package/dist/ui/trial-banner.d.ts +25 -0
  79. package/dist/ui/trial-banner.js +180 -0
  80. package/dist/ui/write.d.ts +20 -0
  81. package/dist/ui/write.js +54 -0
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/dist/wireframe.d.ts +37 -0
  85. package/dist/wireframe.js +352 -0
  86. package/fonts/OFL-albert-sans.txt +93 -0
  87. package/fonts/OFL-ibm-plex-sans.txt +93 -0
  88. package/fonts/OFL-inter.txt +93 -0
  89. package/fonts/OFL-nunito-sans.txt +93 -0
  90. package/fonts/OFL-source-sans-3.txt +93 -0
  91. package/fonts/albert-sans.woff2 +0 -0
  92. package/fonts/ibm-plex-sans.woff2 +0 -0
  93. package/fonts/inter.woff2 +0 -0
  94. package/fonts/nunito-sans.woff2 +0 -0
  95. package/fonts/source-sans-3.woff2 +0 -0
  96. package/package.json +12 -4
package/dist/index.d.ts CHANGED
@@ -5,4 +5,10 @@ export { buildQuery } from './query.js';
5
5
  export type * from './types.js';
6
6
  export { CubeApp, type ConnectOptions, type CubeAppConfig, type CubeAppSession, type CubeAppStatic, type CubeAppUser, } from './app.js';
7
7
  export type { AppManifest, MockData, MockField, MockResource, MockSchema } from './preview.js';
8
+ export { defineBlocks, type BlockScope, type DefineOptions } from './ui/define.js';
9
+ export { contrastRatio, deriveTheme, FONTS, TOKEN_NAMES } from './ui/theme.js';
10
+ export type { Corners, FontFile, FontId, ResolvedTheme, ThemeFile, ThemeSource, ThemeTokens, ThemeWarning, TokenName, } from './ui/theme.js';
11
+ export { formatValue, type Format } from './ui/format.js';
12
+ export type { FieldInfo, ResourceSchema } from './ui/schema.js';
13
+ export { blockManifest, type AttributeSpec, type BlockManifest, type BlockSpec } from './ui/manifest.js';
8
14
  export { VERSION } from './version.js';
package/dist/index.js CHANGED
@@ -3,4 +3,8 @@ export { bearer, createOAuth, discoverMetadata, memoryKeyValueStore, memoryToken
3
3
  export { DmpError, isDmpError } from './errors.js';
4
4
  export { buildQuery } from './query.js';
5
5
  export { CubeApp, } from './app.js';
6
+ export { defineBlocks } from './ui/define.js';
7
+ export { contrastRatio, deriveTheme, FONTS, TOKEN_NAMES } from './ui/theme.js';
8
+ export { formatValue } from './ui/format.js';
9
+ export { blockManifest } from './ui/manifest.js';
6
10
  export { VERSION } from './version.js';
package/dist/preview.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ResourceClient } from './client.js';
2
2
  import type { DmpRecord } from './types.js';
3
+ import type { ResourceSchema } from './ui/schema.js';
3
4
  /**
4
5
  * One field as `dmp.describe` (and so `cube.mock_data`) writes it: a JSON Schema. `type` is a
5
6
  * name, or a list of names where `'null'` makes it nullable. `_capabilities`' own field shape
@@ -53,6 +54,10 @@ export interface AppManifest {
53
54
  description?: string;
54
55
  uses?: Record<string, readonly string[]>;
55
56
  sdk?: string;
57
+ /** `'wireframe'`: a proposal whose data partly does not exist yet; see `ontbreekt`. */
58
+ soort?: string;
59
+ /** What a wireframe needs that the DMP does not have yet: the gap entries (`readGaps`). */
60
+ ontbreekt?: unknown[];
56
61
  [key: string]: unknown;
57
62
  }
58
63
  /**
@@ -68,7 +73,11 @@ export declare class PreviewStore {
68
73
  constructor(data: MockData, manifest?: AppManifest | null,
69
74
  /** Runs before every call; throw to refuse it (the preview's signed-out state). */
70
75
  guard?: () => void);
76
+ /** The rows a resource holds now; empty for an unknown resource. */
77
+ rows(name: string): readonly DmpRecord[];
71
78
  resourceNames(): string[];
79
+ /** The resource as a block sees it, built from the parsed schema; null for an unknown resource. */
80
+ describe(name: string): ResourceSchema | null;
72
81
  resource<T extends DmpRecord = DmpRecord>(name: string): ResourceClient<T>;
73
82
  /**
74
83
  * Unknown resource first (404, as the REST face answers it), then the manifest: live, the app's
@@ -76,3 +85,5 @@ export declare class PreviewStore {
76
85
  */
77
86
  private authorize;
78
87
  }
88
+ /** Nulls first, numbers as numbers, everything else as text (ISO dates sort correctly as text). */
89
+ export declare function compare(a: unknown, b: unknown): number;
package/dist/preview.js CHANGED
@@ -2,7 +2,14 @@ import { DmpError } from './errors.js';
2
2
  import { buildQuery } from './query.js';
3
3
  const RESOURCE_NAME = /^[a-z][a-z0-9_]*$/;
4
4
  const OPERATORS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'like', 'ilike', 'is', 'between'];
5
- const SYMBOLS = [['>=', 'gte'], ['<=', 'lte'], ['<>', 'neq'], ['>', 'gt'], ['<', 'lt'], ['=', 'eq']];
5
+ const SYMBOLS = [
6
+ ['>=', 'gte'],
7
+ ['<=', 'lte'],
8
+ ['<>', 'neq'],
9
+ ['>', 'gt'],
10
+ ['<', 'lt'],
11
+ ['=', 'eq'],
12
+ ];
6
13
  const FUNCTIONS = ['count', 'sum', 'avg', 'min', 'max'];
7
14
  const UNITS = ['day', 'week', 'month', 'quarter', 'year'];
8
15
  const MAX_FILTERS = 20;
@@ -37,9 +44,42 @@ export class PreviewStore {
37
44
  }
38
45
  }
39
46
  }
47
+ /** The rows a resource holds now; empty for an unknown resource. */
48
+ rows(name) {
49
+ return this.tables.get(name)?.rows ?? [];
50
+ }
40
51
  resourceNames() {
41
52
  return [...this.tables.keys()];
42
53
  }
54
+ /** The resource as a block sees it, built from the parsed schema; null for an unknown resource. */
55
+ describe(name) {
56
+ const table = this.tables.get(name);
57
+ if (table === undefined) {
58
+ return null;
59
+ }
60
+ return {
61
+ name,
62
+ actions: [...table.actions],
63
+ fields: [...table.fields.values()].map((field) => {
64
+ const choices = table.choices.get(field.name);
65
+ return {
66
+ name: field.name,
67
+ type: field.types.join('|'),
68
+ format: field.format,
69
+ nullable: field.nullable,
70
+ readonly: !table.writable.has(field.name),
71
+ enum: choices === undefined ? null : choices.map(String),
72
+ description: field.description,
73
+ };
74
+ }),
75
+ summary: [...table.summary],
76
+ filterable: [...table.filterable],
77
+ sortable: [...table.fields.keys()],
78
+ groupable: [...table.groupable],
79
+ // Live lists the field functions only: a bare count needs no field, so `count` is never offered per field.
80
+ aggregatable: Object.fromEntries([...table.aggregatable].map(([key, fns]) => [key, fns.filter((fn) => fn !== 'count')])),
81
+ };
82
+ }
43
83
  resource(name) {
44
84
  if (!RESOURCE_NAME.test(name)) {
45
85
  throw new TypeError(`'${name}' is not a resource name: cube/api names match [a-z][a-z0-9_]*.`);
@@ -119,7 +159,7 @@ function table(name, resource) {
119
159
  for (const row of fields.size === 0 ? rows : []) {
120
160
  for (const key of Object.keys(row)) {
121
161
  if (!fields.has(key)) {
122
- fields.set(key, { name: key, types: [], nullable: true });
162
+ fields.set(key, { name: key, types: [], nullable: true, format: null, description: null });
123
163
  }
124
164
  }
125
165
  }
@@ -143,9 +183,14 @@ function table(name, resource) {
143
183
  for (const [fieldName, choice] of choiceEntries) {
144
184
  choices.set(fieldName, choice.values);
145
185
  }
146
- const known = (list) => Array.isArray(list) ? list.filter((item) => typeof item === 'string' && fields.has(item)) : null;
186
+ const known = (list) => Array.isArray(list)
187
+ ? list.filter((item) => typeof item === 'string' && fields.has(item))
188
+ : null;
147
189
  return {
148
190
  name,
191
+ actions: Array.isArray(schema.actions)
192
+ ? schema.actions.filter((action) => typeof action === 'string')
193
+ : ['list', 'get', 'create', 'update', 'delete'],
149
194
  fields,
150
195
  filterable: known(schema.filters) ?? names,
151
196
  groupable: known(schema.groupable) ?? names,
@@ -169,6 +214,8 @@ function field(name, definition) {
169
214
  name,
170
215
  types: list.filter((type) => type !== 'null' && type !== ''),
171
216
  nullable: definition.nullable === true || list.includes('null'),
217
+ format: typeof definition.format === 'string' ? definition.format : null,
218
+ description: typeof definition.description === 'string' ? definition.description : null,
172
219
  };
173
220
  }
174
221
  /**
@@ -464,7 +511,9 @@ function aggregator(table, fn) {
464
511
  }
465
512
  const allowed = table.aggregatable.get(target);
466
513
  if (allowed === undefined) {
467
- throw problem(422, 'query_invalid', `Unknown aggregatable field: ${target}.`, { fn: [aggregatableHint(table)] });
514
+ throw problem(422, 'query_invalid', `Unknown aggregatable field: ${target}.`, {
515
+ fn: [aggregatableHint(table)],
516
+ });
468
517
  }
469
518
  if (!allowed.includes(name)) {
470
519
  throw problem(422, 'query_invalid', `${name} is not allowed on ${target}.`, {
@@ -483,9 +532,13 @@ function aggregator(table, fn) {
483
532
  case 'avg':
484
533
  return numbers.length === 0 ? null : numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
485
534
  case 'min':
486
- return values.length === 0 ? null : values.reduce((low, value) => (compare(value, low) < 0 ? value : low));
535
+ return values.length === 0
536
+ ? null
537
+ : values.reduce((low, value) => (compare(value, low) < 0 ? value : low));
487
538
  default:
488
- return values.length === 0 ? null : values.reduce((high, value) => (compare(value, high) > 0 ? value : high));
539
+ return values.length === 0
540
+ ? null
541
+ : values.reduce((high, value) => (compare(value, high) > 0 ? value : high));
489
542
  }
490
543
  };
491
544
  }
@@ -778,7 +831,7 @@ function same(actual, expected) {
778
831
  return actual === expected || (typeof actual !== 'object' && String(actual) === String(expected));
779
832
  }
780
833
  /** Nulls first, numbers as numbers, everything else as text (ISO dates sort correctly as text). */
781
- function compare(a, b) {
834
+ export function compare(a, b) {
782
835
  if (a == null || b == null) {
783
836
  return a == null ? (b == null ? 0 : -1) : 1;
784
837
  }
package/dist/size.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "core_gzip": 18169,
3
+ "total_gzip": 46472,
4
+ "ui_gzip": 28303,
5
+ "budget_gzip": 40960
6
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * What the DMP writes into a trial page (`<script type="application/json" id="cube-trial-config">`): the maker's
3
+ * app on the maker's own data, read-only, with a token that lives only in this page.
4
+ */
5
+ export interface TrialConfig {
6
+ mode: 'trial';
7
+ /** The DMP's origin. */
8
+ dmpUrl: string;
9
+ /** The REST face's base URL. */
10
+ apiUrl: string;
11
+ /** The read-only trial token, `cubeproef_…`. Never stored, never logged. */
12
+ token: string;
13
+ /** ISO 8601. */
14
+ expiresAt: string;
15
+ /** `GET` answers whether the trial is still open (the auto-refresh polls it). */
16
+ statusUrl: string;
17
+ version: string | null;
18
+ soort: string | null;
19
+ /** The DMP's user id of the maker. */
20
+ userId: string | null;
21
+ /** The resources the trial reads, for the banner. */
22
+ leest: string[];
23
+ }
24
+ /**
25
+ * Reads the injected config and removes its element, so the token is out of the DOM. The SDK calls it once when the
26
+ * script evaluates; the result is what `trialConfig()` answers from then on (a later call that finds an element
27
+ * replaces it, one that finds none changes nothing and answers null). Anything but a complete `mode: 'trial'` config
28
+ * is warned about and means no trial: the app starts as it would without one.
29
+ */
30
+ export declare function readTrialConfig(doc?: Document): TrialConfig | null;
31
+ /** The trial config this page was served with, or null outside a trial. */
32
+ export declare function trialConfig(): TrialConfig | null;
package/dist/trial.js ADDED
@@ -0,0 +1,55 @@
1
+ const ELEMENT_ID = 'cube-trial-config';
2
+ const REQUIRED = ['dmpUrl', 'apiUrl', 'token', 'expiresAt', 'statusUrl'];
3
+ let current = null;
4
+ /**
5
+ * Reads the injected config and removes its element, so the token is out of the DOM. The SDK calls it once when the
6
+ * script evaluates; the result is what `trialConfig()` answers from then on (a later call that finds an element
7
+ * replaces it, one that finds none changes nothing and answers null). Anything but a complete `mode: 'trial'` config
8
+ * is warned about and means no trial: the app starts as it would without one.
9
+ */
10
+ export function readTrialConfig(doc) {
11
+ const element = doc?.getElementById(ELEMENT_ID) ?? null;
12
+ // Without the element there is nothing new to read: the config read before stays (the element is gone by now).
13
+ if (element === null) {
14
+ return null;
15
+ }
16
+ current = null;
17
+ element.remove();
18
+ let value = null;
19
+ try {
20
+ const parsed = JSON.parse(element.textContent ?? '');
21
+ value = parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
22
+ ? parsed
23
+ : null;
24
+ }
25
+ catch {
26
+ // Reported below, without the content: it holds the token.
27
+ }
28
+ if (value?.mode !== 'trial' || REQUIRED.some((key) => typeof value[key] !== 'string' || value[key] === '')) {
29
+ console.warn(`#${ELEMENT_ID} is not a complete trial config; the app starts without it.`);
30
+ return null;
31
+ }
32
+ const text = (key) => (typeof value[key] === 'string' ? value[key] : null);
33
+ const leest = value.leest;
34
+ current = {
35
+ mode: 'trial',
36
+ dmpUrl: value.dmpUrl,
37
+ apiUrl: value.apiUrl,
38
+ token: value.token,
39
+ expiresAt: value.expiresAt,
40
+ statusUrl: value.statusUrl,
41
+ version: text('version'),
42
+ soort: text('soort'),
43
+ userId: text('userId'),
44
+ leest: Array.isArray(leest) ? leest.filter((name) => typeof name === 'string') : [],
45
+ };
46
+ return current;
47
+ }
48
+ /** The trial config this page was served with, or null outside a trial. */
49
+ export function trialConfig() {
50
+ return current;
51
+ }
52
+ // At evaluation, before any app code runs: the element must be gone before the app could read or dump the DOM.
53
+ if (typeof document !== 'undefined') {
54
+ readTrialConfig(document);
55
+ }
@@ -0,0 +1,73 @@
1
+ import type { CubeAppSession } from '../app.js';
2
+ import type { DmpRecord, DmpResponse, FilterValue } from '../types.js';
3
+ import type { Period } from './period.js';
4
+ import { type PeriodSourceLike } from './query.js';
5
+ /** The frame every aggregate block draws its content in, so a dashboard reads as one set of cards. */
6
+ export declare const CARD_CSS = "\n.card{padding:1rem 1.25rem;border:1px solid var(--cube-color-border);border-radius:var(--cube-radius);\nbackground:var(--cube-color-surface)}\n";
7
+ /** The visible title above a chart or a ranking. */
8
+ export declare const LABEL_CSS = "\n.label{margin:0 0 .5rem;font-weight:600}\n";
9
+ export interface AggregateQueryParts {
10
+ /** The `fn` the API is asked for, and the key of the value in each row: `count`, `sum(total)`. */
11
+ fn: string;
12
+ /** The measured field; null for a row count. */
13
+ field: string | null;
14
+ count: boolean;
15
+ /** The period of the followed picker, or null without one. */
16
+ period: Period | null;
17
+ /** The block's `filter`, narrowed to `range` on the date field when both are there. */
18
+ filterFor(range: {
19
+ from: string;
20
+ to: string;
21
+ } | null): Record<string, FilterValue>;
22
+ }
23
+ /** An attribute the block cannot do without; its absence is explained in the block. */
24
+ export declare function requiredAttr(el: Element, name: string): string;
25
+ /** The fields an aggregate block reads: the measured one, the `by` field (without its truncation), the date field. */
26
+ export declare function aggregateFields(el: Element): string[];
27
+ /** A number from an aggregate row (the API may send decimals as strings); null for nothing or not a number. */
28
+ export declare function toNumber(value: unknown): number | null;
29
+ /**
30
+ * Listens to one period picker at a time and reloads its block on a change. A block that was removed lets go at
31
+ * the next change instead of reloading, so the blocks need no disconnect step of their own for it.
32
+ */
33
+ export declare class PeriodFollower {
34
+ private readonly block;
35
+ private source;
36
+ private readonly onChange;
37
+ constructor(block: {
38
+ readonly isConnected: boolean;
39
+ refresh(): Promise<void>;
40
+ });
41
+ follow(source: PeriodSourceLike | null): void;
42
+ }
43
+ /**
44
+ * Reads `measure`, `filter`, `period` and `date-field` (falling back to the field of a truncated `by`), waits for
45
+ * the picker and follows it. Null when the load was aborted meanwhile: a newer load (or none) owns the block.
46
+ */
47
+ export declare function aggregateQuery(el: HTMLElement, follower: PeriodFollower, signal: AbortSignal, by?: string | null): Promise<AggregateQueryParts | null>;
48
+ /**
49
+ * The aggregate of a grouped block (chart, top) over its period. More groups than the DMP returns in one answer
50
+ * (1000) is refused with a 422; trying again gives the same answer, so the error says what to change instead.
51
+ */
52
+ export declare function groupedAggregate(session: CubeAppSession, resource: string, by: string, query: AggregateQueryParts, signal: AbortSignal): Promise<DmpResponse<DmpRecord[]>>;
53
+ /** A whole number attribute of at least `min`; absent, not a number or too small gives `fallback`. */
54
+ export declare function wholeAttr(el: Element, name: string, min: number, fallback: number): number;
55
+ export interface RankedGroup {
56
+ key: string;
57
+ value: number;
58
+ /** The lookup's label, `#<id>` for an id it has no row for, or the key itself without a lookup. */
59
+ label: string;
60
+ }
61
+ /**
62
+ * The groups of an aggregate ranked by value (`descending` highest first) and cut at `limit`, labelled through the
63
+ * block's `lookup`. A group without a key or without a value is left out. Null when the load was aborted meanwhile.
64
+ */
65
+ export declare function rankGroups(el: Element, session: CubeAppSession, rows: DmpRecord[], keys: {
66
+ by: string;
67
+ fn: string;
68
+ }, order: {
69
+ descending: boolean;
70
+ limit: number;
71
+ }, signal: AbortSignal): Promise<RankedGroup[] | null>;
72
+ /** How the block writes a value: its `format` (default integer for count, else number) and `currency`. */
73
+ export declare function valueFormatter(el: Element, count: boolean): (n: number, compact?: boolean) => string;
@@ -0,0 +1,139 @@
1
+ // What the aggregate blocks (cube-kpi, cube-chart, cube-top) share: their required attributes, the measure and
2
+ // filter they ask with, the period picker they follow and how they write a value.
3
+ import { isDmpError } from '../errors.js';
4
+ import { AuthorError } from './author-error.js';
5
+ import { BlockStateError } from './block.js';
6
+ import { formatValue, parseMeasure } from './format.js';
7
+ import { blockFilter, dateFieldFor, lookupLabels, periodFilter, resolvePeriodSource, } from './query.js';
8
+ import { NL } from './texts.js';
9
+ const FORMATS = ['number', 'integer', 'currency', 'percent'];
10
+ /** The frame every aggregate block draws its content in, so a dashboard reads as one set of cards. */
11
+ export const CARD_CSS = `
12
+ .card{padding:1rem 1.25rem;border:1px solid var(--cube-color-border);border-radius:var(--cube-radius);
13
+ background:var(--cube-color-surface)}
14
+ `;
15
+ /** The visible title above a chart or a ranking. */
16
+ export const LABEL_CSS = `
17
+ .label{margin:0 0 .5rem;font-weight:600}
18
+ `;
19
+ /** An attribute the block cannot do without; its absence is explained in the block. */
20
+ export function requiredAttr(el, name) {
21
+ const value = el.getAttribute(name)?.trim() ?? '';
22
+ if (value === '')
23
+ throw new AuthorError(NL.invalid.missing(el.localName, name));
24
+ return value;
25
+ }
26
+ /** The fields an aggregate block reads: the measured one, the `by` field (without its truncation), the date field. */
27
+ export function aggregateFields(el) {
28
+ const measured = /\(([^)]+)\)/.exec(el.getAttribute('measure') ?? '')?.[1]?.trim();
29
+ const by = el.getAttribute('by')?.split('.')[0];
30
+ return [measured, by, el.getAttribute('date-field')].filter((field) => !!field);
31
+ }
32
+ /** A number from an aggregate row (the API may send decimals as strings); null for nothing or not a number. */
33
+ export function toNumber(value) {
34
+ if (typeof value === 'string' && value.trim() === '')
35
+ return null;
36
+ const n = typeof value === 'number' || typeof value === 'string' ? Number(value) : NaN;
37
+ return Number.isFinite(n) ? n : null;
38
+ }
39
+ /**
40
+ * Listens to one period picker at a time and reloads its block on a change. A block that was removed lets go at
41
+ * the next change instead of reloading, so the blocks need no disconnect step of their own for it.
42
+ */
43
+ export class PeriodFollower {
44
+ block;
45
+ source = null;
46
+ onChange = () => {
47
+ if (this.block.isConnected)
48
+ void this.block.refresh();
49
+ else
50
+ this.follow(null);
51
+ };
52
+ constructor(block) {
53
+ this.block = block;
54
+ }
55
+ follow(source) {
56
+ if (source === this.source)
57
+ return;
58
+ this.source?.removeEventListener('cube-change', this.onChange);
59
+ this.source = source;
60
+ source?.addEventListener('cube-change', this.onChange);
61
+ }
62
+ }
63
+ /**
64
+ * Reads `measure`, `filter`, `period` and `date-field` (falling back to the field of a truncated `by`), waits for
65
+ * the picker and follows it. Null when the load was aborted meanwhile: a newer load (or none) owns the block.
66
+ */
67
+ export async function aggregateQuery(el, follower, signal, by = null) {
68
+ const measure = parseMeasure(requiredAttr(el, 'measure'));
69
+ const filter = blockFilter(el);
70
+ const selector = el.getAttribute('period');
71
+ const dateField = selector === null || selector === ''
72
+ ? null
73
+ : dateFieldFor(el.getAttribute('date-field'), by, el.localName);
74
+ const source = await resolvePeriodSource(el, selector);
75
+ if (signal.aborted)
76
+ return null;
77
+ follower.follow(source);
78
+ return {
79
+ fn: measure.field === null ? measure.fn : `${measure.fn}(${measure.field})`,
80
+ field: measure.field,
81
+ count: measure.fn === 'count',
82
+ period: source?.value ?? null,
83
+ filterFor: (range) => range === null || dateField === null ? filter : { ...filter, ...periodFilter(dateField, range) },
84
+ };
85
+ }
86
+ /**
87
+ * The aggregate of a grouped block (chart, top) over its period. More groups than the DMP returns in one answer
88
+ * (1000) is refused with a 422; trying again gives the same answer, so the error says what to change instead.
89
+ */
90
+ export async function groupedAggregate(session, resource, by, query, signal) {
91
+ try {
92
+ return await session
93
+ .resource(resource)
94
+ .aggregate({ fn: query.fn, group: by, filter: query.filterFor(query.period) }, { signal });
95
+ }
96
+ catch (error) {
97
+ if (isDmpError(error) && error.status === 422
98
+ && (error.reason === 'too_many_groups' || error.code === 'cardinality_exceeded')) {
99
+ throw new BlockStateError({ kind: 'error', detail: NL.tooManyGroups, retry: false });
100
+ }
101
+ throw error;
102
+ }
103
+ }
104
+ /** A whole number attribute of at least `min`; absent, not a number or too small gives `fallback`. */
105
+ export function wholeAttr(el, name, min, fallback) {
106
+ const raw = el.getAttribute(name);
107
+ const n = raw === null || raw.trim() === '' ? NaN : Number(raw);
108
+ return Number.isFinite(n) && n >= min ? Math.floor(n) : fallback;
109
+ }
110
+ /**
111
+ * The groups of an aggregate ranked by value (`descending` highest first) and cut at `limit`, labelled through the
112
+ * block's `lookup`. A group without a key or without a value is left out. Null when the load was aborted meanwhile.
113
+ */
114
+ export async function rankGroups(el, session, rows, keys, order, signal) {
115
+ const direction = order.descending ? -1 : 1;
116
+ const ranked = rows
117
+ .filter((row) => row[keys.by] !== null && row[keys.by] !== undefined)
118
+ .map((row) => ({ key: String(row[keys.by]), value: toNumber(row[keys.fn]) }))
119
+ .filter((group) => group.value !== null)
120
+ .sort((a, b) => direction * (a.value - b.value))
121
+ .slice(0, order.limit);
122
+ const lookup = el.getAttribute('lookup');
123
+ const labels = lookup === null || lookup === '' || ranked.length === 0
124
+ ? null
125
+ : await lookupLabels(session, lookup, ranked.map((group) => group.key), signal);
126
+ if (signal.aborted)
127
+ return null;
128
+ return ranked.map((group) => ({
129
+ ...group,
130
+ label: labels === null ? group.key : (labels.get(group.key) ?? `#${group.key}`),
131
+ }));
132
+ }
133
+ /** How the block writes a value: its `format` (default integer for count, else number) and `currency`. */
134
+ export function valueFormatter(el, count) {
135
+ const wanted = el.getAttribute('format');
136
+ const format = (wanted !== null && FORMATS.includes(wanted) ? wanted : count ? 'integer' : 'number');
137
+ const currency = el.getAttribute('currency') || 'EUR';
138
+ return (n, compact) => formatValue(n, format, { currency, compact: compact === true });
139
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The attribute names each block observes, apart from manifest.ts so the browser bundle does not carry the
3
+ * manifest's documentation texts. test/ui-manifest.test.js keeps the two in step.
4
+ */
5
+ export declare const BLOCK_ATTRIBUTES: Readonly<Record<string, readonly string[]>>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The attribute names each block observes, apart from manifest.ts so the browser bundle does not carry the
3
+ * manifest's documentation texts. test/ui-manifest.test.js keeps the two in step.
4
+ */
5
+ export const BLOCK_ATTRIBUTES = {
6
+ 'cube-state': ['kind', 'message', 'retry'],
7
+ 'cube-app-shell': ['name', 'logo', 'logo-alt'],
8
+ 'cube-page': ['name', 'label'],
9
+ 'cube-field': ['name', 'label', 'format', 'multiline', 'options', 'hidden'],
10
+ 'cube-period-picker': ['default', 'label', 'today'],
11
+ 'cube-kpi': [
12
+ 'resource',
13
+ 'measure',
14
+ 'label',
15
+ 'format',
16
+ 'currency',
17
+ 'filter',
18
+ 'period',
19
+ 'date-field',
20
+ 'compare',
21
+ ],
22
+ 'cube-chart': [
23
+ 'resource',
24
+ 'measure',
25
+ 'by',
26
+ 'kind',
27
+ 'label',
28
+ 'format',
29
+ 'currency',
30
+ 'filter',
31
+ 'period',
32
+ 'date-field',
33
+ 'lookup',
34
+ 'height',
35
+ 'limit',
36
+ ],
37
+ 'cube-top': [
38
+ 'resource',
39
+ 'measure',
40
+ 'by',
41
+ 'limit',
42
+ 'order',
43
+ 'label',
44
+ 'format',
45
+ 'currency',
46
+ 'filter',
47
+ 'period',
48
+ 'date-field',
49
+ 'lookup',
50
+ ],
51
+ 'cube-table': [
52
+ 'resource',
53
+ 'fields',
54
+ 'search',
55
+ 'filters',
56
+ 'sort',
57
+ 'page-size',
58
+ 'export',
59
+ 'detail',
60
+ 'filter',
61
+ 'period',
62
+ 'date-field',
63
+ ],
64
+ 'cube-detail': ['resource', 'record-id', 'fields', 'editable'],
65
+ 'cube-form': ['resource', 'fields', 'submit-label', 'success-text'],
66
+ 'cube-calc': ['label'],
67
+ 'cube-input': ['name', 'label', 'type', 'value', 'min', 'max', 'step', 'options'],
68
+ 'cube-result': ['name', 'label', 'format', 'currency', 'emphasis'],
69
+ 'cube-board': ['resource', 'status-field', 'title-field', 'fields', 'sort', 'filter', 'page-size'],
70
+ 'cube-column': ['value', 'label'],
71
+ 'cube-gap-list': ['scherm'],
72
+ };
@@ -0,0 +1,3 @@
1
+ /** A wrong attribute or markup by the app's builder: the one error whose (Dutch) message a block shows. */
2
+ export declare class AuthorError extends TypeError {
3
+ }
@@ -0,0 +1,3 @@
1
+ /** A wrong attribute or markup by the app's builder: the one error whose (Dutch) message a block shows. */
2
+ export class AuthorError extends TypeError {
3
+ }
@@ -0,0 +1,69 @@
1
+ import { type CubeAppSession } from '../app.js';
2
+ import type { BlockScope, DefineOptions } from './define.js';
3
+ export type StateKind = 'loading' | 'empty' | 'error' | 'forbidden' | 'signed-out';
4
+ export interface BlockState {
5
+ kind: StateKind;
6
+ detail: string | null;
7
+ /** False for an error that trying again cannot solve: the state offers no retry. */
8
+ retry?: boolean;
9
+ }
10
+ /** Thrown by a block's load to show exactly this state. */
11
+ export declare class BlockStateError extends Error {
12
+ readonly state: BlockState;
13
+ constructor(state: BlockState);
14
+ }
15
+ /**
16
+ * What a failure looks like to the person on the page. Only an AuthorError carries its message: the blocks throw
17
+ * those for a wrong attribute, in Dutch. A server's text, a network failure or a client's English programmer error
18
+ * never does.
19
+ */
20
+ export declare function stateFor(error: unknown): BlockState;
21
+ /**
22
+ * Resolves once the page's own scripts have run. A block must not take the page session before the app had its
23
+ * chance to call `CubeApp.connect()` with its own options, or the block would start a second, default one.
24
+ */
25
+ export declare function pageReady(doc: Document): Promise<void>;
26
+ /** The class every block extends. Declared, not defined: the real one is made per window by `blockBase`. */
27
+ export declare abstract class CubeBlockShape extends HTMLElement {
28
+ static tag: string;
29
+ /** The block's own stylesheet text, adopted after BASE_CSS. */
30
+ static css: string;
31
+ /** False for blocks that render without data (cube-state): they never take the session to render. */
32
+ static needsSession: boolean;
33
+ /**
34
+ * False for a block that already shows what its own cube-data-change reports (cube-board): it does not reload on
35
+ * it.
36
+ */
37
+ static reloadsOnOwnChange: boolean;
38
+ static observedAttributes: string[];
39
+ /** The block's content root inside its shadow root. */
40
+ readonly body: HTMLElement;
41
+ /** Settles when the current (and any newer) load has finished. */
42
+ readonly updateComplete: Promise<void>;
43
+ /** Loads again: aborts a running load, shows the loading state, then calls `load`. */
44
+ refresh(): Promise<void>;
45
+ connectedCallback(): void;
46
+ disconnectedCallback(): void;
47
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
48
+ /** Renders the block into `body`. Throw to show a state; check `signal` before touching the DOM after an await. */
49
+ protected abstract load(signal: AbortSignal, session: CubeAppSession | null): Promise<void>;
50
+ /** The fields of `resource` the block shows after its load: one of them invented marks the whole block. */
51
+ protected shownFields(): string[];
52
+ /** Whether `field` of `resource` is invented while the resource itself exists: its column is marked. */
53
+ protected exampleField(session: CubeAppSession, resource: string, field: string): boolean;
54
+ /** `Voorbeeld — wordt niet opgeslagen` next to a write action of a wireframe; null in any other app. */
55
+ protected notSaved(session: CubeAppSession): HTMLElement | null;
56
+ protected session(): Promise<CubeAppSession>;
57
+ protected showState(state: StateKind | BlockState, detail?: string | null): void;
58
+ protected attr(name: string): string | null;
59
+ protected flag(name: string): boolean;
60
+ protected num(name: string, fallback: number): number;
61
+ protected json<T>(name: string): T | null;
62
+ protected emit(name: string, detail?: unknown): boolean;
63
+ }
64
+ export type BlockClass = typeof CubeBlockShape;
65
+ /** A block module's export: makes its element class on top of the window's base class. */
66
+ export type BlockFactory = (Base: BlockClass, scope: BlockScope) => BlockClass;
67
+ /** The header mark of an invented field: `Voorbeeld` on screen, the whole of exampleMissing read out. */
68
+ export declare function exampleBadge(doc: Document): HTMLElement;
69
+ export declare function blockBase(scope: BlockScope, options?: DefineOptions): BlockClass;